C++ 常量:const 与 constexpr

一、常量概念与 const 的本质

在 C++ 中,”常量”是指程序运行期间值不发生改变的量。从内存占用角度,常量分为两类:

  1. 字面值常量(Literal Constants):直接写在源码中、没有标识符的数据。通常处于只读数据段(.rodata),或者直接嵌入机器指令(.text立即数)。
数据类型 字面值示例
整型 42, 0xFF, 0b101
浮点型 3.14, 2.5e10
字符与字符串 'A', '\n', "Hello World"
布尔型 true, false
指针 nullptr
  1. 命名常量(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;
}

在此场景中,yz 具备 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 属性。因此,constexprconst 的严格子集:在”运行时只读”的基础上,进一步要求”编译期已知”。

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,开发者可以将哈希表生成、数学函数计算等复杂逻辑,在程序构建阶段就完成计算。这使得最终生成的可执行文件直接包含计算结果,程序运行时直接使用,无需再次计算。这种零运行时开销的机制,对于嵌入式系统、高频交易及底层渲染引擎等对时延要求极高的领域至关重要。

posted @ 2026-04-21 11:16  noonafter  阅读(35)  评论(0)    收藏  举报