两个比较容易混淆的概念。

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变量(即常量)值放在编译器的符号表中,计算时编译器直接从表中取值,省去了访问内存的时间,从而达到了优化。