《C++Primer》之——引用、指针,以及它们与const的爱恨纠葛

前言

C++入门路上的第一个纠结:引用和指针,两个好好的东西,跟const混上之后,就开始在你脑子里打架了。。。
对常量的引用、常量引用;指向常量的指针、常量指针(底层const、顶层const)。如果单看名字的话,这些概念很清晰,似乎没什么奇怪的呀。
本文就来 扣扣他们的概念,捋一捋他们的关系,不过这都不重要,这个过程只是让它们在你的脑子里先扭打在一起,再分帮结派的站在两边,从而深刻明了的将它们区分开,并了解它们的特性
最终,最重要的还是搞清楚:在代码使用中时,它们能干啥、不能干啥

先定义几个全局变量在前面:
int i=1 , j=2;

1.先介绍一下引用和指针

  • 1.1 引用 &

  1. 引用不是对象:int& a=i; 这其中的a不是一个对象,只是i的别名。故它具有以下性质
    a. 引用不能赋值和拷贝(对引用进行赋值,是对被引用对象赋值)
    b. 引用不能重新绑定一个对象(这很合理,因为压根儿也没有给引用本身赋值的办法呀…)
    c. 引用在定义时必须初始化
    可以将引用理解为一个“常量”,它是不可变、必须初始化的。
  2. 引用的使用要求
    a. 引用只能绑定在对象上(字面值常量、一般表达式是不可以被引用的)(常量引用例外
    b. 引用的类型必须要和绑定的对象严格匹配 (常量引用例外,而且例外的贼离谱!P55)

下面是测试代码

void main() {
	double data = 3.14, bata=4.15;
	double& y1 = data;
	/*测试1.引用的性质*/
	y1 = bata;//对引用y赋值,是对被引用对象data赋值
	//double& y1 = bata;//试图将y1重新绑定一个对象。error:重定义
	cout << data << endl 
		 << y1 << endl;
		 
	/*测试2.引用的使用要求*/
	//int& y2 = 666;//error:非常量引用的初始值必须为左值
	//int& y3 = data;//error:无法用 "double" 类型的值初始化 "int &" 类型的引用(非常量限定)
}
  • 1.2 指针 *

  1. 指针是一个对象:int* p=&i; 这其中p是一个对象。故它具有以下性质
    a. 指针可以赋值和拷贝
    b. 在其生命周期内可以先后指向不同的对象(即指针的值是可以修改的)
    c. 指针无在定义时初始化(无须,但是不是无需,避免野指针,从你我做起~)
    指针是一个存放地址值的变量,它存放的值随你改。
  2. 指针的使用要求
    a. 指针必须指向变量(左值或函数标识符)(不能指向字面值常量或一般表达式,没有例外
    b. 指针类型要和它指向的对象严格匹配(eg:指向int的指针,其指向的对象必须是int)(“指向常量的指针”指向非常量是个例外

下面是测试代码

void main() {
	double data = 3.14, bata=4.15;
	double* p1=&data;
	/*测试1.指针的性质*/
	p1=&bata;
	
	/*测试2.指针的使用要求*/
	//double* p2=&(2.33);//error: 表达式必须为左值或函数标识符。这也不是人能写出来的玩意儿a
	//p1=1024;//error:不能将 "int" 类型的值分配到 "double *" 类型的实体。人确实有可能写出来这玩意儿
	p1=0;//这是可以的,相当于赋了个nullptr	
	//int* p3=&data;//error: 无法从“double *”转换为“int *”
}
  • 1.3 代码使用中要注意

针对上面的性质和要求:

  1. 引用类型和被绑定对象类型要匹配
  2. 指针别瞎给值,好好地往对象上指,或者给个nullptr
  3. 指针定义的时候记得初始化,没得指就给nullptr

2. 引用与const限定符

对引用加上const的修饰,仅对引用可参与的操作做出了限定,引用的对象本身是否是个常量未作限定。

  • 2.1 对常量的引用——常量引用

  1. 从结合形式上来看:引用与const的结合形式只有一种:
	const int k=3;
	const int& cy=k;

  [如果非要说,其实另一种也行:
    int m=1;
    int& const cy=m;//这也不是人能写出来的玩意儿啊(小声哔哔)
  这里的const限定符其实是没起啥效果的,因为人家引用本来就不可变(可以理解为给引用加顶层const属性,这操作是没啥意义的)。
  直接 int& cy=m; 也是一样的效果]

  1. 故“常量引用”与“对常量的引用”不用刻意区分,就当前者是后者的简称就好(后文即是如此)

    严格来说,“常量引用”并不存在,因为引用不是一个对象,尤其引用是不可修改的(引用绑定的对象是不可变的)
    让引用保持恒定不变这个事儿本来就是不存在的。P55
  • 2.2 引用与const结合后带来的例外

    常量引用允许用任意表达式作为初始值,只要该表达式能转换成引用的类型即可:
    分解翻译一下:

    1. 对常量的引用 可以绑定字面值、一般表达式;(不再是只能绑定在对象上)
    2. 对常量的引用 可以绑定另外类型的对象;(类型不用严格匹配了)(另外类型的对象 包括 对应类型的非常量对象

    这就离谱。。。没错,下面这些代码都是正确的:

	double data = 3.14, bata=4.15;
	const int& cy1 = data;//cy1是3
	const int& cy2 = 6.66;//cy2是6
  • 2.3 const引用的使用要注意

  1. 正常代码中,一般也没人拿引用去绑定别的类型的对象或是绑定字面值常量(主要用于函数传参),所以上面的两个例外知道就好,使用中不用太在意
  2. 例外中有一点需要注意:常量引用时,被引用对象可以是常量也可以是非常量。(这是个类型匹配的例外:引用是常量,而被引量不是常量)
  3. 但如果被引用对象是常量时,一定要用常量引用
  4. 实际上,引用就是用来方便的修改变量的值的,所以常量引用基本没用。那它存在的意义是什么呢?参考这个链接
// 对引用加上const的修饰,仅对引用可参与的操作做出了限定,引用的对象本身是否是个常量未作限定。
int a=1;
const int b=2;
/*对常量的引用*/
const int& cy1=a;
const int& cy2=b;
//int& cy3=b;//error: 对常量的引用必须是常量引用
  • 2.4 来思考几个充满哲理的问题

先忘掉2.1、2.2、2.3节所看到的所想到的东西,从如下这写字眼的本质出发,思考一下下面这些问题
(思考完就赶紧忘了吧,不是啥好东西,就当没看过)

问题一:“常量引用”一定是“对常量的引用”吗?
 单从字面来看:
  1.对常量的引用——被引用对象是常量;常量引用——引用本身是常量
  2.常量引用的被引用对象不一定是常量
 故,“常量引用”不一定是“对常量的引用”。
问题二: “对常量的引用”一定是“常量引用”吗?
 是的,“对常量的引用”必须使用“常量引用”
 故 “对常量的引用”一定是“常量引用”。
问题三:“对常量的引用”一定引用的是常量吗?
 不是的,参考上面2.2中的例外。

3. 指针与const限定符

  整理一下思绪,大的💊来了。。。

  • 3.1 指向常量的指针—与—常量指针

    与引用不同,指针是一个对象,它本身的值是可变的,所以让指针的值保持恒定不变这件事儿是真实存在的。完成这个事儿的就叫:常量指针(const指针,顶层const)
    指向一个常量对象,这个指针必须得用“指向常量的指针”(指针 to const,底层const)(但是指向常量的指针有个例外,它可以指向一个非常量的对象)
  1. 从结合形式上说起
    指针*与const的结合有两种形式:
const int* p = &i;//指向常量的指针:指针本身可变,不能通过该指针修改被指对象(无论被指对象是否是常量对象)

int* const p = &i;//常量指针:指针本身是不可变的,能否通过该指针修改被指对象取决于此对象是否是常量对象
				  //(若被指对象是常量,则该常量指针也必是“指向常量的常量指针”)
  1. 常量指针 与 顶层const
    a. 常量指针——不变的是指针本身,而非指针指向的对象(即仅限制了在其生命周期内可以先后指向不同的对象 这一性质);很自然地,既然这个指针不可变,那他在初始化的时候一定是需要初始化的
    b. 顶层cosnt——表示指针本身是个常量(P57);(一般变量也是有“顶层const”这一概念的P57P191)
  2. 指向常量的指针 与 底层const
    a. 指向常量的指针——指针本身可变,但不能通过该指针修改被指对象的值。(与“对常量的引用”类似,此处也有一个类型匹配的例外:指向常量的指针可以指向一个对应类型的非常量对象
    b. 底层const——表示指针所指对象是一个常量(P57);(对于一般变量没有“底层const”这一概念,底层const只与指针和引用等复合类型有关)
  • 3.2 顶层const 与 底层const

  1. 更一般的,顶层const可以表示任意的对象是常量,这一点对任何数据类型都适用(顶层const作用于对象本身)
  2. 底层const只与指针和引用有关
  3. 指针类型即可以是顶层const也可以是底层const(const int* const ptr=&i ;)
  4. 引用类型只能是底层const(const int ci=1; const int& r=ci;)
  5. 拷贝
    a.顶层const对象拷贝给另一个对象时,顶层const属性不影响
    b.底层const对象拷贝给另一个对象时,另一个对象必须具有相同的底层const资格
int i=1;
const int* ptrc = &i;
int* const cptr = &i;
const int* const cptrc=&i;

//int* ptr1=ptrc;//error
int* ptr1=cptr;//正确

ptrc=cptr;//正确:顶层const拷贝给底层const
//cptr=ptrc;//error:底层const拷贝给不具备底层const属性的顶层const
cptrc=ptrc;//正确:顶层const拷贝给底层const
  • 3.3 指针与const 使用时需注意

  1. 指向常量对象的指针——必须是“指向常量的指针”;
    而“指向常量的指针”——不一定指向常量对象。
  2. 若想将指针本身定为常量,则用“常量指针”,若同时常量指针指向的是常量对象,则该常量指针也必须是“指向常量的常量指针”
const int ci = 1;
int i=2;

/*指向常量的指针*/
const int* cptr1 = &ci;//正确
const int* cptr2 = &i;//正确
/*常量指针*/
//int* const cptr3 = &ci; //error:"const int *"类型的值不能用于初始化"int *const"类型的实体
const int* const cptr4 = &ci;//正确(指向常量的常量指针)
int* const cptr3 = &i;//正确

4. [const与指针] 在强制类型转换、函数传参、函数重载等部分需要注意

  • 4.1 底层const与const_cast

这里讲的十分透彻
https://www.cnblogs.com/ider/archive/2011/07/22/cpp_cast_operator_part2.html

  1. const_cast——只能改变运算对象的底层const(去掉底层const性质)
    底层const——表示指针所指对象是一个常量。(这只是底层const一厢情愿的认为它指了个常量P56)
    故具备底层const属性的指针一定是指向常量的指针被指对象可以是常量,也可以是非常量

  2. const_cast使用后的效果
    a. 被指对象是常量:
      此时去掉指针的底层const属性,可以通过指针进行修改被指对象值的操作,不过会产生未定义的后果(被指对象的值不会变,会发生啥看编译器)。
    b. 被指对象是非常量
      此时去掉指针的底层const属性,可以通过指针正常修改被指对象的值。

    下面看例子a

	const int ci = 21;
	const int* ptrc = &ci;
	int* ptr = const_cast<int*>(ptrc);
	*ptr = 7;//未定义的行为,试图对const对象ci进行改值
	cout<<*ptr<<endl
		<<*ptrc<<endl
		<<ci<<endl;
输出结果:7;7;21。神奇:ci没变,ptrc和ptr指向的值却变了。  

  下面看例子b

	int ci = 21;
	const int* ptrc = &ci;
	int* ptr = const_cast<int*>(ptrc);
	*ptr = 7;//正常行为
	cout<<*ptr<<endl
		<<*ptrc<<endl
		<<ci<<endl;
} 
输出结果:7;7;7。可以用于想修改被指非常量对象,但却只知道其 指向常量的指针 时。  
  • 4.2 函数传参与顶层const、底层const

  1. 函数形参的顶层const会被忽略(用实参初始化形参时,会忽略掉形参的顶层const)
    形参有顶层const时,传给他常量对象非常量对象都是可以的
  2. 底层const的形参(底层const是不会被忽略的)
    函数形参的初始化与一般变量的初始化方式是一样的,故——底层const的形参可以用常量初始化,也可以用非常量初始化(参考前文2.2和3.1/3中“常量引用”与“指向常量的指针”的例外情况)
    故当形参有底层const时,传给他常量对象非常量对象都是可以的
    (但是如果形参没有底层const,传给它const对象是不可以的)
  3. 把函数不会改变的形参定义成普通的引用是错误的,定义成“常量引用”(具备底层const)(速度又快,又安全,支持的实参类型又多)
  • 4.3 函数重载与顶层const、底层const

  1. 先看例子:
/*顶层const对重载的影响*/
void func(int i);
void func(const int i);//error:重复声明

void func(int* i)
void func(int* const i);//error: 重复声明
/*底层const对重载的影响*/
void func(int& i);
void func(const int& i);//正确:新函数

void func(int* i)
void func(const int* i);//正确:新函数
  1. 简单分析一下:
    形参的顶层const会被忽略。故顶层const不能讲形参区分开来。

结语

先这样吧,const限定符还有很多其他的神奇用处与纠结之处。
学到我再来总结。


----------书山有路勤为径---------
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值