const和constexpr常量表达式

很多 C++ 开发者从入门到写了多年代码,都容易对「常量表达式」这个概念模棱两可。我们天天写 const 修饰变量,但很少有人能说清:哪些 const 是真正的编译期常量?哪些只是运行时的只读变量?为什么用 const 定义数组长度有时报错有时通过?为什么 const_cast 修改 const 变量,输出结果会违背直觉?

从 C++11 引入 constexpr 关键字开始,C++ 终于有了明确的「编译期常量」语义。本文结合代码实例,从底层原理到实战坑点,把常量表达式、constconstexpr 一次性讲透。

一、到底什么是常量表达式?

1.1 核心定义

常量表达式需要同时满足两个条件:

  1. 值不会发生改变
  2. 编译阶段就能计算出确定结果

简单来说:编译器在翻译代码时,就能 100% 确定它的最终值,不需要等待程序运行时计算。

1.2 最容易踩的误区:不是所有 const 都是常量表达式

const 只是语法层面的「只读约束」,它不天然等于「编译期常量」。一个 const 变量是不是常量表达式,完全取决于它的初始化来源

我们直接看一组经典示例:

cpp

运行

复制代码
// ✅ 字面量初始化,编译期确定 → 常量表达式
const int a = 1;
// ✅ 常量表达式运算结果 → 仍是常量表达式
const int b = a + 1;

// ❌ 普通变量初始化,运行时才能确定 → 不是常量表达式
int c = 1;
const int d = c;

// ❌ 普通函数返回值,运行时才执行 → 不是常量表达式
int size() { return 10; }
const int e = size();

最直观的验证方式,就是用它们定义数组长度。在 MSVC(Visual Studio)等严格遵循 C++ 标准的编译器中,不支持变长数组(VLA),数组大小必须是编译期常量表达式:

cpp

运行

复制代码
int arr1[b];  // ✅ 编译通过,b 是常量表达式
int arr2[d];  // ❌ 编译报错,d 不是常量表达式

这也是很多人写代码时遇到「玄学报错」的根源。

二、const 的双重身份:编译期常量 vs 运行时只读

为什么同是 const,差异这么大?本质上 const 在 C++ 中承担了两种完全不同的语义:

  1. 编译期常量语义 :用字面量或常量表达式初始化的 const,编译器会触发常量折叠优化 ------ 代码中所有用到该变量的地方,会直接替换为对应的数值,变量甚至可能不占用运行时内存,或存放于只读内存段。
  2. 运行时只读语义 :用普通变量初始化的 const,只是语法层面禁止直接修改,本质还是栈上的普通变量,值要到运行时才确定。

这两种语义的差异,会直接导致一个经典的「诡异 Bug」------ 用 const_cast 修改 const 变量。

三、经典踩坑复盘:const_cast 修改 const,为什么输出没变?

我们先看一段很多人都跑过、结果却和直觉不符的代码:

cpp

运行

cpp 复制代码
#include <iostream>
using namespace std;

int main()
{
    // 情况1:常量表达式 const
    const int i = 0;
    cout << "修改前 i = " << i << endl;
    
    int* ptr = const_cast<int*>(&i);
    *ptr += 1; // 强行通过指针修改内存
    
    cout << "修改后 i = " << i << endl;
    cout << "指针读取值 = " << *ptr << endl;

    // 情况2:运行时只读 const
    int j = 0;
    const int k = j;
    cout << "\n修改前 k = " << k << endl;
    
    ptr = const_cast<int*>(&k);
    *ptr += 1;
    
    cout << "修改后 k = " << k << endl;
    cout << "指针读取值 = " << *ptr << endl;

    return 0;
}

主流编译器开启优化后的运行结果:

plaintext

cpp 复制代码
修改前 i = 0
修改后 i = 0
指针读取值 = 1

修改前 k = 0
修改后 k = 1
指针读取值 = 1

同样是修改 const 变量,为什么 i 的值没变,k 的值变了?

  • 对于变量 i:它是常量表达式,编译器触发了常量折叠。cout << i 在编译阶段就被替换成了 cout << 0,运行时根本不会去读取 i 的内存。哪怕你修改了内存中的值,输出也不会变化。
  • 对于变量 k:它只是运行时只读变量,没有常量折叠优化,每次读取都会访问内存,所以修改内存后值会同步变化。

⚠️ 重要警示 :修改原本就以 const 定义的常量对象,属于 C++ 标准明确规定的未定义行为。不同编译器、不同优化等级下结果完全不可预测,极端情况下还可能因内存只读保护触发程序崩溃,绝对不要在生产代码中这么写。

补充:如果给常量表达式加上 volatile 关键字,会强制编译器每次都访问内存,禁止常量折叠,此时修改后输出会变化。但这依然属于未定义行为,仅可用于原理验证。

