前言
C++入门路上的第一个纠结:引用和指针,两个好好的东西,跟const混上之后,就开始在你脑子里打架了。。。
对常量的引用、常量引用;指向常量的指针、常量指针(底层const、顶层const)。如果单看名字的话,这些概念很清晰,似乎没什么奇怪的呀。
本文就来 扣扣他们的概念,捋一捋他们的关系,不过这都不重要,这个过程只是让它们在你的脑子里先扭打在一起,再分帮结派的站在两边,从而深刻明了的将它们区分开,并了解它们的特性。
最终,最重要的还是搞清楚:在代码使用中时,它们能干啥、不能干啥
先定义几个全局变量在前面:
int i=1 , j=2;
1.先介绍一下引用和指针
- 引用不是对象:int& a=i; 这其中的a不是一个对象,只是i的别名。故它具有以下性质:
a. 引用不能赋值和拷贝(对引用进行赋值,是对被引用对象赋值)
b. 引用不能重新绑定一个对象(这很合理,因为压根儿也没有给引用本身赋值的办法呀…)
c. 引用在定义时必须初始化
可以将引用理解为一个“常量”,它是不可变、必须初始化的。 - 引用的使用要求:
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 &" 类型的引用(非常量限定)
}
- 指针是一个对象:int* p=&i; 这其中p是一个对象。故它具有以下性质:
a. 指针可以赋值和拷贝
b. 在其生命周期内可以先后指向不同的对象(即指针的值是可以修改的)
c. 指针无须在定义时初始化(无须,但是不是无需,避免野指针,从你我做起~)
指针是一个存放地址值的变量,它存放的值随你改。 - 指针的使用要求:
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 *”
}
针对上面的性质和要求:
- 引用类型和被绑定对象类型要匹配
- 指针别瞎给值,好好地往对象上指,或者给个nullptr
- 指针定义的时候记得初始化,没得指就给nullptr
2. 引用与const限定符
对引用加上const的修饰,仅对引用可参与的操作做出了限定,引用的对象本身是否是个常量未作限定。
- 从结合形式上来看:引用与const的结合形式只有一种:
const int k=3;
const int& cy=k;
[如果非要说,其实另一种也行:
int m=1;
int& const cy=m;//这也不是人能写出来的玩意儿啊(小声哔哔)
这里的const限定符其实是没起啥效果的,因为人家引用本来就不可变(可以理解为给引用加顶层const属性,这操作是没啥意义的)。
直接 int& cy=m; 也是一样的效果]
- 故“常量引用”与“对常量的引用”不用刻意区分,就当前者是后者的简称就好(后文即是如此)
严格来说,“常量引用”并不存在,因为引用不是一个对象,尤其引用是不可修改的(引用绑定的对象是不可变的)
让引用保持恒定不变这个事儿本来就是不存在的。P55
-
2.2 引用与const结合后带来的例外
常量引用允许用任意表达式作为初始值,只要该表达式能转换成引用的类型即可:
分解翻译一下:- 对常量的引用 可以绑定字面值、一般表达式;(不再是只能绑定在对象上)
- 对常量的引用 可以绑定另外类型的对象;(类型不用严格匹配了)(另外类型的对象 包括 对应类型的非常量对象)
这就离谱。。。没错,下面这些代码都是正确的:
double data = 3.14, bata=4.15;
const int& cy1 = data;//cy1是3
const int& cy2 = 6.66;//cy2是6
- 正常代码中,一般也没人拿引用去绑定别的类型的对象或是绑定字面值常量(主要用于函数传参),所以上面的两个例外知道就好,使用中不用太在意
- 例外中有一点需要注意:常量引用时,被引用对象可以是常量也可以是非常量。(这是个类型匹配的例外:引用是常量,而被引量不是常量)
- 但如果被引用对象是常量时,一定要用常量引用
- 实际上,引用就是用来方便的修改变量的值的,所以常量引用基本没用。那它存在的意义是什么呢?参考这个链接
// 对引用加上const的修饰,仅对引用可参与的操作做出了限定,引用的对象本身是否是个常量未作限定。
int a=1;
const int b=2;
/*对常量的引用*/
const int& cy1=a;
const int& cy2=b;
//int& cy3=b;//error: 对常量的引用必须是常量引用
先忘掉2.1、2.2、2.3节所看到的所想到的东西,从如下这写字眼的本质出发,思考一下下面这些问题
(思考完就赶紧忘了吧,不是啥好东西,就当没看过)
问题一:“常量引用”一定是“对常量的引用”吗?
单从字面来看:
1.对常量的引用——被引用对象是常量;常量引用——引用本身是常量
2.常量引用的被引用对象不一定是常量
故,“常量引用”不一定是“对常量的引用”。
问题二: “对常量的引用”一定是“常量引用”吗?
是的,“对常量的引用”必须使用“常量引用”
故 “对常量的引用”一定是“常量引用”。
问题三:“对常量的引用”一定引用的是常量吗?
不是的,参考上面2.2中的例外。
3. 指针与const限定符
整理一下思绪,大的💊来了。。。
-
3.1 指向常量的指针—与—常量指针
与引用不同,指针是一个对象,它本身的值是可变的,所以让指针的值保持恒定不变这件事儿是真实存在的。完成这个事儿的就叫:常量指针(const指针,顶层const)
指向一个常量对象,这个指针必须得用“指向常量的指针”(指针 to const,底层const)(但是指向常量的指针有个例外,它可以指向一个非常量的对象)
- 从结合形式上说起:
指针*与const的结合有两种形式:
const int* p = &i;//指向常量的指针:指针本身可变,不能通过该指针修改被指对象(无论被指对象是否是常量对象)
int* const p = &i;//常量指针:指针本身是不可变的,能否通过该指针修改被指对象取决于此对象是否是常量对象
//(若被指对象是常量,则该常量指针也必是“指向常量的常量指针”)
- 常量指针 与 顶层const
a. 常量指针——不变的是指针本身,而非指针指向的对象(即仅限制了在其生命周期内可以先后指向不同的对象 这一性质);很自然地,既然这个指针不可变,那他在初始化的时候一定是需要初始化的。
b. 顶层cosnt——表示指针本身是个常量(P57);(一般变量也是有“顶层const”这一概念的P57P191) - 指向常量的指针 与 底层const
a. 指向常量的指针——指针本身可变,但不能通过该指针修改被指对象的值。(与“对常量的引用”类似,此处也有一个类型匹配的例外:指向常量的指针可以指向一个对应类型的非常量对象)
b. 底层const——表示指针所指对象是一个常量(P57);(对于一般变量没有“底层const”这一概念,底层const只与指针和引用等复合类型有关)
- 更一般的,顶层const可以表示任意的对象是常量,这一点对任何数据类型都适用(顶层const作用于对象本身)
- 底层const只与指针和引用有关
- 指针类型即可以是顶层const也可以是底层const(const int* const ptr=&i ;)
- 引用类型只能是底层const(const int ci=1; const int& r=ci;)
- 拷贝
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
- 指向常量对象的指针——必须是“指向常量的指针”;
而“指向常量的指针”——不一定指向常量对象。 - 若想将指针本身定为常量,则用“常量指针”,若同时常量指针指向的是常量对象,则该常量指针也必须是“指向常量的常量指针”
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与指针] 在强制类型转换、函数传参、函数重载等部分需要注意
这里讲的十分透彻
https://www.cnblogs.com/ider/archive/2011/07/22/cpp_cast_operator_part2.html
-
const_cast——只能改变运算对象的底层const(去掉底层const性质)
底层const——表示指针所指对象是一个常量。(这只是底层const一厢情愿的认为它指了个常量P56)
故具备底层const属性的指针一定是指向常量的指针(被指对象可以是常量,也可以是非常量) -
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。可以用于想修改被指非常量对象,但却只知道其 指向常量的指针 时。
- 函数形参的顶层const会被忽略(用实参初始化形参时,会忽略掉形参的顶层const)
当形参有顶层const时,传给他常量对象 或非常量对象都是可以的 - 底层const的形参(底层const是不会被忽略的)
函数形参的初始化与一般变量的初始化方式是一样的,故——底层const的形参可以用常量初始化,也可以用非常量初始化(参考前文2.2和3.1/3中“常量引用”与“指向常量的指针”的例外情况)
故当形参有底层const时,传给他常量对象 或非常量对象都是可以的
(但是如果形参没有底层const,传给它const对象是不可以的) - 把函数不会改变的形参定义成普通的引用是错误的,定义成“常量引用”(具备底层const)(速度又快,又安全,支持的实参类型又多)
- 先看例子:
/*顶层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);//正确:新函数
- 简单分析一下:
形参的顶层const会被忽略。故顶层const不能讲形参区分开来。
结语
先这样吧,const限定符还有很多其他的神奇用处与纠结之处。
学到我再来总结。