深入浅出 C++:const 与 constexpr 的本质区别
在 C++11 引入 constexpr 之后,许多开发者对它和传统的 const 产生了混淆。它们看起来都表示“不可变”,但实际上,它们的设计哲学和应用场景有着本质的区别:const 侧重于“只读”(运行时不可变),而 constexpr 侧重于“常量表达式”(编译期求值)。
本文将为你详细拆解这两个关键字,并梳理它们在现代 C++ 中的最佳实践。
1. const:运行时的“只读”承诺
const 的核心语义是只读 (Read-only)。它向编译器和程序员传达一个契约:“我保证不会通过这个变量名去修改其底层的内存数据”。
关键特征:
初始化时机:可以在编译期初始化,也可以在运行期初始化。
主要作用:防止意外修改,提升代码安全性和可读性。
int get_random_number(); // 一个在运行时产生随机数的函数
int main() {
const int a = 10; // 编译期初始化,只读
const int b = get_random_number(); // 运行期初始化,只读!(完全合法)
// b = 20; // 错误:不能修改 const 变量
}
注意:在 C++ 中,如果一个
const 整数被一个常量表达式初始化(如上面的 a),它可以被用作数组的长度。但这只是一个特例,const 的普遍意义依然是“运行时只读”。
2. constexpr:编译期的“常量”与“求值”
constexpr (Constant Expression) 是 C++11 引入的,它的核心语义是编译期求值 (Compile-time evaluation)。它告诉编译器:“这个变量或函数的结果在编译期间就可以、而且必须被计算出来”。
2.1 constexpr 变量
如果你用 constexpr 修饰一个变量,你就是在强制要求编译器在编译期就计算出它的值。如果算不出来,编译器会直接报错。
推论:所有的constexpr变量默认都是const的,但const变量不一定是constexpr的。
int get_runtime_value() { return 5; }
int main() {
constexpr int a = 10; // 正确:10 是编译期常量
constexpr int b = a + 5; // 正确:a 和 5 都是编译期常量
// constexpr int c = get_runtime_value(); // 错误!该函数无法在编译期求值
}
2.2 constexpr 函数:双重身份
constexpr 修饰函数的意义更加微妙:它表示该函数有能力在编译期求值。
如果你传入编译期已知的参数,它就在编译期执行,返回编译期常量。
如果你传入运行期才知的参数,它就退化为普通的函数,在运行期执行。
// 一个简单的 constexpr 函数
constexpr int square(int x) {
return x * x;
}
int main() {
// 场景 1:编译期求值
constexpr int val1 = square(10); // 100 在编译期算出,可以作为数组大小
int arr[square(5)]; // 合法,数组大小为 25
// 场景 2:运行期求值
int runtime_x = 8; // 非 const 变量
int val2 = square(runtime_x); // 合法,退化为普通函数,在运行时计算 64
}
_( 注:C++14 大幅放宽了对 constexpr 函数内部逻辑的限制,允许使用局部变量、if 语句和循环,不再要求只能有单条 return 语句。)_
3. 核心对比:const vs constexpr
| 特性 | const | constexpr |
| --- | --- | --- |
| 核心语义 | 我是只读的,别改我 | 我在编译期就能算出来 |
| 初始化时机 | 编译期 或 运行期 | 必须是编译期 |
| 修饰变量 | 变量不能被修改 | 变量不能被修改,且必须在编译期初始化 |
| 修饰函数 | 修饰成员函数,表示不修改对象状态 | 表示函数可以在编译期求值 |
| 是否可用于模板参数 | 取决于是否是整型常量 | 总是可以(只要类型符合要求) |
经典翻车场景对比
void test(int n) {
const int c1 = 5;
const int c2 = n; // 正确:运行期初始化只读变量
constexpr int ce1 = 5;
// constexpr int ce2 = n; // 错误!n 的值在编译期未知,无法用于初始化 constexpr
int arr1[c1]; // 合法(c1 是编译期确定的常量)
// int arr2[c2]; // 错误!c2 的值要到运行时才知道(VLA在标准C++中不被允许)
int arr3[ce1]; // 合法(ce1 是 constexpr)
}
4. 延伸:C++20 的两员大将 (consteval 与 constinit)
为了进一步细化编译期计算的控制,C++20 引入了两个新关键字,作为 constexpr 的补充:
consteval (立即函数):比 constexpr 更严格。它要求函数必须、且只能在编译期求值。如果在运行期调用它,直接编译报错。
constinit (强制编译期初始化):强制变量必须在编译期(或静态初始化阶段)完成初始化,但不要求它只读。它主要用来解决 C++ 臭名昭著的“静态初始化顺序惨案 (SIOF)”问题。
5. 最佳实践 (Rule of Thumb)
1. 只要一个变量的值能在编译期确定,就毫不犹豫地使用 constexpr。(这能提升性能,并将错误提前到编译期)。
2. 如果变量的值必须在运行时才能确定,但确定后不应该被修改,使用 const。
3. 对于短小、无副作用的工具函数(如数学计算、类型转换),尽量加上 constexpr。 这样它们既能用于编译期计算,也能用于运行期。
4. 表示类成员方法不修改内部状态时,只能用 const(例如 int getSize() const;)。