提到 C/C++ 里的 const,很多人的第一反应是:"哦,只读变量嘛。"然后就没了。
但实际上 const 是 C/C++ 里语义最丰富的关键字之一,用得好能让编译器帮你抓 Bug、让接口自文档化、让多线程代码更安全。用不好,就是一堆编译错误让你怀疑人生。
这篇文章把 const 的 4 种核心用法拆开讲透,每种都配代码,看完你应该能重新审视自己写的每一行带 const 的代码。
用法一:修饰普通变量(只读变量)
这是最广为人知的用法,但很多人只停留在"不能改"这个层面。
const int max_size = 100;
max_size = 200; // 编译错误
表面上看,这就是个"不能修改的变量"。但它真正的价值在于编译器替你守门:
void process(const int threshold) {
threshold = 0; // 编译错误,threshold 是只读的
// ...
}
调用者传进来的值,在函数体内不会被意外修改。这在大型项目里非常重要——你不需要通读函数实现,光看签名就知道"这个函数不会改我的参数"。
const 与 #define 的区别
很多人用 #define 定义常量,但 const 变量有类型检查:
#define MAX 100 // 无类型,预处理期替换
const int max = 100; // 有类型,编译器检查
const 变量还能取地址、参与类型推导,在调试时也能在符号表里看到,而宏在预处理后就已经消失了。
用法二:修饰指针(左定值,右定向)
这是最多人踩坑的地方。核心口诀:const 在 * 左边,值不能改;const 在 * 右边,指针不能改。
四种组合,逐个击破
int a = 10, b = 20;
1. 指向常量的指针(pointer to const)
const int *p = &a;
*p = 30; // 错误:不能通过 p 修改指向的值
p = &b; // 正确:p 本身可以指向别的地址
2. 常量指针(const pointer)
int *const p = &a;
*p = 30; // 正确:可以修改指向的值
p = &b; // 错误:p 本身不能指向别的地址
3. 指向常量的常量指针
const int *const p = &a;
*p = 30; // 错误
p = &b; // 错误
4. 无 const
int *p = &a;
*p = 30; // 正确
p = &b; // 正确
记忆技巧
把 * 当作分界线:
const int *p; // const 在 * 左边 → 指向的内容是常量
int *const p; // const 在 * 右边 → 指针本身是常量
读代码时从右往左读:
| 声明 | 读法 |
|---|---|
const int *p | p is a pointer to const int |
int *const p | p is a const pointer to int |
const int *const p | p is a const pointer to const int |
用法三:修饰函数参数与返回值
这是 const 在接口设计中最有价值的用法,但很多人写函数时根本想不到加。
修饰函数参数
场景:你不打算修改参数,就加上 const
void print_string(const char *str) {
// 函数体内不能修改 str 指向的内容
printf("%s\n", str);
}
调用者看到这个签名,立刻知道:"传进去的字符串不会被改。"这消除了调用者的心理负担。
对比不加 const 的版本:
void print_string(char *str); // 调用者会想:你到底改不改我的字符串?
修饰引用参数(C++ 特有)
void process(const std::string &s) {
// s 不能被修改,同时避免了拷贝
}
这是 C++ 里最高频的 const 用法之一。不加 const 的引用参数,调用者会担心函数内部修改了传入的对象。
修饰返回值
返回 const 值:
const int getValue() {
return 10;
}
这个用法比较少见,主要是防止对返回值做意外修改:
getValue() = 20; // 编译错误
返回 const 指针/引用(重要):
const char* get_name() {
static char name[] = "Alice";
return name;
}
调用者拿到的是 const char*,不能修改内部字符串。这在库开发中非常重要——你不想让调用者修改你内部维护的数据。
char* get_name() {
static char name[] = "Alice";
return name;
}
// 调用者可以:get_name()[0] = 'B'; // 改了内部数据!危险!
用法四:修饰成员函数(C++ 特有)
这是 C++ 中 const 最独特、最强大的用法,也是区分 C 和 C++ 的关键特征之一。
基本用法
class Person {
std::string name;
int age;
public:
std::string get_name() const {
return name; // 不能修改成员变量
}
void set_name(const std::string &n) {
name = n; // 可以修改成员变量
}
};
const 放在成员函数末尾,表示这个函数不会修改对象的状态(mutable 成员除外)。
为什么这很重要?
1. const 对象只能调用 const 成员函数
const Person p;
p.get_name(); // 正确:get_name() 是 const 的
p.set_name("Bob"); // 错误:set_name() 不是 const 的
2. const 重载
class Buffer {
char data[1024];
public:
char& operator[](size_t i) {
return data[i];
}
const char& operator[](size_t i) const {
return data[i];
}
};
Buffer buf;
const Buffer cbuf;
buf[0] = 'A'; // 调用非 const 版本,可以修改
char c = cbuf[0]; // 调用 const 版本,只读
这是标准库容器(如 std::vector)的工作方式。
3. 编译器强制保证
class Counter {
int count;
public:
int get() const {
count++; // 编译错误!const 成员函数不能修改成员变量
return count;
}
};
编译器帮你保证:只要函数声明为 const,函数体内任何修改成员变量的行为都会被拦截。
mutable 关键字
如果你确实需要在 const 成员函数中修改某个成员(比如缓存、互斥锁),用 mutable:
class Cache {
mutable std::mutex mtx; // 锁可以被 const 函数修改
mutable bool dirty;
public:
void invalidate() const {
std::lock_guard<std::mutex> lock(mtx);
dirty = true; // 合法
}
};
进阶:const 与类型转换
const_cast(C++)
const int a = 10;
int *p = const_cast<int*>(&a);
*p = 20; // 未定义行为!a 原本是 const 变量
规则:不要用 const_cast 去掉指向 const 变量的指针的 const 属性去修改它。 这是未定义行为。
const_cast 的正确用途是:去掉指向非 const 变量的 const 指针的 const 属性(比如调用一个不修改参数但没声明 const 的老接口)。
C 风格转换
const int *p = &a;
int *q = (int *)p; // C 风格,能编译,但同样危险
总结
| 用法 | 位置 | 核心语义 |
|---|---|---|
| 修饰变量 | const int x | 变量不可修改 |
| 修饰指针 | const int *p / int *const p | 区分"指向的内容"和"指针本身"不可改 |
| 修饰函数参数/返回值 | void f(const T&) / const T f() | 接口契约,告诉调用者"我不会改你的数据" |
| 修饰成员函数 | int get() const | 函数不修改对象状态,const 对象可调用 |
一句话总结:
const 不只是"只读",它是编译期的契约系统。它让接口自文档化,让编译器成为你的代码审查员,让并发代码更安全。养成"能加 const 就加 const"的习惯,你的代码质量会有一个可见的提升。
