两个比较容易混淆的概念。
0. 记忆方法
const int * p;
int * const p;
int const * p;
这是常见的三种写法,按照从左至右的顺序记忆,按照从右至左的顺序理解。
记忆
只关注const 和 * 的先后先后顺序,const读做常量,*读做指针。
参考《Effective c++》Item21上的做法,如果const位于星号的左侧,则const就是用来修饰指针所指向的变量,即指针指向为常量;如果const位于星号的右侧,const就是修饰指针本身,即指针本身是常量。
比如
const int * p; // 常量指针
int * const p; // 指针常量
int const * p; // 常量指针
理解
将 * 读做指向。
const int * p; // p指向一个const
int * const p; // 常量p 指向一个 int
int const * p; // p指向一个const
1. 指针常量
int * const p; // 常量p 指向一个 int
重点在“常量”,这是一个常量,也是一个指针,也就是说,这个指针一旦指向了一个地址,那么它就不可以再指向别的地址空间。
2. 常量指针
const int * p; // p指向一个const
int const * p; // p指向一个const
重点在“指针”,这是一个用const修饰的指针,它既可以指向常量,也可以指向变量。
所谓的“常量”指针或者“常量”引用,不过是指针或引用“自以为是”罢了,只是不能通过该指针或引用来修改对象的值。
code:
int a = 15;
const int *p = &a;
cout << *p << endl;
output:
15
通过指针修改
int a = 15;
const int *p = &a;
*p = 10; // 通过常量指针修改
cout << *p << endl;
output:
编译报错
通过变量自身修改
int a = 15;
const int *p = &a;
a = 10; // 通过变量自身修改
cout << *p << endl;
output:
10
3. 底层const和顶层const
对于一个指针对象来说,它本身可以是const的,也就是说它只可以指向初始化的地址,不可以再指向其他地址空间。另外,指针所指的对象也可以是const的,这就叫底层const,也就是说它所指的地址空间或者对象是不可以通过它修改的(前边说过,这是“自以为是”的)。
在执行拷贝操作时,顶层const不受影响,拷入和拷出的对象是否是常量没有关系。
而底层const不可以,拷入和拷出的对象必须具有相同的底层const资格,或者两个对象的数据类型必须能够转换,一般来说,非常量可以转换为常量,反之则不行。
几个转换的例子
// 变量声明
const int v2 = 0;
int v1 = v2;
int *p1 = &v1, &r1 = v1;
const int *p2 = &v2, *const p3 = &i, &r2 = v2;
// 下列哪些是合法的?
r1 = v2; // 合法
p1 = p2; // 不合法,p1是变量,p2是底层const。二者要么都有底层const,要么是变量赋给常量。
p2 = p1; // 合法, p1是变量,p2是底层const。执行的是变量转到常量。
p1 = p3; // 不合法,p1是变量,p3是顶层const+底层const,二者要么都有底层const,要么是变量赋给常量。
p2 = p3; // 合法,p2是底层const,p3是顶层const+底层const,二者都有底层const资格。
4. 通过地址修改const修饰的变量
使用const修饰变量以后,该变量就成为了一个常量。使用赋值的方式对该变量直接进行修改是行不通的。但是可以通过变量的地址进行修改(做一次强制类型转换)。
const int a = 2;
int *p = const_cast<int*>(&a);
(*p)++;
cout << a << endl;
cout << (*p) << endl;
上边代码输出的是
2
3
为什么?
再次输出变量a的地址和p所指向的地址,也是相同的。但是输出的时候a却显示3。原因在于:C++中的常量折叠。指const变量(即常量)值放在编译器的符号表中,计算时编译器直接从表中取值,省去了访问内存的时间,从而达到了优化。