const 与 constexpr
面试回答
常见问法
const和constexpr有什么区别?const一定是编译期常量吗?constexpr一定会在编译期执行吗?- 什么情况下用
const,什么情况下用constexpr? const、constexpr、consteval、constinit怎么区分?- 顶层
const和底层const是什么?
回答
const 和 constexpr 最核心的区别在于它们表达的语义不同:
const强调的是只读约束,意思是“这个对象初始化后不能再改”,本质是语义约束 / 接口承诺;constexpr强调的是常量表达式能力,意思是“这个值或函数可以用于编译期求值”,本质是编译期计算能力。
所以可以这样记:
const:不能改constexpr:能在编译期算
它们有交集,但不等价。
int get_size();
// const:只读,但值可能运行时才知道
const int size = get_size();
// constexpr:要求初始化表达式在编译期可求值
constexpr int max_size = 100;
constexpr int square(int x) {
return x * x;
}
如果面试里要进一步说“怎么选”,可以这样答:
-
只需要表达只读语义,用
const- 比如函数参数、只读引用、成员函数不修改对象状态
-
需要编译期常量能力,用
constexpr- 比如数组大小、模板参数、
case标签、编译期查表、元编程
- 比如数组大小、模板参数、
-
能用
constexpr且语义也合理时,优先考虑constexpr- 因为它包含了更强的语义:既常量、又可参与编译期求值
-
不要为了“看起来高级”滥用
constexpr- 如果值天然就是运行时得到的,或者没有编译期场景需求,用
const更自然
- 如果值天然就是运行时得到的,或者没有编译期场景需求,用
一个很容易被问到的点是:
constexpr 并不代表每次调用都一定发生在编译期。
它表示“这个函数/对象有资格参与编译期求值”,真正是否发生在编译期,要看调用上下文。
constexpr int add(int a, int b) {
return a + b;
}
constexpr int x = add(1, 2); // 编译期求值
int n = 10;
int y = add(n, 2); // 合法,但这里通常是运行期求值
追问
const变量什么时候也能当编译期常量用?constexpr函数在 C++11 / C++14 / C++20 有什么演进?if constexpr是什么,解决了什么问题?consteval和constexpr区别是什么?constinit是干什么的,和静态初始化有什么关系?- 顶层
const、底层const在指针和引用里怎么区分? const成员函数真的“完全不修改对象”吗?
原理展开
1. const:只读语义,不等于编译期常量
const 的核心作用是限制修改行为,它可以修饰:
- 普通对象
- 指针
- 引用
- 成员函数
- 返回值/参数
const int a = 10; // a 不能被修改
const int* p1 = &a; // 不能通过 p1 修改 *p1
int* const p2 = nullptr; // p2 自己不能改指向
但要注意:
const 不承诺编译期可求值。
int get_value();
const int x = get_value(); // x 是只读,但不是编译期常量
很多人会误以为“const 就是常量,因此一定在编译期确定”,这是不准确的。
更准确地说:
const只保证初始化后不可修改- 是否能作为常量表达式,还要看类型、初始化方式、上下文等条件
在某些场景下,const 对象也可能恰好是编译期常量,比如:
const int n = 5;
int arr[n]; // 某些编译器支持,标准层面更推荐用 constexpr
但在现代 C++ 工程里,只要你想明确表达“编译期常量”这个意图,就应该优先写成 constexpr,避免语义模糊。
2. constexpr:编译期可求值能力
constexpr 用来声明:
constexpr变量constexpr函数constexpr构造函数constexpr对象
它的核心要求是: 初始化表达式或函数体必须满足常量表达式规则。
constexpr int n = 10;
constexpr int square(int x) {
return x * x;
}
constexpr int m = square(5);
它最典型的价值在于:可以把一部分逻辑前移到编译期完成,从而支持:
- 模板参数
- 数组维度
switch-case标签- 编译期分支
- 编译期查表
- 消除运行期开销的一部分可能性
template<int N>
struct Buffer {
int data[N];
};
constexpr int size = 16;
Buffer<size> buf;
但是要强调两点:
第一,constexpr 是“可以在编译期求值”,不是“必须”
constexpr int mul(int a, int b) {
return a * b;
}
constexpr int a = mul(2, 3); // 编译期
int x = 2;
int b = mul(x, 3); // 运行期也可以
第二,constexpr 变量必须在声明时初始化
constexpr int x = 42; // 正确
// constexpr int y; // 错误,必须立即初始化
3. 为什么现代 C++ 更鼓励用 constexpr
因为它比 const 表达的信息更多、更明确。
同样是“不会变”的值:
const int a = 100;
constexpr int b = 100;
两者都只读,但 b 额外告诉编译器和读代码的人:
- 它是编译期已知的
- 它可用于常量表达式上下文
- 它可参与更强的静态检查和优化机会
所以工程上常见原则是:
- 接口只读性:用
const - 编译期常量:用
constexpr - 如果二者都满足,且你希望表达编译期语义,就优先
constexpr
4. constexpr 函数的演进:别把版本规则说错
这是面试里很容易加分的点。
C++11
constexpr 函数限制很严格,函数体非常受限,通常只能写成非常简单的返回表达式风格。
constexpr int factorial(int n) {
return n <= 1 ? 1 : n * factorial(n - 1);
}
C++14
限制大幅放宽,允许更复杂的局部变量、循环和分支,constexpr 函数开始更实用。
constexpr int sum_to(int n) {
int s = 0;
for (int i = 1; i <= n; ++i) {
s += i;
}
return s;
}
C++17
引入 if constexpr,解决模板代码中“按类型做静态分支”的问题。
#include <type_traits>
#include <string>
template<typename T>
auto process(T t) {
if constexpr (std::is_integral_v<T>) {
return t * 2;
} else {
return t + std::string(" processed");
}
}
C++20 以后
constexpr 的适用范围继续扩大,标准库中更多类型和操作支持编译期使用。与此同时,还引入了 consteval、constinit 等更细粒度工具。
5. if constexpr:静态分支,不是普通 if
if constexpr 的关键点是:
分支选择发生在编译期,未选中的分支不会参与实例化。
这和普通 if 完全不同。
template<typename T>
void print_type_info(T value) {
if constexpr (std::is_pointer_v<T>) {
// 只有 T 是指针时,这个分支才参与编译
} else {
// 否则编译这个分支
}
}
它最常用于:
- 模板泛型代码分支裁剪
- 替代大量
enable_if - 减少模板报错复杂度
- 提升可读性
面试时可以一句话点明:
if constexpr 是模板时代的“编译期开关”。
6. consteval、constinit:是 constexpr 的进一步细分
这是高频追问点。
consteval
表示必须在编译期求值,比 constexpr 更强。
consteval int cube(int x) {
return x * x * x;
}
constexpr int a = cube(3); // 正确
int n = 3;
// int b = cube(n); // 错误,n 不是编译期常量
对比:
constexpr:可以编译期求值consteval:必须编译期求值
constinit
用于静态存储期对象,表示它必须进行常量初始化,但对象本身不一定是 const。
constinit static int global_value = 42; // 保证静态初始化阶段完成初始化
它主要解决的是:
- 静态初始化顺序问题中的一部分风险
- 明确要求“不要退化成动态初始化”
简化记忆:
constexpr:可编译期consteval:必须编译期constinit:必须常量初始化,但未必只读
7. 顶层 const 与底层 const
这是指针/引用相关追问的高频坑点。
顶层 const(top-level const)
修饰对象自身是否可改。
int x = 10;
int* const p = &x; // p 自己不能改指向
这里 const 修饰的是指针变量 p 本身,所以是顶层 const。
底层 const(low-level const)
修饰“通过该对象访问到的内容”是否可改。
const int* p = &x; // 不能通过 p 修改 *p
这里 const 修饰的是 *p 指向的对象,所以是底层 const。
一起出现
const int* const p = &x;
- 指针自身不能改
- 指向内容也不能通过它改
面试里一句话总结很实用:
顶层 const 看“自己能不能变”,底层 const 看“我指到的东西能不能通过我变”。
8. const 成员函数:承诺不改对象的逻辑状态
成员函数后面的 const,本质上是给隐式的 this 指针加限定:
class Demo {
public:
int get() const { return value; }
private:
int value = 0;
};
等价理解为:
// this 的类型近似看成 Demo const* const
也就是说在这个函数里,不能修改普通成员。
但这里有两个重要边界:
1)可以修改 mutable 成员
class Cache {
public:
int get() const {
++access_count; // 合法
return value;
}
private:
int value = 0;
mutable int access_count = 0;
};
2)const 更强调“逻辑常量性”,不是绝对物理不变
例如缓存、统计计数器等不影响对外语义的成员,常常会声明为 mutable。
所以回答时最好不要说“const 成员函数绝对不修改任何成员”,更准确的表达是:
它承诺不修改对象的可观察逻辑状态。
9. 工程实践里怎么选
实际开发中可以按这个顺序判断:
场景一:表达只读接口
优先 const
void print(const std::string& s);
int size() const;
这是最常见、最稳定、最重要的用法。
场景二:确实需要编译期常量
优先 constexpr
constexpr int kMaxRetry = 3;
constexpr std::string_view kName = "worker";
场景三:模板 / 元编程 / 编译期配置
优先 constexpr / if constexpr / consteval
场景四:静态对象初始化安全
考虑 constinit
场景五:运行时值即使不变,也别强上 constexpr
const int thread_num = read_from_config(); // 合理
// constexpr int thread_num = read_from_config(); // 不成立
工程上的关键不是“把所有常量都写成 constexpr”,而是:
让代码准确表达意图。
对比总结
| 概念 | 核心语义 | 是否只读 | 是否要求编译期可求值 | 典型场景 | 优点 | 局限 / 注意点 |
|---|---|---|---|---|---|---|
const | 只读约束 | 是 | 否 | 参数、引用、成员函数、只读对象 | 语义清晰,接口约束强,最常用 | 不代表编译期常量 |
constexpr | 常量表达式能力 | 通常是 | 是(对初始化/定义有要求) | 模板参数、数组大小、编译期计算 | 语义更强,可用于编译期场景 | 不代表每次调用都在编译期执行 |
consteval | 必须编译期求值 | 通常是 | 是,且必须 | 强制编译期生成值 | 约束最强,错误更早暴露 | 灵活性最差 |
constinit | 必须常量初始化 | 不一定 | 只针对初始化阶段 | 静态/全局对象初始化 | 避免退化成动态初始化 | 不是“编译期常量”替代品 |
const vs constexpr
| 维度 | const | constexpr |
|---|---|---|
| 主要目的 | 不可修改 | 可参与常量表达式 |
| 初始化时机 | 可运行期 | 必须满足编译期规则 |
| 适合作为模板参数 | 不稳定,不建议依赖 | 可以 |
适合作为 case 标签 | 通常不如 constexpr 明确 | 可以 |
| 工程语义 | 接口约束 | 编译期能力 |
| 面试关键词 | “只读” | “编译期可求值” |
顶层 const vs 底层 const
| 写法 | 含义 | 属于哪种 const |
|---|---|---|
int* const p | 指针本身不可变 | 顶层 const |
const int* p | 不能通过指针修改所指对象 | 底层 const |
const int* const p | 两者都不可变 | 同时具有 |
易错点
const不等于编译期常量,它只是“初始化后不能改”。constexpr不等于“每次都在编译期执行”,是否真的编译期求值取决于上下文。constexpr变量必须在声明时初始化。constexpr函数能在运行期执行,前提是调用上下文不是常量表达式场景。- 不要把“只读语义”和“编译期语义”混为一谈。
const成员函数不是绝对不改任何成员,mutable成员仍可修改。- 顶层
const和底层const很容易在指针题里答反。 - 需要表达编译期常量时,最好直接用
constexpr,不要让别人猜一个const到底是不是常量表达式。 constinit不是constexpr的替代,它主要管的是初始化阶段,不是“到处都能当常量表达式用”。
记忆技巧
const= 不要改我constexpr= 我能在编译期算consteval= 我必须现在就算constinit= 我必须在静态初始化阶段就初始化好
再记一个选择口诀:
- 只读接口:
const - 编译期常量:
constexpr - 强制编译期:
consteval - 静态初始化安全:
constinit
一个特别适合面试时说的记忆句:
const管“能不能改”,constexpr管“能不能提前算”。
面试速答版
const 和 constexpr 的区别,本质上是只读语义和编译期语义的区别。const 表示对象初始化后不能修改,但不保证它一定是编译期常量;constexpr 表示这个值或函数可以用于常量表达式,强调编译期可求值。
所以如果只是表达接口只读,比如参数、引用、成员函数,用 const;如果要做模板参数、数组长度、case 标签、编译期计算,就用 constexpr。另外,constexpr 函数不代表每次都在编译期执行,它只是“可以”在编译期执行,是否真的发生要看调用上下文。
面试加分版
const 和 constexpr 我一般从语义层级来区分。const 的核心是只读约束,它解决的是“这个对象后续不能被修改”的问题,更多用于接口设计,比如 const T& 参数、const 成员函数、只读对象。它不天然等于编译期常量,比如 const int x = get_value();,这里 x 仍然是运行时确定的。
而 constexpr 更强,它表达的是常量表达式能力,也就是这个对象或函数满足编译期求值规则,可以出现在模板参数、数组长度、case 标签这类需要编译期常量的地方。所以我会把它理解成:const 是“不能改”,constexpr 是“能提前算”。
工程上怎么选,主要看意图:
- 如果只是想表达只读语义,用
const - 如果确实需要编译期常量,用
constexpr - 如果两者都满足,而且我希望代码明确表达“这是编译期常量”,我会优先写
constexpr
还有两个面试里容易追问的边界:
第一,constexpr 函数不代表一定在编译期执行,它也可以在运行期执行,取决于调用上下文。
第二,现代 C++ 里还要区分 consteval 和 constinit:consteval 是必须编译期求值,constinit 是静态对象必须常量初始化,它们分别解决更强约束和初始化安全问题。
如果面试官继续问,我通常会再展开顶层 const / 底层 const、const 成员函数的逻辑常量性,以及 if constexpr 在模板静态分支里的作用。