一、问题背景
本文主要针对《C++Primer》一书中,关于顶层const与底层const的相关描述中,就自己所遇到的一些疑惑,以及最终自己的一些理解,提供经验分享。
二、预备知识背景
1. 指针常量:
例如
int* const p
,const
紧挨着p
,表示其对p
的限定,而p
是一个int*
类型的指针,所以我们说这个指针(地址)不能再进行改变,所以称为指针常量。
2. 常量指针:
例如
const int *q
,const
紧挨着*q
,表示对*q
的限定,而*q
是指针q
所指向的内容,所以说,这个指针(地址)所指向的内容不能再进行改变,所以称为常量指针。
备注:
- 在《C++Primer》中,删去了指针常量这一概念,而用常量指针来表述,上面提到的指针常量,即地址不变。并且使用指向常量的指针来表述上述的常量指针,即指针指向的内容不变。这是完全是国内编程界与书本中的一些名词的解释上的区分,只需要理解它的本质即可,并不影响我们对书本的阅读。
- 网络上可能存在一些说法,说
int const *p
中的const
是对指针的p
的修饰,int *const p
中的const
是对p
所指向的内容的修饰,这只是一种说法上的区别,也可以说是一种记忆上的差别,这并不会导致对指针常量或者常量指针定义的差别化,大家阅读时不用过分纠结。
三、相关定义
3.1、狭义上
- 顶层const
表示指针本身是个常量。——《C++Primer》
- 底层const
表示指针所指向的对象是一个常量。——《C++Primer》
3.2、广义上
- 顶层const
更一般的,顶层const可以表示任意的对象是常量,这一点对任何数据类型都适用,如算术类型、类、指针等。——《C++Primer》
- 底层const
更一般的,任意的对象所指向的内容或者是任意对象所绑定的内容是一个常量,而修饰这个任意对象的const就称为底层const 。——个人见解
3.3、定义说明
3.3.1、狭义定义的狭义性
- 狭义上的定义,之所以称为狭义,是因为在针对
const
修饰的常量例如const int i
时,我们不方便按照定义对其进行解释,事实上我们都知道这个常量前面的const
是一个顶层const
(原因见后文),因为按照狭义定义,“表示指针本身是一个常量”,这句话咋一看有点无头无脑,实际上,它表述的含义是:const修饰指针时,如果此时限定的是指针(地址)本身不能进行更改,即指针常量,那么const
就是一个顶层const
。显然根据这个定义,我们似乎只能对对象(变量)为指针的声明进行描述,那么对于const int i
,这样一个普通的整型常量,显然i
并不是一个指针,那么上述的两个狭义上的定义就对其不适用了。
于是,在书本上就出现了广义上更一般的说法进行进一步补充,即上述的广义定义。
3.3.2、广义定义的普适性
- 顶层const
更一般的,顶层const可以表示任意的对象是常量,这一点对任何数据类型都适用,如算术类型、类、指针等。——《C++Primer》
- 底层const
更一般的,任意的对象所指向的内容或者是任意对象所绑定的内容是一个常量,而修饰这个任意对象的const就称为底层const 。——个人见解
以上附上定义,方便阅读。
下面我们使用广义定义对三个例子进行解释说明,来验证其普适性。这里顺便一提,我们将对象当成变量的别称即可,方便理解。
const int i
此处i
是变量,也就是定义中的对象,所以任意对象是常量。const int i
符合定义,所以其是,顶层const
。
const int *p
此处p
是变量,即对象,任意对象所指向的内容,那么就是指针p
所指向的内容,即*p是一个常量的话,那么该const
就是一个底层const
。由于const
修饰的是*p
,所以const int*p
是一个常量指针,即*p
是一个常量。综上,const int *p
中的const
是一个底层const
。
int * const p
此处p
是变量,即对象,根据顶层const
定义,任意对象是常量,即指针p是常量的话,那么就能证明该const是顶层const
,显然const
直接修饰p
,即指针变量,指针p
无法更改满足定义,故该const
是顶层const
。而该指针所指向的内容并没有进行限定,所以无法得知任意对象所指向的内容是否为常量,所以不满足底层const
的条件。
阶段总结
以上的三个例子很好的说明了广义定义的普适性和唯一性,所以我们使用该定义去判断const
的类型是可行的。
四、引用const
是底层const
4.1、问题引出
这句话出现在书中的一句代码的注释中,该部分代码如下:
int i = 0;
int *const p1 = &i;
const int ci = 42;
const int *p2 = &ci;
const int *const p3 = p2;
const int &r = ci;//用于声明引用的const都是底层const
就是这样一句不起眼的注释,却告诉了我们对于修饰引用的const
直接就是底层const
,这比给我们一个定义可方便多了。在读到这句话的时候,我也是一头雾水,知道总结出上面的广义底层const
定义之后,我再对这句话进行解释,发现是完全适用的,不过这需要插入一个小知识点之后才能进行说明。
4.2、知识点引入
常量引用的对象可以是非常量。——《C++Primer》
这个知识点我相信大家应该没有什么疑问,一般来说,引用的类型和与之绑定的对象的类型严格匹配。但是书上给出了我们两个例外,第一个就是常量引用,下面举例:
int val = 42;
const int &r = val;//r是一个常量引用,其对象是val.
显然,常量引用r的对象的类型是int类型的变量,而r的类型是const int类型,这是不匹配的,按照严格匹配的原则,也应该是const int val = 42
。但是前面说了常量引用是个例外,它允许其对象是非常量,所以此处对象是val这个变量也是可行的,当然这不是我们需要探讨的关键,我们需要引入为什么能够实现这一例外的知识点。
- 编译器对上述代码进行了优化,其优化过程如下:
const int temp = val;
const int &r = temp;
通过创建一个临时整型常量去接收val
这个整型变量,非常量是可以通过自动类型转换赋值给常量类型的,但是反之不行。所以temp
成功接收,然后使用temp
这个整型常量作为一个整型常量的引用的对象,这样就间接的实现的严格匹配。这个转换机制就是我们需要引入的知识点。
4.3、回归正题
利用广义定义解释
“用于声明引用的const都是底层const”
- 底层const
更一般的,任意的对象所指向的内容或者是任意对象所绑定的内容是一个常量,而修饰这个任意对象的const就称为底层const 。——个人见解
解释:
const int &r = val
任意对象即r
,其所绑定的内容,即对象val
,是否是一个常量,如果是那么说明该const
是一个底层const
。显然,如果没有引入4.2
节中的知识点,即转换机制。那么是无法解释的,因为常量引用的对象可以是非常量,这是一个很明确的知识点,而此处所绑定的对象val
是一个变量,那么是否就说明不满足底层const
的定义呢?答案是其实还是满足底层const
的定义的,因为引用r
绑定的对象其实并不是变量val
,而是转换机制中的临时量const int temp
.这个临时量是一个常量。这也刚好切合了底层const
的广义定义。
附带说一句,
正是由于绑定的是临时变量temp
而不是变量val
,所以,常量引用是不能通过引用对其对象(val
)进行修改的,因为根本就没绑定到val
。修改的只是temp
,编译器肯定是不希望发生这样的事的,所以就认定为非法。