四、C++11 constexpr:给编译器的「强制常量承诺」

既然 const 的语义模糊,C++11 就引入了 constexpr 关键字,专门用来标记常量表达式,从语法层面消除歧义。

4.1 constexpr 修饰变量:编译期常量的强约束

constexpr 修饰的变量,有两条铁则:

  1. 一定是常量表达式
  2. 它的初始化值必须是常量表达式,否则直接编译报错

cpp

运行

cpp 复制代码
constexpr int aa = 1;        // ✅ 字面量初始化
constexpr int bb = aa + 1;   // ✅ 常量表达式运算

// constexpr int cc = c;     // ❌ 编译报错!c 是普通变量
// constexpr int cc = size();// ❌ 编译报错!普通函数运行时求值

如果说 const 是「我尽量不修改」,那 constexpr 就是「我向编译器保证,这一定是编译期就能确定的常量」。编译器会帮你做强制检查,从根源上避免语义模糊。

4.2 constexpr 修饰指针:顶层 const 的本质

很多人容易搞混 constexpr 指针的语义,这里给出明确结论: constexpr 修饰指针时,作用的是「顶层 const」------ 也就是指针本身是常量,且指针指向的地址必须在编译期就能确定。

cpp

运行

复制代码
int d = 10; // 局部栈变量
const int* p2 = &d; // ✅ 普通指针,指向 const int

// constexpr const int* p3 = &d; // ❌ 编译报错

为什么会报错?因为局部变量 d 存放在栈上,它的地址要到程序运行、函数调用时才会分配,编译期根本无法确定。

什么样的地址是编译期确定的?全局变量、静态变量,它们的地址在编译链接阶段就固定了:

cpp

运行

复制代码
static int num = 10; // 静态存储期变量
constexpr const int* p = &num; // ✅ 合法

这个指针的完整类型等价于 const int* const p:指针本身不可修改,指向的内容也不可修改。

五、const 与 constexpr 核心对比

表格

特性 const constexpr
是否一定是常量表达式 不一定,取决于初始化来源 强制为常量表达式
初始化要求 运行时、编译时值均可 必须是编译期常量表达式
能否做数组长度 仅常量表达式类 const 可以 全部可以
编译器优化 常量表达式会触发常量折叠 强制编译期求值、常量折叠
指针修饰范围 可自定义顶层 / 底层 const 固定为顶层 const(指针本身不可改)

六、工程最佳实践与避坑指南

  1. 能用 constexpr 就不用 const :如果确定值是编译期固定的常量(比如数组大小、固定配置参数、模板参数),优先用 constexpr,语义更清晰,编译器也能做更多优化。
  2. const 只用于运行时只读 :当变量的值要运行时才能确定,但初始化后不会修改时,使用 const
  3. 绝对不要用 const_cast 修改原生 const 变量const_cast 仅适用于「原本非 const,仅被临时加上 const 修饰」的场景,比如对接缺少 const 修饰的旧版 API。
  4. 避免用 #define 定义常量 :宏常量没有类型检查、不受作用域约束,现代 C++ 应优先使用 constconstexpr
  5. 用 static_assert 做编译期校验 :配合 constexpr 可以在编译阶段验证常量的合法性,提前发现问题。

七、总结

最后我们用一句话梳理清楚三者的关系:

  • 常量表达式是属性:描述一个值能否在编译期确定
  • const语法约束 :表示「运行时不可修改」,一部分 const 变量具备常量表达式属性
  • constexpr强制标记:强制变量必须是常量表达式,由编译器做编译期检查

理解了常量表达式的本质,你就能避开 C++ 里绝大多数和 const、编译期常量相关的坑,写出更安全、性能更好的代码。

相关推荐
码云数智-大飞1 小时前
RAII 与智能指针深度拆解
java·前端·算法
云烟成雨TD1 小时前
Agent Scope Java 2.x 系列【19】Harness:从零搭建 MySQL 文件系统
java·人工智能·agent
qq3621967051 小时前
阿里裁员新消息(2026最新动态汇总)
java·开发语言·前端
a1117762 小时前
“黑夜流星“个人引导页 网页html
java·前端·html
砚底藏山河2 小时前
沪深A股:如何获取基金持股数据
java·python·数据分析·maven
代码改善世界2 小时前
【C++进阶】C++11:列表初始化、右值引用与移动语义、完美转发全解析
java·开发语言·c++
饼饼饼2 小时前
React19 状态解惑:State 没那么神秘,一文读懂 React 状态不可变原则与 Hooks 底层链表
前端·react.js
AIGS0012 小时前
JBoltAI V4.5企业智能体平台:技术架构拆解
java·人工智能·ai大模型应用
一勺菠萝丶2 小时前
Maven SNAPSHOT 父 POM 无法解析问题排查
java·maven