在C++代码里,const和constexpr都能用来修饰变量,很多人以为它们只是新旧写法的区别,实际上两者语义差别很大。const的含义是“运行期不可修改”,它修饰的变量完全可以在运行期才完成初始化;而constexpr的含义是“编译期常量”,它要求变量的值在编译阶段就必须确定下来。这个差别会直接影响到数组长度、模板实参、case标签等场景能否编译通过。

一、const的本质:只读承诺,而非编译期常量
const关键字最初的定位是“只读”,它向编译器承诺这个变量初始化之后不再被修改,但它并不要求初始化发生在编译期。看下面这段代码:
int get_value() {
int x;
std::cin >> x;
return x;
}
const int a = 10; // 编译期初始化,可以用作数组长度
const int b = get_value(); // 运行期初始化,只是一个只读变量这里变量b虽然是const,但它的值要等用户输入之后才能确定,编译器在编译阶段根本不知道它是多少,所以它只是一个“运行期只读变量”。这样的const变量不能用作数组维度,不能传给模板的非类型参数,也不能用在case标签里。
而变量a在编译期就完成了初始化,某些编译器会把它放进符号表,甚至参与常量折叠优化。但要注意,这只是编译器的优化行为,从语言标准的角度,const本身并没有强制编译期求值的要求。换句话说,const修饰的变量“可能是”编译期常量,也可能不是,取决于初始化表达式能否在编译期求值。
二、constexpr的强约束:编译期必须算出结果
constexpr是C++11引入的关键字,它的语义比const严格得多:被它修饰的变量必须能由常量表达式初始化,否则直接编译报错。这个约束反而成了优点,它把“我想在编译期计算”的意图明确告诉编译器,编译器会帮你验证这一点。
constexpr int square(int n) {
return n * n;
}
constexpr int len = square(5); // 正确:编译期算出25
int arr[square(5)]; // 正确:可作数组维度
const int c = rand(); // 编译通过:运行期初始化
constexpr int d = rand(); // 编译错误:rand不是常量表达式上面最后两行是区分两者最典型的例子:同一个初始化表达式,const能编译通过,constexpr直接报错。这不是constexpr的缺陷,而是它的设计目的,让你在写代码时就发现“这个值编译期算不出来”,而不是等到使用处才发现问题。
constexpr函数也有一些要求。在C++11中,constexpr函数体内只能有一条return语句;C++14放宽了限制,允许局部变量、循环、条件分支;C++17进一步允许constexpr Lambda和if constexpr。如果用constexpr修饰指针,需要注意它修饰的是指针本身而非所指对象,这点与const恰好相反。
三、典型使用场景对比
最直观的对比是数组定义。C++要求数组长度必须是编译期常量,两种写法的表现完全不同:
int n; std::cin >> n; const int size1 = n; // 编译通过 // char buf[size1]; // 编译错误:size1不是常量表达式 constexpr int size2 = 100; char buf[size2]; // 正确
在模板编程中,非类型模板参数同样要求编译期常量。假如有一个template<int N>这样的模板,传入const变量会导致编译失败,而constexpr变量可以正常实例化。switch语句的case标签也是同理,case后面必须跟常量表达式,只有编译期常量才能胜任。
另一个容易混淆的点是constexpr成员函数。在类中声明为constexpr的成员函数表示它可以在常量表达式中被调用,配合constexpr构造函数就能在编译期构造对象,比如常用的字面值类型。此外constexpr还支持递归调用,配合模板可以在编译期完成复杂计算,这是元编程的重要手段之一。
四、如何选择:按需求而非习惯
选择原则其实很简单:如果希望这个值一定在编译期确定,就用constexpr,比如数组长度、位掩码、数学常量、模板参数;如果只是想表达“这个值初始化后不允许修改”,比如函数内对外部数据的只读引用、运行期才能获得的配置值,用const更合适,因为强行加constexpr会导致编译失败。
从C++17开始,constexpr变量被隐式声明为const且内部链接,而C++11里constexpr成员函数并不隐含const,这个细节在C++14中已修正。实践中还有一条经验法则:能用constexpr就用constexpr,让编译器帮你把关;确实做不到编译期求值的场景,再退回const。理解了“只读承诺”与“编译期求值”这两个核心语义,两者之间的界限就再也不会混淆了。