在编写代码时,我们经常需要保证某些数据不被意外修改,C++ 提供了 const 关键字来实现这一目的。
修饰变量
const 名叫常量限定符,用来限定特定变量,以通知编译器该变量是不可修改的。
本文详解 C++ const 关键字的核心用法。涵盖修饰变量与数组、宏常量对比、指针组合(指向常量、常量指针等)、函数参数传递(值、指针、引用)、返回值限制以及类中的应用(const 成员函数、mutable、静态成员)。同时辨析了 const 与 constexpr 在初始化时机、编译期求值及指针修饰上的区别。正确使用 const 能提升代码安全性与可读性。

在编写代码时,我们经常需要保证某些数据不被意外修改,C++ 提供了 const 关键字来实现这一目的。
const 名叫常量限定符,用来限定特定变量,以通知编译器该变量是不可修改的。
const int maxUsers = 100; // maxUsers 从此恒定 100
const int arr[] = { 1, 2, 3 };
const 定义时必须初始化(除非用 extern 声明),而一旦初始化,其值在生命周期内不可修改。
与 C 语言不同,C++ 中的 const 变量(尤其是全局 const)通常不会分配内存,而是直接嵌入到指令中(类似#define)。
宏常量是用 #define 在预处理阶段定义的:
#define MAX_USERS 100
它只是单纯的文本替换,在预处理阶段就把代码里的 MAX_USERS 全部替换成 100。 编译器根本不知道有个叫 MAX_USERS 的东西。 从定义点开始,到文件结束(除非 #undef),它会污染所有后续代码,不管你在不在命名空间里。
想象一下你要写一个计算圆面积的函数,用 const 和宏分别定义圆周率:
// const 版本
const double PI = 3.14159;
double area(double r) { return PI * r * r; }
// 宏版本
#define PI 3.14159
double area(double r) { return PI * r * r; }
看起来差不多,但坑往往藏在细节里。假如你在另一个文件里不小心写了个变量叫 PI:
int PI = 42; // 合法,PI 只是一个普通变量名
用 const 版本:完全没问题,PI 作为常量有自己的作用域,不会和这个变量冲突。 用宏版本:编译错误!因为 #define PI 是全局的,编译器会把 int PI = 42; 里的 PI 也替换成 3.14159,变成 int 3.14159 = 42;,显然不合法。 这就是宏的名称污染问题。 因此定义常量时,首选 const。 宏的唯一用武之地是那些必须发生在预处理阶段的事情,比如条件编译(#ifdef)、头文件保护、以及一些需要字符串化或拼接的技巧。 所以放下对宏的执念吧,拥抱 const,让代码更安全、更可维护!
到了 C++ 里最让人头疼的组合—— const 与指针。 当它们在一起后,就会出现三种组合:
有句话叫:指针可以变,但指向的值不能动。
const int* p; // 或者 int const* p(两种写法等价)
p 是一个指针,它指向一个 const int。 你可以改变 p 本身,让它指向别处;但你不能通过 p 来修改它当前指向的那个整数。 就像你拿着一张禁止涂改的便签,你可以把便签贴到不同的墙上,但无论贴哪,墙上的字都不能改。
int a = 10, b = 20;
const int* p = &a; // p 指向 a
*p = 30; // 错误!不能通过 p 修改 a
p = &b; // 可以,p 现在指向 b
可以记住,const 在 * 的左边,但不能改变指向的值。
指针本身不能动,但指向的值可以改。
int* const p = &a; // p 是一个常量指针,必须初始化
p 是一个常量指针,也就是说指针本身是只读的。 一旦它指向某个变量,就不能再指向别处。 但是,你可以通过它修改它指向的那个变量(只要变量本身不是 const)。
int a = 10, b = 20;
int* const p = &a; // p 永远指向 a
*p = 30; // 可以,a 现在是 30
p = &b; // 错误!不能修改 p 本身
记住 const 在 * 的右边,不能改变指针。
指针和指向的值都不能动。
const int* const p = &a;
合体版!p 本身是常量,指向的也是常量。 所以既不能改 p 的指向,也不能通过 p 修改指向的值。
int a = 10, b = 20;
const int* const p = &a; // p 永远指向 a,且不能通过 p 改 a
*p = 30; // 错误!
p = &b; // 错误!
| 类型 | 声明形式 | 指针本身可改? | 指向的值可改? |
|---|---|---|---|
| 指向常量的指针 | const int* / int const* | 可改 | 不可改 |
| 常量指针 | int* const | 不可改 | 可改 |
| 指向常量的常量指针 | const int* const / int const* const | 不可改 | 不可改 |
const 与函数参数——就像给函数的输入加一道安检门,不同的参数传递方式对应不同的安检级别。
void func(const int x) {
// x 是只读的,不能修改
}
参数 x 是通过值传递的,函数内操作的是实参的副本。 加上 const 意味着这个副本在函数内是只读的。 在函数内部,你不能修改 x。但这对外面的实参没有任何影响——因为本来就是副本。 其实对于值传递,加不加 const 对外部调用者来说没有区别(反正都是副本)。 它的作用主要是对内:告诉函数的读者(以及编译器)这个参数在函数内不应该被修改,起到自我约束的作用,避免不小心改错。 不过,因为值传递本身就会拷贝,如果对象很大,拷贝开销会很高,所以通常只适用于基本类型或小型对象。
指针传递时,const 可以修饰指针本身,也可以修饰指针指向的数据,这就回到了我们之前讨论的指向常量的指针和常量指针。 在函数参数中,它们分别扮演不同角色:
void func(const int* p) {
// 不能通过 p 修改 *p,但可以修改 p 本身(比如让 p 指向别处)
}
假如你想通过指针传入一个大型数组或对象,并且承诺不会修改它。 这样调用者可以放心地把数据交给你,即使数据本身是 const 的也能传进来。 这么做避免了拷贝,同时保证了数据只读。
void func(int* const p) {
// 不能修改 p 本身(比如让 p 指向别处),但可以通过 p 修改 *p
}
你希望这个指针在函数内始终指向同一个对象(比如用于遍历时固定起点)。 但这种情况很少单独使用,因为通常我们更关心数据是否被修改,而不是指针本身是否变。
void func(const int* const p) {
// 既不能改 p,也不能通过 p 改 *p
}
你想表达我不仅不修改数据,也不会改变指向。 不过这种写法有点过度约束,通常用常引用(见下文)更简洁。
void func(const MyClass& obj) {
// obj 是常引用,不能通过它修改对象
}
obj 是传入对象的别名,加上 const 表示这个别名是只读的。 优势:
| 传递方式 | 语法 | 适用场景 | 优势 |
|---|---|---|---|
| 值传递 | void f(const int x) | 基本类型小数据 | 调用者无需担心数据被修改 |
| 指针传递 | void f(const int* p) | 只读大数组/对象,允许空 | 避免拷贝,保护指向内容 |
| 引用传递 | void f(const T& ref) | 大对象只读,首选方式 | 避免拷贝和指针语法,保护对象 |
这是 const 在函数出口设置的关卡,告诉调用者:给你这个返回值,但有些事你不能做! 不同的返回方式配上 const,效果天差地别,我们逐一拆解。
const int getAge() { return 10; }
函数返回的是一个 const 限定的对象(通常是副本)。 不过对基本类型而言,返回 const 值意义不大,因为右值本来就不能被修改。 但对类类型可防止意外赋值:
const std::string getName() const; // 返回 const 对象
比如 getName().append(suffix) 会被编译器阻止,因为 append 是非 const 成员函数。 这在某些场景下能避免无意义的修改(毕竟临时对象马上就销毁了)。
const int* getData() { return nullptr; } // 返回 const int*,指向的数据只读
返回一个指针,指向的数据是 const 的。 调用者不能通过这个指针修改指向的数据,但可以修改指针本身(比如让指针指向别处)。
const std::string& getName() { return xingxing; } // 返回 const 引用
返回一个 const 左值引用,指向某个对象。调用者可以通过这个引用读取对象,但不能修改它。 就像你家墙上开了一扇玻璃窗,你可以透过窗户看到屋里的东西(读取数据),但你不能伸手进去改(不能修改)。窗户本身是固定的(引用不能改指向),而且不占你地方(无拷贝)。
这次我们走进类的内部,看看 const 在类中担任哪些角色。 这里涉及四个角色:const 成员函数、mutable 关键字、const 对象、const 静态成员。 它们各有各的规矩,我们一个个介绍。
在类的成员函数后面加 const,就是向编译器和调用者承诺:这个函数不会修改对象的状态(非静态成员变量)。
class Student {
public:
Student(std::string name = "") : _name(name), _age(10) {}
std::string getName() const {
return _name;
}
void setName(const std::string& n) {
_name = n;
}
int getAge() {
return _age;
}
private:
std::string _name;
int _age;
};
我们在 getName() 成员函数后面加上 const,如果在 getName 里面调用非 const 成员函数:
std::string getName() const {
int a = getAge(); // 报错,类型不兼容
return _name;
}
编译器就会报错,同样的,调用非 const 成员变量也会报错。 因为 const 向编译器和调用者承诺了不会修改对象状态。
假设你有一个 const 对象(比如 const Student s;),它只能调用 const 成员函数。 如果 getName 没加 const,s.getName() 就会编译错误。 当你创建对象时加上 const:
const Student alice("Alice");
alice.getName(); // 可以,因为 getName 是 const
alice.setName("Bob"); // 错误!不能调用非 const 成员函数
这个对象从创建到销毁,如果不是 mutable 成员变量都不可改变。它只能调用 const 成员函数。 此方法常用于表示不可变的数据实体,比如配置文件、常量配置等。
我们了解了 const 成员函数之后可以知道,在类的成员函数后面加 const 后就不能修改其对象。 但生活总有意外——有时候在逻辑上不应该修改对象,可技术上却不得不改一些内部状态。比如: 你有一个互斥锁,需要在 const 成员函数里加锁解锁,这当然会修改锁的状态,但逻辑上并不影响对象的数据。 这时候 const 的严格性就成了障碍。那么我们就可以用 mutable 给 const 开个后门。
class ThreadSafeCounter {
public:
int get() const {
std::lock_guard<std::mutex> lock(m); // m 被修改(加锁)
return value;
}
private:
mutable std::mutex m;
int value = 0;
};
这里的 m 是 mutable 的,因为加锁操作改变了它的状态,但这并不影响 value 的读取。逻辑上 get() 仍是只读操作。
最常见的组合是 const static 成员(顺序无所谓,static const 和 const static 等价)。 这表示一个属于类的常量,所有对象都能访问,且不能修改。
class MathConstants {
public:
static const double PI; // 声明
static const int MAX = 100; // 整型常量可以在类内初始化
};
// 类外定义(如果不在类内初始化)
const double MathConstants::PI = 3.14159;
我们可以看到整型(或枚举类型)的 const 静态成员可以在类内直接初始化,但是非整形的还需在类外定义。 不过在 C++17 后引入了 inline static,就不存在以上问题了。
class MathConstants {
public:
inline static const double PI = 3.14159;
};
通过以上内容我们知道 const 它的核心是承诺不变。 你可以用它修饰变量,告诉编译器:这个值我不会改,你别让我改它。 但这个值到底是在编译时确定还是运行时确定,const 并不关心。
而 constexpr 是 C++11 引入的,它的核心是编译时可知。 它强制要求在编译期就能算出值(或至少在编译期可求值)。
const int a = 42; // 可以,编译期常量
const int b = rand(); // 也可以,运行时初始化,之后不能改
constexpr int c = 42; // 可以,编译期常量
constexpr int d = rand(); // 错误!rand() 不是常量表达式
const 成员函数:如前问所述,承诺不修改对象状态,与编译期求值无关。 constexpr 函数:如果传入的参数是常量表达式,那么该函数可以在编译期求值;如果传入运行时的值,它也可以像普通函数一样在运行时调用。
constexpr int square(int x) { return x * x; }
int arr[square(5)]; // 编译期求值,数组大小合法
int y = 10;
int z = square(y); // 运行时调用,也可以
int x = 10;
const int* p1 = &x; // 指向常量的指针,可改指向,不可改值
int* const p2 = &x; // 常量指针,不可改指向,可改值
constexpr int* p3 = &x; // ?
const 指针:我们之前讨论过,规则灵活。 constexpr 指针:C++11 起,constexpr 指针必须初始化为 nullptr、0,或者静态存储期对象的地址(如全局变量、静态变量),因为它们的地址在编译期是已知的。 局部变量的地址在运行时才确定,不能用于初始化 constexpr 指针。
static int slocal = 42;
constexpr int* p = &slocal; // 可以
int local = 10;
constexpr int* q = &local; // 不可以!local 地址不是编译期常量
constexpr 指针本身是常量指针(即不能改指向),而且指向的地址必须在编译期确定:
constexpr int* p = &global;
*p = 100; // 可以,global 变为 100
如果希望指向的值也不能改,可以结合 const:
const constexpr int* p = &slocal;
*p = 100; // 不可以
在 C++ 中,const 关键字用于声明一个不可修改的实体,它在编译时提供语义约束并由编译器强制执行。 const 可以修饰基本类型变量、指针、引用(常引用)、函数参数(值传递、指针传递、引用传递)以及函数返回值。 在类中,const 成员函数承诺不修改非 mutable 成员变量,使 const 对象能够调用这些函数。 mutable 成员则允许在 const 成员函数中修改不影响对象逻辑状态的内部数据。 static const 成员定义类级别的常量。 而 constexpr(C++11 起)进一步要求编译期求值,可用于变量、函数和构造函数,实现真正的编译时常量,与 const 互补。 正确使用 const 能提升代码的安全性、可读性,并辅助编译器优化。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML 转 Markdown 互为补充。 在线工具,Markdown 转 HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML 转 Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online