C++ 中的 const 限定符
const 需要与“常量”这个说法做一个区分。
const 就是 const,常量是常量,有时候可以通用,但有时候又不能混淆。
const 限定符
const 修饰的对象的值不能被改变。可以用来提高警惕,防止程序修改 const 限定对象的值。
格式: const int bufSize = 512;
这样,我们就说 bufSize 是一个常量。任何想修改 bufsize 的值都会报错。
因为 const 对象一旦初始化便不能修改,因此 const 对象必须要初始化。
const 初始化
如果利用一个变量去初始化 const 对象,那么这个变量是不是 const 都无关紧要。
int i = 42;
const int ci = i; // 正确 初始化 const 对象
int j = i; // 正确 简单初始化 int 类型
ci 的常量特性,只是在修改 ci 的值的时候,才能体现出来。
默认状态下,const 对象仅仅在文件内有效
当以编译时初始化的方式定义一个 const 对象时,就如对 bufSize 的定义一样:const int bufSize = 512;
编译器在编译过程中会把用到的地方都替换成对应的值。也就是说,编译器会找到代码中所有用到 bufSize 的地方,然后用 512 替换。
为了执行上述替换,编译器必须知道变量的初始值。如果程序包含多个文件,则每个用了 const 对象的文件都必须得能访问到它的初始值才行。要做到这一点,就必须在每个用到变量的文件都有对它的定义。为了支持这一用法,同时避免对同一变量的重复定义,默认情况下, const 对象被设定为仅在文件内有效。当多个文件中出现了同名的 const 变量时,其实等同于在不同文件中分别定义了独立的变量。
某些时候有这样一种 const 变量,它的初始值不是一个常量表达式,但又确实有必要在文件间共享,这种情况下,我们不希望编译器为每个文件分别生成独立的变量。相反,我们想让这类 const 对象像其他(非 const)对象一样,也就是说,只在一个文件中定义 const ,而在其他多个文件中,声明并使用它。
解决办法是,对于 const 变量不管是声明还是定义,都添加 extern 关键字,这样只需定义一次就可以了:
// file_1.cc 定义并初始化了一个常量,该常量能被其他文件访问
extern const int bufSize = fcn();
// file_1.h 头文件
extern const int bufSize;
这样的话,就可以通过包含 file_1.h 头文件从而实现 bufSize 对象的共享,同时 bufSize 只会初始化一次。
Note:如果想要在多个文件之间共享 const 对象,必须在变量的定义之前添加 extern 关键字。
const 的引用
可以把引用绑定到 const 对象上,我们称之为**对常量的引用 (references to const) **对常量的引用不能被用作修改它所绑定的对象:
const int ci = 1024;
const int& r1 = ci; // 正确 const 引用
r1 = 42; // 错误:无法通过 const 引用修改对象的值
int& r2 = ci; // 错误:const 对象需要使用 const 引用,普通引用会编译错误
“常量引用”与 “对 const 的引用”:C++ 程序员经常把词组 “对 const 的引用”称为“常量引用”,这只是个简称而已。严格来说,并不存在常量引用,因为 C++ 不允许随意改变引用所绑定的对象,所以从这一层意义上理解所有的引用都是常量。
所以还是“对 const 的引用”要准确一点。
对 const 的引用的初始化
一般来说,引用的类型必须与其所引用的对象的类型一致。但是初始化对 const 的引用时,可以允许用任意表达式作为初始值,只要表达式的结果能够转换成引用的类型即可。int& r = 42;
这段代码会引发异常,因为引用要引用到对象上const int& r = 42;
这段代码并不会引发异常,因为编译器做了额外的操作
例如
double d = 3.14;
const int& r = d;
这段代码是可以编译通过的。编译器会针对 const int& r = d;
创建一个临时 const 变量,如下:
double d = 3.14;
const int temp = d;
const int& r= temp;
在这种情况下, r 绑定了一个临时量 (temporary) 对象。
当 r 不是对 const 的引用时,int& r = d;
会引发编译错误,因为引用的类型与对象的类型不一致。
对 const 的引用可能引用一个非 const 的对象
常量引用仅对引用可参与的操作做出了限定,对于引用的对象本身是不是一个常量未作限定。
就是说可以针对普通变量创建对 const 的引用,通过这个引用是不能去修改对象的值的。
int i = 42;
int& r1 = i;
const int& r2 = i;
r1 = 10; // ok
r2 = 24; // 错误: 对 const 的引用无法去修改值
r2 绑定 i 是合法的操作,然而 r2 是不能去修改 i 的值的。
指针和 const
指针也可以指向常量或者非常量。指向常量的指针 (pointer to const) 不能用于改变其所指对象的值。
示例
const double pi = 3.14;
double* ptr = π // 错误 ptr 是一个普通指针
const double* cptr = π // 正确 cptr 是指向 const 对象指针
*cptr = 42.0; // 错误 cptr 不能修改指向对象的值
和对 const 的引用一样,指向常量的指针也没有规定其所指的对象必须是一个常量。所谓指向常量的指针仅仅要求不能通过该指针改变对象的值,而没有规定那个对象的值不能通过其他途径改变。
const 指针
指针是对象而引用不是,因此就像其他对象类型一样,允许把指针本身定为常量。常量指针 (const pointer) 必须初始化,而且一旦初始化完成,则它的值(也就是存放在指针中的地址)就不能再改变。把 * 放在 const 关键字之前用以说明指针是一个常量,这样的书写形式隐含着一层意味,即不变的是指针本身的值而非指向的那个值:
int errNumb = 0;
int* const curErr = &errNumb; // const 指针指向普通对象
const double pi = 3.14;
const double* const pip = π // 指向 const 对象的 const 指针
要想弄清楚这些声明的含义最行之有效的办法是从右往左阅读。此例中,离 curErr 最近的符号是 const ,意味着 curErr 本身是一个 const 对象,对象的类型由声明符的其余部分确定。声明符中的下一个符号是 * ,意思是 curErr 是一个常量指针。最后,该声明语句的基本数据类型部分确定了 const 指针指向的是一个 int 对象。与之相似,我们也能推断出, pip 是一个 const 指针,它指向的对象是一个 const 双精度浮点型对象。
顶层 const
顶层 const (top-level const) 和底层 (low-level const) 是针对指针来说的。顶层 const 表示指针本身是一个常量,底层 const 表示指针所指的对象是一个常量。
当执行对象的拷贝操作时,常量是顶层 const 还是底层 const 区别明显。
其中,顶层 const 不受影响。但是底层 const 的限制不能忽视。当执行对象的拷贝操作时,拷入和拷出的对象必须具有相同的底层 const 资格,或者两个对象的数据类型必须能够转换。一般来说,非常量可以转换为常量,反之则不行。
什么意思呢?
就是要用指针指向 const 对象,只能用指向 const 的指针。但是指向 const 的指针不一定要指向 const 对象。
可以把普通对象指针赋值给指向 const 的指针,但是反过来不行。
constexpr 常量表达式
常量表达式 (const expression) 是指值不会改变,并且在编译过程中就能得到计算结果的表达式。显然,字面值属于常量表达式,用常量表达式初始化的 const 对象也是常量表达式。
C++ 允许将变量声明为 constexpr 类型,以便由编译器来验证变量的值是否是一个常量表达式。声明为 constexpr 的变量一定是一个常量,而且必须用常量表达式初始化。
constexpr int mf = 20; // 20 是常量表达式
constexpr int limit = mf + 1; // limit 是常量表达式
constexpr int sz = size(); // 只有 size() 是一个常量表达式时,才正确
编译器会给出建议 用 constexpr 代替之前没有类型检查的 #define
如果编程过程中,认为一个变量是常量表达式,那么就可以直接定义为 constexpr 类型
一个 constexpr 指针的初始值必须是 nullptr 或者 0,或者指向固定地址的对象。
如果一个 constexpr 声明中定义了一个指针,限定符 constexpr 仅对指针有效,与指针所指的对象无关。
const int* p = nullptr; // p 是指向 const 对象的指针
constexpr int* q = nullptr; // q 是常量表达式,q 的值是 nullptr
p 与 q 的类型相差甚远,p 是一个指向常量的指针,而 q 是一个常量指针。
constexpr 指针可以指向常量,也可以指向非常量(但是要地址固定)。