在C++面试中,const int* p、int const* p 和 int* const p 的区别几乎是必考题。掌握它不仅是应付面试,更是理解C++类型安全和设计哲学的关键。
一个让你终身不忘的口诀
“左定值,右定向”。
这六个字是什么意思?我们直接通过例子来理解,避免晦涩的理论。
int a = 10;
int b = 20;
const int* p1 = &a; // const在*左边
int const* p2 = &a; // const在*左边,和上面等价
int* const p3 = &a; // const在*右边
第一种和第二种写法完全等价!const 在 * 左边,表示 “指向的值不能通过这个指针修改”,但指针本身可以指向别的地址。
*p1 = 15; // 编译错误!不能通过p1修改a的值
p1 = &b; // 合法!p1可以指向b
第三种,const 在 * 右边,表示 “指针本身存储的地址不能改”,但指向的值可以通过这个指针修改。
*p3 = 15; // 合法!可以通过p3修改a的值
p3 = &b; // 编译错误!p3不能重新指向其他地址
核心规则就这么简单,牢牢记住“左定值,右定向”即可。

为何会有这种设计?
我们需要理解编译器的视角。const 关键字本质上是进行 “访问权限控制”。
当 const 在 * 左边时,编译器理解为你做出了一个承诺:“这个指针不会修改它指向的内容”。这在函数参数传递中极其有用,是一种良好的编程契约。例如:
void printArray(const int* arr, int size){
for(int i = 0; i < size; i++) {
cout << arr[i] << " ";
}
}
这里使用 const int* arr 明确告知函数的调用者:“请放心把数组传给我,我承诺不会修改你的数据”。这体现了C++的“契约精神”,能有效防止意外修改,提升代码安全性。
当 const 在 * 右边时,编译器理解为:“这个指针变量本身的地址是固定的”。这种用法在类成员变量中较为常见。
class MyClass {
int* const p; // 这个指针一旦初始化就不能更改指向
public:
MyClass(int* ptr) : p(ptr) {} // 必须在初始化列表里初始化
};
扩展到const成员函数
面试官通常不会只问指针,紧接着就会问:“那 const 成员函数呢?”
首先需要理解 this 指针的变化。
- 在普通成员函数内部,
this 指针的类型是 MyClass* const。这意味着指针本身不能改(由编译器保证),但指向的对象(即对象的成员变量)可以被修改。
- 在
const 成员函数内部,this 指针的类型变成了 const MyClass* const。这意味着不仅指针本身不能改,通过 this 指针访问到的对象内容(成员变量)也被视为不可修改。
class Person {
string name;
int age;
public:
// const成员函数,承诺不修改任何成员变量
void showInfo() const {
cout << name << " " << age << endl;
// age = 30; // 编译错误!不能在const成员函数内修改成员变量
}
void setAge(int a){
age = a; // 普通成员函数,可以修改成员变量
}
};
const对象只能调用const成员函数
这是一个核心规则,必须牢记:
const Person p1("张三", 25);
p1.showInfo(); // 合法!const对象调用const成员函数
p1.setAge(30); // 编译错误!const对象不能调用非const成员函数
Person p2("李四", 30);
p2.showInfo(); // 合法!普通对象可以调用const成员函数
p2.setAge(35); // 合法!普通对象可以调用非const成员函数
为什么这样设计?逻辑很清晰:如果允许 const 对象调用非 const 成员函数,那么该函数就有可能修改对象状态,这直接违背了 const 对象“不可修改”的语义。编译器必须在编译期就杜绝这种可能性。

实战场景:设计一个不可修改的配置类
让我们来看一个实战例子。假设你需要设计一个配置管理系统,要求配置对象一旦创建就不可修改,但可以被多个模块安全地读取。
class AppConfig {
private:
const string dbHost; // const成员变量,必须在初始化列表初始化
const int maxConnections;
const int timeout;
vector<string> allowedUsers; // 普通成员变量,但在const对象中也不能被const成员函数修改
public:
AppConfig(const string& host, int maxConn, int time)
: dbHost(host), maxConnections(maxConn), timeout(time) {
allowedUsers.push_back("admin");
}
// 所有getter都是const成员函数,提供只读访问接口
const string& getDbHost() const{
return dbHost;
}
int getMaxConnections() const{
return maxConnections;
}
const vector<string>& getAllowedUsers() const{
return allowedUsers;
}
// 注意:没有提供任何setter方法,从接口层面确保对象不可修改
};
使用示例:
void initializeService(const AppConfig& config){
string host = config.getDbHost(); // 合法
int maxConn = config.getMaxConnections(); // 合法
// config.dbHost = "newhost"; // 编译错误!dbHost是const成员
// config.setDbHost("newhost"); // 编译错误!类设计时就没提供setter
}
int main(){
AppConfig config("localhost", 100, 30);
initializeService(config);
return 0;
}
在这个设计中,const 发挥了双重作用:
- const成员变量 确保了核心配置项在构造后不可更改。
- const成员函数 & const引用传递 构成了编译期的安全保证。当以
const AppConfig& 形式传递对象时,编译器会确保没有任何代码能调用可能修改该对象的函数。
这就是“const正确性”(const-correctness)的设计哲学,它能使你的代码更健壮、意图更清晰,是编写高质量、可维护C++代码的重要实践,也是面试求职中考察候选人代码素养的常见点。
mutable关键字的用途
有时,我们可能需要在 const 成员函数中修改某些特定的成员变量,例如为了实现缓存机制。这时就需要使用 mutable 关键字。
class DataProcessor {
vector<int> data;
mutable int cachedSum; // mutable成员,即使在const函数里也能修改
mutable bool cacheValid;
public:
int getSum() const{
if(!cacheValid) {
cachedSum = 0;
for(int num : data) {
cachedSum += num;
}
cacheValid = true; // 在const成员函数中修改mutable变量是允许的
}
return cachedSum;
}
void addData(int num){
data.push_back(num);
cacheValid = false; // 数据变化,缓存失效
}
};
mutable 是一个例外,它告诉编译器:“这个变量虽然位于 const 对象或 const 成员函数中,但我需要修改它,请放行”。使用 mutable 需要谨慎,它不应被滥用来破坏 const 语义,而只应用于这种不影响对象“逻辑状态”(logical constness)的场合,比如缓存、调试计数、线程同步原语等。
希望这篇从口诀到原理,再到实战的解析,能帮助你彻底掌握C++中 const 与指针、成员函数的各种用法。理解并善用 const,是迈向成熟C++开发者的关键一步。如果你想深入探讨更多C++核心话题,欢迎到技术社区云栈社区交流分享。