什么是const
当我们需要定义一种变量,它的值不能被修改时,我们就使用cosnt来对变量类型进行限定:
const int i = 0;
此时变量 i 就被定义成了一个常量,任何试图为 i 赋值的行为都将引发错误。
const与初始化
因为const对象一旦创建后其值就不能改变,所以const对象必须初始化:
const int i = get_size(); // 正确:运行时初始化
const int j = 42; // 正确:编译时初始化
const int k; // 错误: k是一个未经初始化的常量
让const对象在文件间共享
默认状态下,const对象仅在文件内有效(包括编译时初始化的和运行时初始化的const对象)。当多个文件中出现了同名的cosnt变量时,等同于在不同文件中分别定义了独立的变量。
某些时候有这样一种const变量,它的初始值不是一个常量表达式,但又确实有必要在文件间共享。这种情况下,只在一个文件中定义const,而在其他多个文件中声明并使用它。解决的办法是,对于const变量,不管是声明还是定义都添加extern关键字,这样只需定义一次就可以了:
// file.cpp 定义并初始化了一个常量,该常量能够被其他文件访问
extern const int bufSize = func();
// file.h 头文件
extern const int bufSize; // 与 file.cpp 中定义的 bufSize 是同一个
const与引用
将引用绑定到常量上,称作对常量的引用,通常简称为常量引用。对常量的引用在定义时也要加const限定符:
const int ci = 1024;
const int &r1 = ci; // 正确:引用及其对应的对象都是常量
r1 = 42; // 错误:r1是对常量的引用,不能修改
int &r2 = ci; // 错误:试图让一个非常量引用指向一个常量对象
引用是“别名”而不是对象,因此引用的对象是常量还是非常量决定了其所能参与的操作,却无论如何都不会影响到引用和对象的绑定关系本身。
对const的引用和初始化
在学习引用的定义时我们学到,引用的类型必须与其引用的对象的类型一致,但有两个例外。其中一个例外就是在初始化const引用时可以用任意表达式作为初始值,只要表达式的结果能转换成引用的类型即可。尤其,允许为一个const引用绑定非常量的对象、字面值甚至是个一般表达式:
int i = 42;
double d = 1.0;
const int &r1 = d; // 正确:允许用double类型的变量来初始化int类型的常量引用
const int &r2 = 42; // 正确:r1是一个常量引用
const int &r3 = r1 * 2; // 正确:r3是一个常量引用
int &r4 = r1 * 2; // 错误:r4是一个普通的非常量引用
出现这种例外的原因要弄清楚当一个常量引用被绑定到另一种类型的过程:
double dval = 3.14;
const int &ri = dval;
编译器是这么处理这个过程的:
const int temp = dval; // 由双精度浮点数生成一个临时的整型变量,类型转换在这里完成
const int &ri = temp; // 让ri绑定这个临时量
const引用可能引用一个非常量对象
常量引用仅对引用可参与的操作做出了限定,对于引用的对象本身是不是一个常量未作限定。因为对象也可能是个非常量,所以允许通过其他途径改变它的值:
int i = 42;
int &r1 = i;
const int &r2 = i;
r1 = 0; // 正确:r1是一个非常量引用,改变了i的值
r2 = 0; // 错误:r2是一个常量引用,不能通过r2改变i的值
不允许通过r2来修改i的值,但是允许通过直接对 i 赋值或者对 r1 赋值来修改 i 的值。
指针与cosnt
指向常量的指针
与引用相同,可以让指针指向常量和非常量。指向常量的指针不能用于改变所指对象的值。
const int ci = 1024;
const int *p = &ci; // 常量被绑定到指向常量的指针上
常量对象的地址只能用指向常量的指针来存放,但是指向常量的指针可以存放常量和非常量对象的地址。
int i = 1;
const int *p = &i; // 指向常量的指针存放了非常量对象的地址
*p = 0; // 错误:不能通过指向常量的指针来改变非常量的值
常量指针
指针是对象而引用不是,因此指针本身可以是一个常量。常量指针与一般的常量对象一样,定义时必须初始化,而且初始化后就不能改变指向的地址。不变的是指针本身的值而不是指向的那个值。
int i1 = 1;
int *const p = &i1; // 定义了一个常量指针,指向了非常量i
int i2 = 2;
p = &i2; // 错误:常量指针指向的地址不能修改
*p = 2; // 正确:可以通过常量指针修改其指向的对象的值
顶层与底层cosnt
指针既可以指向一个常量对象,其本身也是一个对象,因此指针本身是不是常量和指针所指的是不是一个常量就是相互独立的问题了。顶层const表示指针本身是一个常量,底层const表示指针所指的对象是一个常量。
int i = 0; // 普通非常量 int 类型
int *const p1 = &i; // 常量指针:指针指向的地址不能变,指向的对象本身可以修改,顶层const
const int ci = 42; // 常量 int 类型,顶层const
const int *p2 = &ci; // 指向int常量的指针,本身可以改变指向的地址,但是只能指向常量,底层const
const int *const p3 = p2; // 常量指针,指向常量int,左边的是底层const,右边的是顶层const
const int &r = ci; // const引用全部是底层const,可以用非常量初始化r,但是不能通过r来修改非常量
进行对象的拷贝操作时,顶层const对拷贝并没有影响。而拷贝双方必须有相同的底层const资格,或者能够相互转换即只有非常量能拷贝到常量,反之不行。
// 执行对象拷贝时,顶层const没影响,但是底层const之间必须类型一致,或者可以互相转换
i = ci; // ci是顶层const,不影响拷贝操作
p2 = p3; // p3的顶层const部分对拷贝没影响,底层const部分和p2的底层const部分的类型相同,可以拷贝
int *p = p3; // p3有底层const的定义,而p没有,常量不能转换成非常量
p2 = p3; // p2和p3的底层const定义都是const int,可以拷贝
p2 = &i; // 非常量 int* 可以向常量const int* 转换
int &r = ci; // 常量只能绑定到const引用
const int &r2 = i; // 非常量可以绑定到const引用