⚡C++ 语言基础

const 与 constexpr

面试回答

常见问法

  • constconstexpr 有什么区别?
  • const 一定是编译期常量吗?
  • constexpr 一定会在编译期执行吗?
  • 什么情况下用 const,什么情况下用 constexpr
  • constconstexprconstevalconstinit 怎么区分?
  • 顶层 const 和底层 const 是什么?

回答

constconstexpr 最核心的区别在于它们表达的语义不同:

  • 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;
}

如果面试里要进一步说“怎么选”,可以这样答:

  1. 只需要表达只读语义,用 const

    • 比如函数参数、只读引用、成员函数不修改对象状态
  2. 需要编译期常量能力,用 constexpr

    • 比如数组大小、模板参数、case 标签、编译期查表、元编程
  3. 能用 constexpr 且语义也合理时,优先考虑 constexpr

    • 因为它包含了更强的语义:既常量、又可参与编译期求值
  4. 不要为了“看起来高级”滥用 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 是什么,解决了什么问题?
  • constevalconstexpr 区别是什么?
  • 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 的适用范围继续扩大,标准库中更多类型和操作支持编译期使用。与此同时,还引入了 constevalconstinit 等更细粒度工具。


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. constevalconstinit:是 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

维度constconstexpr
主要目的不可修改可参与常量表达式
初始化时机可运行期必须满足编译期规则
适合作为模板参数不稳定,不建议依赖可以
适合作为 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 管“能不能提前算”。


面试速答版

constconstexpr 的区别,本质上是只读语义编译期语义的区别。const 表示对象初始化后不能修改,但不保证它一定是编译期常量;constexpr 表示这个值或函数可以用于常量表达式,强调编译期可求值。 所以如果只是表达接口只读,比如参数、引用、成员函数,用 const;如果要做模板参数、数组长度、case 标签、编译期计算,就用 constexpr。另外,constexpr 函数不代表每次都在编译期执行,它只是“可以”在编译期执行,是否真的发生要看调用上下文。


面试加分版

constconstexpr 我一般从语义层级来区分。const 的核心是只读约束,它解决的是“这个对象后续不能被修改”的问题,更多用于接口设计,比如 const T& 参数、const 成员函数、只读对象。它不天然等于编译期常量,比如 const int x = get_value();,这里 x 仍然是运行时确定的。

constexpr 更强,它表达的是常量表达式能力,也就是这个对象或函数满足编译期求值规则,可以出现在模板参数、数组长度、case 标签这类需要编译期常量的地方。所以我会把它理解成:const 是“不能改”,constexpr 是“能提前算”。

工程上怎么选,主要看意图:

  • 如果只是想表达只读语义,用 const
  • 如果确实需要编译期常量,用 constexpr
  • 如果两者都满足,而且我希望代码明确表达“这是编译期常量”,我会优先写 constexpr

还有两个面试里容易追问的边界: 第一,constexpr 函数不代表一定在编译期执行,它也可以在运行期执行,取决于调用上下文。 第二,现代 C++ 里还要区分 constevalconstinitconsteval必须编译期求值constinit静态对象必须常量初始化,它们分别解决更强约束和初始化安全问题。

如果面试官继续问,我通常会再展开顶层 const / 底层 constconst 成员函数的逻辑常量性,以及 if constexpr 在模板静态分支里的作用。