C++ 常量:const 与 constexpr
一、常量概念与 const 的本质
在 C++ 中,”常量”是指程序运行期间值不发生改变的量。从内存占用角度,常量分为两类:
- 字面值常量(Literal Constants):直接写在源码中、没有标识符的数据。通常处于只读数据段(.rodata),或者直接嵌入机器指令(.text立即数)。
| 数据类型 | 字面值示例 |
|---|---|
| 整型 | 42, 0xFF, 0b101 |
| 浮点型 | 3.14, 2.5e10 |
| 字符与字符串 | 'A', '\n', "Hello World" |
| 布尔型 | true, false |
| 指针 | nullptr |
- 命名常量(Named Constants):拥有独立内存地址和标识符的对象,最典型的是由
const修饰的变量。
| 特性 | 字面值常量 (Literal) | const 变量 (Constant Variable) |
|---|---|---|
| 是否有名字 | 否 | 是 |
| 是否占内存 | 通常不占用独立内存(嵌入指令) | 占用内存(有地址) |
| 是否有类型 | 有(编译器推导) | 有(显式定义) |
| 例子 | 100, "Apple" |
const int price = 100; |
1.1 const 的核心语义:只读
const 的语义是只读(Read-only):变量初始化后,禁止通过该标识符对内存进行写操作。但它不规定”值的确定时机”。
因此,按值的确定时机,const 变量分为两类:编译时常量和运行时常量。
1.2 编译时常量(Compile-time Constant)
若 const 变量的初始化依赖于常量表达式(见第二节),编译器能够在编译阶段确定其值。
const int x { 3 }; // 编译时常量,基于字面值
const int y { 4 }; // 编译时常量
const int z { x + y }; // 编译时常量,依赖项均在编译期已知
对于编译时常量,编译器完成词法与语法分析后,通常会触发常量折叠(Constant Folding),在所有引用该变量的 AST 节点处直接替换为具体数值,消除运行时的内存读写指令。具体可参考常量表达式、编译时常量和运行时常量。
1.3 运行时常量(Runtime Constant)
若 const 变量的初始值依赖运行时数据(如函数返回值、系统调用或 I/O 输入),该变量即为运行时常量。
#include <iostream>
int getNumber() {
int y{};
std::cin >> y;
return y;
}
int main() {
const int x { 3 }; // 编译时常量
const int y { getNumber() }; // 运行时常量:初始值阻塞至运行阶段
const int z { x + y }; // 运行时常量:传递性依赖运行时变量 y
return 0;
}
在此场景中,y 和 z 具备 const 属性,保证初始化后不被修改,但其值必须等到程序执行至对应语句时才能确定并写入内存。
1.4 const 体系的核心差异对比
| 分类 | 编译时常量 (Compile-time) | 运行时常量 (Runtime) |
|---|---|---|
| 初始值来源 | 纯字面值或常量表达式 | 非常量表达式(函数返回、I/O 等) |
| 求值落定时机 | 编译(Compile)阶段 | 运行(Runtime)阶段 |
| 用于数组维度声明 | 合法 | 非法(VLA 并非标准 C++ 特性) |
| 用作非类型模板参数 | 合法 | 非法 |
| 核心工程价值 | 触发编译期优化,消除运行时开销 | 接口安全性,防止内存状态被意外破坏 |
二、常量表达式与编译期优化
为理解 const 的局限性并引出 constexpr,需要先明确编译器优化的核心依据:常量表达式(Constant Expression)。
2.1 as-if 规则与优化自由度
C++ 标准定义了 as-if 规则:只要程序最终的可观察行为(如 I/O 操作、volatile 内存访问)不变,编译器可以任意重构指令序列。常量表达式是编译器执行死代码消除(Dead Code Elimination)与计算前置的前提条件。
2.2 常量表达式的判定
常量表达式要求其中所有子表达式的值在编译期可知,且所调用的运算符或函数支持编译期求值。
int main() {
int x { 3 + 4 };
std::cout << x << '\n';
return 0;
}
上述代码中,3 + 4 是常量表达式。编译器将其静态求值为 7,生成的汇编代码等价于直接输出 7。无论该指令被循环调用多少次,计算只在编译阶段发生一次,运行时无额外开销。
三、constexpr:强制编译期求值
3.1 const 的模糊地带
如第一节所述,const 无法在语法层面标明变量的确切求值时机。在需要严格编译期常量的场景(如数组大小、模板参数)中,编译器无法仅凭 const 判断变量是否满足要求。
int x = 5;
const int a = 10; // 编译时常量
const int b = x; // 运行时常量
int arr[a]; // 语法合法
int arr[b]; // 语法非法:b 的值在 AST 构建阶段无法确定
3.2 constexpr 的强语义约束
C++11 引入 constexpr,明确区分 const 的两种语义。constexpr 要求该标识符的值必须在编译期确定,否则直接报编译错误。
constexpr int c = 20; // 合法:显式编译时常量
// constexpr int d = x; // 编译期 Error:x 为非常量表达式,违背求值约束
类型推导说明:constexpr 变量自动具备 const 属性。因此,constexpr 是 const 的严格子集:在”运行时只读”的基础上,进一步要求”编译期已知”。
3.3 constexpr 的推导链条
constexpr 的推导起点必须是字面值常量,不能依赖任何运行时数据,即便该数据被标记为 const:
int x = 5;
const int y = x; // 合法:运行时常量
// constexpr int z = x; // 非法:x 的值在编译期不存在
C++11 同步引入了 constexpr 函数,支持在编译阶段执行函数、循环和条件判断,将原本在运行时才能完成的计算提前到编译期,例如:
constexpr int square(int x) {
return x * x;
}
int main() {
// 依赖链条完整:字面值 5 -> constexpr 函数计算 -> constexpr 变量接收
constexpr int result = square(5);
int arr[result]; // 绝对合法:result 在编译期已折叠为 25
return 0;
}
当输入参数满足编译期常量的条件时,constexpr 函数即可参与常量折叠。
四、const 与 constexpr 的设计哲学
C++ 常量体系的演化,反映了语言标准在内存安全和编译期计算能力上的持续改进。
| 历史阶段 | 核心工具 | 机制底层逻辑 | 架构局限性 |
|---|---|---|---|
| C 语言时代 | #define |
预处理器文本替换(AST 构建前) | 不在类型系统内,无作用域隔离,容易引发符号污染和调试困难。 |
| 早期 C++ | const |
编译器介入的类型安全与内存访问保护 | 语义不够精确,无法在语法层面区分”运行时只读”和”编译期已知”。 |
| 现代 C++ (C++11 起) | constexpr |
编译阶段强制执行计算 | 约束较严,早期版本对函数体内的语句限制较多(C++14 后逐步放宽)。 |
4.1 const:只读语义
在现代 C++ 架构中,const 的核心任务就两个:接口声明规范和内存状态保护。
例如,定义接口 void process(const std::string& s) 时,就是明确告诉编译器:在这个函数作用域内,禁止对 s 进行任何修改。
至于性能优化,只是 const 的“副作用”,编译器并不保证完成。它的核心价值在于工程安全:既防止代码意外篡改数据,也避免多线程环境下因数据冲突引发的 Bug。
4.2 constexpr:计算左移
constexpr 的核心在于将计算任务从运行时转移至编译时(计算左移,Shift-Left)。
通过 constexpr,开发者可以将哈希表生成、数学函数计算等复杂逻辑,在程序构建阶段就完成计算。这使得最终生成的可执行文件直接包含计算结果,程序运行时直接使用,无需再次计算。这种零运行时开销的机制,对于嵌入式系统、高频交易及底层渲染引擎等对时延要求极高的领域至关重要。

浙公网安备 33010602011771号