C 的 const 与 C++ 的 const/constexpr

constconstexpr 都是很常见的关键字,但实际使用时经常会把“只读语义”和“编译期常量”混在一起。把这几个概念放在同一篇里梳理清楚。

C 里的 const

C 里的 const 本质上是一个类型限定符,表示不能通过这个表达式去修改对象。

先看最基本的变量场景:

1
const int n = 10;

这里的重点不是“对象绝对不可变”,而是编译器不允许你再通过 n 这个表达式给它赋值。

1
2
const int n = 10;
// n = 20; // 编译报错

指针里的 const

const 一旦和指针放在一起,就容易混。最稳妥的判断方式是直接看它修饰谁。

1
2
3
const int *p1 = 0;
int *const p2 = 0;
const int *const p3 = 0;

它们分别表示:

  • const int *p1p1 指向的内容不能通过 p1 修改,但 p1 可以改指向
  • int *const p2p2 本身不能改指向,但可以通过 p2 修改它指向的内容
  • const int *const p3:指向不能改,内容也不能通过它改

实践里可以先按下面这个规则判断:

  • const* 左边,通常限制“指向的内容”
  • const 靠近变量名时,通常限制“指针本身”

举个例子:

1
2
3
4
5
6
7
8
9
10
int a = 1;
int b = 2;

const int *p1 = &a;
p1 = &b; // 可以
// *p1 = 3; // 不可以

int *const p2 = &a;
*p2 = 3; // 可以
// p2 = &b; // 不可以

C 里 const 的一个常见误区

一个很常见的误解,是把 const 和“编译期常量”直接画等号。

在 C 里,const 更偏向“只读语义”,不等于它一定能拿去做所有要求编译期常量的场景。也就是说,const 的重点是“限制修改”,不一定是“在预处理或编译早期就能完全替换成字面量”。

所以在 C 语言里,先把 const 理解成“只读限制”,通常是最准确的切入点。

C++ 里的 const

C++ 继承了 C 里 const 的基本含义,但因为 C++ 引入了引用、类和成员函数,const 的使用范围也明显更广。

const 变量

最基本的写法和 C 类似:

1
const int n = 10;

语义和 C 中一致:不能通过这个名字修改对象。

const 引用

在 C++ 里,const 引用最常见的落点就是函数参数:

1
2
3
void print(const std::string& s) {
// 不能修改 s
}

这种写法通常有两个直接收益:

  • 避免不必要的对象拷贝
  • 同时明确函数不会修改参数

因此在“只读的大对象传参”这个场景里,const T& 基本是默认选项。

const 成员函数

类的成员函数后面也可以加 const

1
2
3
4
5
6
7
struct A {
int x;

int get() const {
return x;
}
};

这里的 const 表示这个成员函数不会修改当前对象的普通成员。

也正因为如此,const 对象只能调用 const 成员函数:

1
2
3
const A a{1};
// a.set(2); // 如果 set 不是 const,就不能调
a.get(); // 可以

这也是类接口设计里经常要区分“观察操作”和“修改操作”的原因。

constexpr 是什么

constexpr 是 C++ 提供的关键字,C 语言里没有对应概念。

可以先把它概括成一句话:constexpr 用来描述“这个值或这个函数结果可以在编译期确定”。

最常见的是 constexpr 变量:

1
constexpr int buf_size = 1024;

它比普通 const 更强,因为它强调的是“编译期常量表达式”。

constexpr 也可以修饰函数:

1
2
3
constexpr int square(int x) {
return x * x;
}

如果传入的是编译期可知的值,那么结果也可以在编译期算出来:

1
constexpr int n = square(8);

需要注意的是,constexpr 函数不等于“只能在编译期调用”。它也可以参与运行期计算,只是在条件满足时,编译器可以提前完成求值。

constconstexpr 的区别

这两个概念很容易混在一起,但侧重点其实很明确:

  • const 强调“只读,不允许修改”
  • constexpr 强调“编译期可求值”

可以直接记成:

  • const 解决的是“能不能改”
  • constexpr 解决的是“能不能在编译期确定”

例如:

1
2
const int a = 10;
constexpr int b = 10;

这两个看起来都像常量,但语义并不相同。b 明确是编译期常量;a 只是只读对象,在很多场景下也能作为常量使用,但它的核心语义仍然是“不可修改”。

所以如果要表达“这是一个用于编译期求值的常量”,优先考虑 constexpr;如果只是表达“这个对象创建后不应再被修改”,const 就足够了。

constexprconst#define 的区别

这三个经常都被拿来“定义常量”,但它们其实不在同一个层面上。

先看一个对比表:

方式 所处阶段 有无类型 有无作用域 主要语义 更适合的场景 主要问题
#define BUF_SIZE 1024 预处理阶段 文本替换 条件编译、头文件保护、宏开关 没有类型,调试体验差,容易产生意外替换
const int buf_size = 1024; 语言语义层面 只读对象 表达“这个值不应该再改” 重点是只读,不是显式强调编译期常量
constexpr int buf_size = 1024; 语言语义层面 编译期常量表达式 数组大小、模板参数、constexpr 函数等需要编译期信息的场景 只在 C++ 里有,使用条件比 const 更严格

如果只抓最关键的一点:

  • #define 是预处理器做文本替换
  • const 表达的是只读语义
  • constexpr 表达的是编译期常量语义

该怎么选

如果只是做预处理替换,或者做条件编译,用 #define 很正常:

1
#define LOG_DEBUG 1

如果是在 C++ 里定义普通常量,通常优先考虑 constconstexpr,尽量不要再回退到 #define

  • 只想表示“这个值不应该再改了”:用 const
  • 想明确表示“这是编译期常量”:用 constexpr
  • 需要做预处理替换或条件编译:用 #define

小结

最后收束成几句话:

  • C 里的 const 重点是“只读限制”
  • C++ 里的 const 在引用、成员函数这些地方更常见也更重要
  • constexpr 是 C++ 提供的“编译期常量”工具,语义比 const 更强
  • 在 C++ 里定义普通常量,优先考虑 constconstexpr

如果后面还要继续展开,这个话题还可以再单独补一篇,把 constconstexprconstevalconstinit 放在一起看。