深入了解const
前言
之前在学习C++的时候,对于const等相关概念了解的不是很清楚,最后在使用时就很痛苦😖
这次彻底的总结一下const相关的知识点
阅读变量声明
实际上,const位于变量声明的不同位置,会产生不同的作用。因此,首先我们需要学会如何阅读变量的声明语句,从而了解变量的具体类型,获取const修饰的对象,才能知道其作用。
这里我们依照由右至左规则,可以有效的分解变量的类型。
整个由右至左规则如下所示:
- 从变量名称开始
- 向右依次列出所有的关键词,直到遇到不匹配的右括号或者结束停止
- 向左依次列出所有的关键词,直到遇到不匹配的左括号或者结束停止
如果因为括号停止,则跳出括号,重新执行步骤2.
这里我们举一个比较有挑战性的例子
1
char (*(*x())[])();
下面给出按照上述分析的步骤
- 根据步骤1,找到变量名称
x
,即char (*(*
x
())[])();
- 根据步骤2
- 找到
()
,即char (*(*
x()
)[])();
,因此为无参函数 - 右边遇到不匹配右括号,结束
- 找到
- 根据步骤3
- 找到
*
,即char (*(
*x()
)[])();
,因此返回值为指针 - 左边遇到不匹配左括号,结束
- 找到
- 根据步骤4,跳出括号,即
char (*
(*x())
[])();
- 根据步骤2
- 找到
[]
,即char (*
(*x())[]
)();
,因此其指针指向一个数组 - 右边遇到不匹配右括号,结束
- 找到
- 根据步骤3
- 找到
*
,即char (
*(*x())[]
)();
,因此该数组元素都是指针 - 左边遇到不匹配左括号,结束
- 找到
- 根据步骤4, 跳出括号,即
char
(*(*x())[])
();
- 根据步骤2
- 找到
()
,即char
,因此该指针是指向无参函数的指针(*(*x())[])();
- 右边完结,结束
- 找到
根据步骤3
- 找到
char
,即,因此函数的返回值为char类型char (*(*x())[])();
- 左边遇到完结,结束
综上可知,该变量是一个无参函数,其返回值是一个指针,该指针指向一个数组,数组中的元素都是指针,这些指针指向返回值为
char
的无参函数。- 找到
const性质
const表示修饰的值不能改变,这就是该关键词的性质。
那么实际上,根据上面的规则,我们很容易完全理解const的种种特性
const type a;
/type const a;
即const用来修饰type
类型,也就是变量a
的值不能进行改变const type *a;
/const type &a;
根据前面的规则,这是一个指针/引用,其指向/引用一个const type
类型——也就是其指向/引用的值不能修改,但其本身并没有要求type * const a;
根据前面的规则,这是一个const
对象,该对象是一个指向type
的指针——也就是该指针不能重新指向新的对象,但是其指向的值可以进行任意的修改const type * const a;
根据前面的规则,这是一个const
对象,该对象是const type *
类型,即该对象是一个指针,指针指向const type
类型——即该指针既不能重新指向新的对象,也不可以修改其指向的值。总的来说,基本就是按照前面的规则解释变量类型,其中
const
修饰的对象的值不能改变
顶层const和底层const
顶层const
对于对象本身是一个常量,不可更改对象本身的值,但在对像是指针或引用的情况下,可能修改其指向或引用的值,则称为顶层const,如int * const p1;
底层const
如果对象是指针或引用,其指向或引用的对象的值不可更改,则称为底层const,如const int &r1;
差别
在使用时,顶层const和底层const差别十分大,这里简单说一下常见的差别
赋值、初始化
对于顶层const,由于const可以兼容非const,因此对于赋值对象是否为常量不影响操作,如下所示
1
2
3int *p1 = nullptr, *const p2 = nullptr;
int *const p3 = p1, *const p4 = p2; //有效,const兼容非const的p1对于底层const,虽然const可以兼容非const,但是如果在底层const的情况下也进行兼容,可能会修改const类型的值,如下所示
1
2
3
4
5const int val = 0;
int *p1 = nullptr;
int const **p2 = &p1; //如果底层const可以被兼容,则&p1为指向变量的指针,p2为指向常量的指针,则p2可以兼容&p1
*p2 = &val; //此时p1指向了&val
*p1 = 1; //由于p1是指向变量的指针,因此其值可以进行改变,从而修改了常量val的值为了避免上面情况的发生,这里规定底层const不能兼容非const,如下所示
1
2
3int const val = 0;
int const &r1 = val, *const p1 = &val; //有效,其底层const类型一致,顶层const可以兼容
int * const *p2 = &val; //无效,因为其底层const不一致:p2底层指向变量,而&val底层是常量
注意点
这里需要注意一下typedef
关键字。即通过typedef
关键字,其生成新的基本数据类型,并不是简单的宏替换。这点在const中特别关键,如下
1 | typedef char *pstring |
如果仅仅将typedef
当作简单的宏替换,那么p2
的定义进行展开就是p1
,其是一个指向常量char的指针,即p1
的值可以进行任意的修改,但是p1
指向的值不能进行修改
但实际上,我们需要将pstring
当作一个基本数据类型看待,即这是一个常量pstring类型,即p2
不能随意更改其值;而由于pstring
是指针类型,实际上我们仍然可以修改其指向的值。
可以看到,两种理解方法,得出来的结论完全相反。