const 大概是 C++ 初学者最早接触的关键字之一,但它绝不仅仅表示"一个不可修改的常量"。随着学习的深入,你会发现它在类型安全、编译器优化、接口设计乃至多线程编程中都扮演着重要角色。
1. 基础篇:定义一个常量
最简单的用法,让变量值不可修改。
cpp
const int MAX_SIZE = 100;
// MAX_SIZE = 200; // 错误!不能修改
int const MAX_SIZE_2 = 100; // 与上一行完全等价,const 在类型前后均可
要点:
const修饰的变量必须初始化,否则之后也没机会赋值了。const int和int const是一样的,但为了风格统一,通常把const写在最前面。
const 与宏常量的区别
在 C++ 中,更推荐使用 const 或 constexpr 代替 #define 定义常量,因为它有类型检查,且遵循作用域规则。
2. 指针与 const:厘清谁是不可变的
这是最绕也是面试最爱考的地方。记住核心:看 const 在 * 的哪一边。
2.1 指向常量的指针
cpp
int value = 10;
const int* ptr = &value; // 等同于 int const* ptr
- 含义 :不能通过
ptr修改其所指对象的值 (*ptr = 20是错的)。 - 但 :
ptr自身可以指向别的对象,而且value本身如果不是 const,仍可通过value直接修改。
2.2 常量指针
cpp
int value = 10;
int* const ptr = &value;
- 含义 :
ptr本身的值(即指向的地址)不可变,一旦初始化就不能再指向别处。 - 但 :可以通过
*ptr修改所指对象的值。
2.3 指向常量的常量指针
cpp
const int value = 10;
const int* const ptr = &value;
- 含义:指针本身不可变,且不能通过它修改所指对象。一切都被锁定。
记忆技巧 :以 * 为界,const 在左修饰指向的对象,在右修饰指针本身。所以 const int * 修饰的是 int,int * const 修饰的是指针。
3. 顶层 const 与底层 const
这个概念在泛型编程和类型推导中尤为重要。
-
顶层 const :表示指针本身 或对象本身 是常量。
cppint i = 0; int *const p1 = &i; // 顶层 const,p1 不可变,一旦初始化就不能再指向别处。 const int ci = 42; // 顶层 const,ci 不可变 -
底层 const :表示所指对象 或所引用对象 是常量。
cppconst int *p2 = &ci; // 底层 const,*p2 不可变 const int &r = ci; // 底层 const,r 引用的对象不可变
关键区别:
-
拷贝顶层 const 不受影响,因为拷贝时会复制值,原对象是否 const 无所谓。
cppint a = ci; // 正确,ci 的顶层 const 被忽略 -
底层 const 在拷贝时必须保持"常量"性质,不能将底层 const 的指针或引用赋给非 const 的指针或引用。
cppconst int *cp = &ci; int *p = cp; // 错误!cp 是底层 const,不能给普通 int*
4. const 与函数
const 在函数中的应用,是写出健壮接口的关键。
4.1 const 形参
像普通变量一样,函数内部不会修改形参时,可以用 const 修饰。但注意,在函数声明中,顶层 const 会被忽略:
cpp
void func(int val); // 1
void func(const int val); // 2 与 1 重复声明!顶层 const 被忽略
而对于指针和引用,底层 const 则有实质区别:
cpp
void func(int& val); // 只能接受普通左值
void func(const int& val); // 可以接受 const 或非 const 的左值/右值
使用 const & 作为参数是避免拷贝、同时保护原数据的最佳实践。
4.2 const 返回类型
返回一个 const 值类型通常没有意义(如 const int func()),因为调用者拷贝后本来就可以随便改。但它常用于返回指针或引用,防止调用者通过返回值修改内部数据。
cpp
class MyClass {
int data;
public:
const int& getData() const { return data; } // 返回底层 const 引用
};
5. const 成员函数:接口设计的精髓
这是面向对象中的重头戏。
语法 :在成员函数参数列表后加 const 关键字。
cpp
class Widget {
private:
int value;
public:
int getValue() const { // 常量成员函数
// value = 10; // 错误!不能修改任何非 mutable 成员变量
return value;
}
};
作用:
-
承诺不修改对象状态 :在函数体内,
this指针的类型是const Widget* const,因此不能修改非mutable成员。 -
可被 const 对象调用 :
const Widget w; w.getValue();是合法的。普通对象也可以调。 -
构成重载 :可以根据成员函数是否为
const来重载。cppclass Widget { public: void display() { std::cout << "non-const\n"; } void display() const { std::cout << "const\n"; } }; Widget w; w.display(); // 调用普通版本 const Widget cw; cw.display(); // 调用 const 版本
mutable 的妙用
如果一个 const 成员函数确实需要修改某个与对象逻辑状态无关的变量(如缓存、互斥锁),可将该变量声明为 mutable。
cpp
class Cache {
mutable int cachedValue;
mutable std::mutex mtx;
public:
int getValue() const {
std::lock_guard<std::mutex> lock(mtx); // 可以在 const 函数内加锁
if (cachedValue < 0) cachedValue = compute();
return cachedValue;
}
};
6. const 与迭代器
STL 迭代器与指针类似,也有 const 之分。
cpp
std::vector<int> vec = {1, 2, 3};
const std::vector<int>::iterator iter = vec.begin(); // 迭代器本身是 const
*iter = 10; // 可以修改所指元素
// ++iter; // 错误,迭代器本身不可变
std::vector<int>::const_iterator cIter = vec.begin(); // 所指元素是 const
// *cIter = 10; // 错误,不能修改元素
++cIter; // 可以,迭代器自身可变
需要元素不可变的遍历时,优先使用 cbegin() 和 cend()。
总结
const 是 C++ 类型系统中的一位"守夜人":
- 对变量:声明常量,强制初始化。
- 对指针:通过相对位置区分不可变的是指针自身还是所指对象。
- 对函数 :
const &传参防止拷贝和修改;const返回值保护内部数据。 - 对类 :
const成员函数是可被常量对象调用的安全接口,mutable 处理物理常量性与逻辑常量性的分离。
掌握 const 不仅仅是记住语法,更重要的是形成"用 const 告诉编译器你的不变性约束"的思维习惯,这样才能写出更安全、更清晰的 C++ 代码。
希望这篇梳理对你有帮助,const 的世界很清晰,多写多练就不容易晕了。
与 const 知识点相关的常考面试题
一、基础概念题
1. const 变量必须在定义时初始化吗?为什么?
答案要点:必须初始化。因为 const 对象一旦创建,其值就不能再改变,若不初始化,它将永远拥有一个不确定的值。
2. const int 和 int const 有区别吗?
答案要点:没有区别,完全等价。当 const 修饰的是基本类型且没有指针参与时,位置不影响含义。
3. const 与 #define 定义常量有什么区别?
答案要点:
const有类型检查,#define只是文本替换。const遵循作用域规则,#define从定义处到文件结束(或#undef)。const可以被调试器看到,#define宏名在编译预处理后就被替换了。- 在 C++ 中更推荐用
const或constexpr。
二、指针与 const(必考,高频)
4. 解释 const int *p 和 int *const p 的区别。
答案要点:
const int *p:指向常量的指针,不能通过*p修改所指对象,但 p 可以指向别处。int *const p:常量指针,p 本身不可变(不能指向别处),但可以通过*p修改所指对象。
5. 以下代码错在哪?
cpp
int a = 10;
const int *p = &a;
int *q = p; // 这行有什么问题?
答案要点 :错误,p 是底层 const,q 是普通指针。如果把 p 赋给 q,就可以通过 *q 修改一个 const int 指向的对象,破坏了常量性。需要改为 const int *q = p; 或强制类型转换(不推荐)。
6. 判断对错并解释:
cpp
const int a = 10;
int *p = (int*)&a;
*p = 20;
答案要点 :代码可能编译通过,但行为是未定义的 。a 本身是 const 对象,可能被放在只读内存区,强制修改会导致运行时崩溃或奇怪的优化行为。不要这样做。
三、顶层 const 与底层 const
7. 什么是顶层 const?什么是底层 const?各自在赋值时有什么影响?
答案要点:
- 顶层 const :对象本身是常量(如
const int a中的 a,或int *const p中的 p)。 - 底层 const :所指/所引用的对象是常量(如
const int *p中的 *p,或const int &r中的引用对象)。 - 赋值时,顶层 const 可以被忽略(拷贝出新值),底层 const 必须保持一致性,不能把底层 const 赋给非 const。
8. 给定代码,判断能否编译:
cpp
int i = 0;
const int ci = i;
const int *p1 = &ci;
int *const p2 = &i;
p1 = p2; // (1)
p2 = p1; // (2)
const int *const p3 = p2; // (3)
答案要点:
- (1) 可以,
p2是顶层 const,赋值给p1(底层 const)没问题,p1的底层 const 保持不变。 - (2) 不可以,
p2是顶层 const 指针,本身不能被赋值。 - (3) 可以,
p2的底层是 non-const,赋给const int是安全的。
四、const 与函数
9. 这两个函数声明是否构成重载?为什么?
cpp
void f(int);
void f(const int);
答案要点:不构成重载,这是重复声明。顶层 const 在函数参数中被忽略,编译时会报重复定义。因为它们接受的实参类型没有区别。
10. 以下函数各自能接受什么类型的实参?
cpp
void f1(int&);
void f2(const int&);
答案要点:
f1只能接受非 const 的左值,不能接受 const 对象或右值(字面量、临时对象等)。f2可以接受任何 int 类型的实参 :非 const 左值、const 左值、右值。const &是一个通用的只读引用。
11. 返回 const 引用有什么意义?和返回值类型有什么区别?
答案要点:
- 返回 const 引用可以避免拷贝,同时保护内部数据不被调用者修改。
- 如果返回值类型(非引用),调用者得到的是副本,返回值的 const 修饰没有实际意义,调用者可以随意修改副本。
五、const 成员函数(高频)
12. 什么是 const 成员函数?它有什么作用?
答案要点:
- 在成员函数参数列表后加
const修饰,承诺该函数不会修改对象的状态。 - 作用:
- 可以被 const 对象调用。
- 与普通成员函数构成重载。
- 明确代码意图,增加安全性。
13. 简述 mutable 关键字的作用,并给出一个典型使用场景。
答案要点:
mutable修饰的成员变量,即使在 const 成员函数中也可以被修改。- 典型场景:缓存数据、互斥锁(在 const 函数中加锁以保护线程安全)、访问计数器等。这些变量的改变不影响对象的"逻辑常量性"。
14. 分析以下代码的输出:
cpp
class A {
public:
void show() { cout << "non-const" << endl; }
void show() const { cout << "const" << endl; }
};
int main() {
A a;
const A ca;
a.show();
ca.show();
return 0;
}
答案要点 :输出 non-const 和 const。const 对象调用 const 版本,非 const 对象优先调用非 const 版本。
15. const 成员函数中,this 指针的类型是什么?
答案要点 :const 类名* const。即指向常量对象的常量指针,因此不能通过 this 修改成员变量(mutable 除外)。
六、const 与迭代器
16. iterator 和 const_iterator 有什么区别?
答案要点:
iterator:可读写所指元素。const_iterator:只能读,不能修改元素(*it返回 const 引用)。类似于const T*。- 还有
const iterator(迭代器本身是常量),类似于T* const,实际使用较少。
17. cbegin() / cend() 的作用是什么?
答案要点 :无论容器本身是否为 const,都返回 const_iterator,便于编写只读遍历代码,增强安全性。
这些题目覆盖了 const 在面试中从基础到进阶的核心考察点,如果能全部清晰回答,const 这块基本就算过关了。