C++里 const int* 与 int const*
const int* 和 int const* 在语义上完全等价,编译器生成的机器码 100% 相同。这是 C++ 标准的明确规定。
但这里却牵扯着一个争吵了好多年的问题,那就是const 到底修饰谁?
你不理解这个,那很难走出模板推导、auto 推导、函数重载的坑。
下面直接把这件事说透。看完你若还不懂,那你很可能不适合 C++。
一、C++ 类型声明是一场语法灾难
C++ 的类型声明语法,不是为程序员设计的,是为编译器设计的。
比如这个:
int (*(*p)[10])(int);
你从右向左读,我估计你会直接被绕晕。
要读懂你需要将其拆分成 using 别名。
// 别写这种东西
int (*(*p)[10])(int);
// 写成这样,才可读
using FuncPtr = int(*)(int);
using FuncPtrArray = FuncPtr[10];
using ArrayPtr = FuncPtrArray*;
ArrayPtr p;
C++11 引入的using别名,就是为了解决这个问题的。
二、const到底修饰谁
const int* 和 int const* 没区别。但const到底修饰谁,这你该搞清楚。
C++ 语法规定:const 修饰它左边的类型,如果左边没有类型,就修饰右边的类型。
const int // const修饰int
int const // const修饰int(左边有int)
这两种写法完全等价,编译器看到的是同一个类型。但当*加入后,情况变了。
const int* p; // const 在 * 左边,修饰 int,即指向常量的指针
int* const p; // const 在 * 右边,修饰 *,即指针本身是常量
记住:const 修饰它左边的东西,除非左边没东西,才修饰右边。
这条规则在 C++11 之后变得更加重要,因为constexpr、constinit、consteval这些新关键字的加入,让const到底修饰谁从单纯的指针问题,升级成了类型系统的基础问题。
三、int* const 与 const int* const
忘掉const int*和int const*的区别吧,它们没区别。
int* const p:指针本身是常量
int x = 10;
int y = 20;
int* const p = &x;
*p = 30; // OK:修改指向的数据
// p = &y; // 编译错误:p是指针常量,不能修改p本身
Top-level const,这是关键概念。顶层 const 在模板类型推导中会被忽略,这是 C++ 模板推导的基本规则,也是很多人掉坑的地方。
template<typename T>
void f(T param);
int* const p = nullptr;
f(p); // T被推导为int*,而不是int* const
// 因为top-level const在值传递时被忽略
你要传 top-level const 给模板,必须用引用或指针。
template<typename T>
void g(T& param); // 用引用,保留top-level const
int* const p = nullptr;
g(p); // T被推导为int* const,正确保留了const
const int* const p:最严格的约束
int x = 10;
int y = 20;
const int* const p = &x;
// *p = 30; // 错误:不能修改指向的数据
// p = &y; // 错误:不能修改指针本身
p本身和p指向的数据都不可修改。这是最严格的 const 约束。
一张表看懂所有组合
|
声明 |
指针本身可改? |
指向的数据可改? |
Top-level const? |
|---|---|---|---|
int* p |
✅ |
✅ |
❌ |
const int* p |
✅ |
❌ |
❌ (low-level) |
int const* p |
✅ |
❌ |
❌ (low-level) |
int* const p |
❌ |
✅ |
✅ |
const int* const p |
❌ |
❌ |
✅ (指针部分) |
关键结论:const在*左边,修饰指向的数据(low-level const);const在*右边,修饰指针本身(top-level const)。
这个区别在模板推导、函数重载、auto类型推导中至关重要。
四、const int* vs int const*
既然const int*和int const*完全等价,为什么两种写法都能在代码库里看到?
这不是技术问题,是风格问题。但它影响你读代码的速度、你理解复杂类型的准确度、你和新人解释代码时的沟通成本。
const int*
传统 C++ 程序员、Google C++ Style Guide、绝大多数教科书都用这种写法。
优点是符合英语阅读习惯,社区接受度最高,新人上手快。缺点是const int*和int* const的 const 位置不对称,容易记混;复杂类型中,const 的位置不够统一,记忆成本高。
int const*
Bjarne Stroustrup、Herb Sutter、C++ 核心指南推荐这种写法。
优点是完美契合「const修饰左边」的规则,不需要例外;复杂类型中,const的位置统一,记忆成本低。缺点是不符合英语阅读习惯,读起来拗口。
五、实际工程中的 const
这部分不是知识点,是工程经验。
1、函数参数中的 const
// 好的写法:只读参数用 const 引用或 const 指针
void process(const std::string& str);
void process(const int* data, size_t size);
// 坏的写法:不必要的拷贝
void process(std::string str); // 每次调用都拷贝,大字符串时性能灾难
规则:
-
只读参数:用
const T&或const T* -
小型值类型(int、double、指针等):直接传值,不要用const引用
-
C++23之后:考虑用
std::span<const T>替代const T*+size_t组合
2、返回类型中的const
// C++11之前:返回const值类型有意义
const std::string get_name();
// C++11之后:返回const值类型无意义
std::string get_name();
C++11之后,返回值类型不要加const,因为编译器会阻止对临时对象的赋值。这条规则很多人不知道,还在新代码里写const std::string get_name(),画蛇添足。
3、成员变量中的const
class Widget {
const int id_; // OK:id_永远不变,必须在构造函数初始化列表中初始化
int* const buffer_; // OK:buffer_指向的地址不变,但内容可变
};
规则:
-
成员变量能用 const 就用 const,最大化不变性
-
const 成员变量必须在构造函数初始化列表中初始化,不能在构造函数体中赋值
-
mutable 成员变量可以在 const 成员函数中修改,常用于缓存、互斥锁等场景
class Cache {
mutablestd::mutex mtx_; // OK:mutex需要在const函数中lock
mutablestd::map<int, int> cache_; // OK:缓存需要在const函数中更新
public:
int get(int key) const {
std::lock_guard<std::mutex> lock(mtx_);
auto it = cache_.find(key);
if (it != cache_.end()) return it->second;
int value = compute(key);
cache_[key] = value; // OK:mutable成员可以在const函数中修改
return value;
}
int compute(int key) const; // 假设这是一个昂贵的的计算
};
4、避免 const_cast
void process(const int* p) {
int* q = const_cast<int*>(p); // 危险:如果p真的指向常量,写入是UB
// *q = 42; // 如果p指向const int,这里是未定义行为
}
规则:
-
除非你 100% 确定 p 指向的不是真正的常量,否则不要用 const_cast
-
真正需要修改常量的情况极少,几乎总是设计问题
-
如果确实需要修改,考虑修改接口设计,去掉 const
六、现代 C++ 对 const 的改进,别再用 C++98 了
1、constexpr 编译期常量(C++11起)
// C++98:运行时常量
const int size = 100;
// int array[size]; // 错误:size不是编译期常量(某些编译器扩展允许)
// C++11:编译期常量
constexpr int size = 100;
int array[size]; // OK:size是编译期常量
规则:
-
能用
constexpr就用constexpr,它保证编译期求值 -
const只保证运行时不变,不保证编译期不变 -
constexpr变量是implicit const,但你不应该写constexpr const
2、consteval 必须编译期求值(C++20起)
consteval int square(int x) { return x * x; }
int arr[square(5)]; // OK:编译期计算
// int x = 5;
// int y = square(x); // 编译错误:x不是编译期常量
使用场景:当你需要确保某个计算在编译期完成时,比如加密算法的S盒生成、查找表的预计算、编译期字符串处理。
3、const 与 auto 类型推导
const int x = 10;
auto a = x; // a的类型是int,不是const int(top-level const被忽略)
auto& b = x; // b的类型是const int&(low-level const被保留)
规则:
-
auto推导时,top-level const被忽略,low-level const被保留
-
想要保留top-level const,用
auto&或const auto& -
C++14起,lambda参数也可以用
auto,规则相同
七、常见错误
1、返回局部变量的 const 引用
const std::string& get_name() {
std::string name = "Alice";
return name; // 错误:返回局部变量的引用
} // name销毁,引用悬挂
正确写法:
std::string get_name() {
std::string name = "Alice";
return name; // OK:返回值,触发移动或RVO
}
// 或者返回const引用,但必须保证生命周期
const std::string& get_global_name() {
static const std::string name = "Global";
return name; // OK:返回静态变量的引用
}
2、const 成员的拷贝赋值运算符
class Widget {
const int id_;
public:
Widget& operator=(const Widget& other) = default; // 错误:id_是const,不能赋值
};
正确写法:
class Widget {
const int id_;
public:
Widget(int id) : id_(id) {}
// 禁止拷贝赋值
Widget& operator=(const Widget& other) = delete;
// 或者去掉const,如果你需要赋值
// int id_;
};
3、误以为 const 函数内不能修改任何东西
class Cache {
mutable std::mutex mtx_;
mutable std::map<int, int> cache_;
public:
int get(int key) const {
std::lock_guard<std::mutex> lock(mtx_); // OK:mutable成员可以修改
// ...
}
};
mutable关键字就是为这个场景设计的。const 成员函数内可以修改 mutable 成员,这是明确定义的行为,不是 hack。
4、调试技巧
当你看到 const 相关的编译错误时,按照以下步骤排查:
-
检查函数签名: 函数参数是否少了 const?返回类型是否应该是 const?
-
检查对象类型: 对象是否是 const的?如果是,只能调用 const 成员函数。
-
检查成员变量: 是否需要 mutable?是否试图在 const 成员函数中修改非 mutable 成员?
-
检查模板推导: 模板参数是否推导成了非 const 类型?用
typeid(T).name()或编译器错误信息查看推导结果。 -
检查赋值操作: 是否试图对 const 对象赋值?是否试图将 const 指针赋值给非 const 指针?
总结
const int*和int const*没有区别。但你要关注以下几个问题:
1、const修饰谁?
在*左边修饰数据(low-level const),在*右边修饰指针本身(top-level const)。
**2、遵守团队规范 **
const int*和int const*选一个,全项目统一。
3、能用 const 就用 const
最大化不变性,减少bug,让编译器帮你检查。
4、升级到 constexpr/consteval/constinit
2026年了,别再用 C++98 的 const 了。编译期计算不是可选项,是必选项。
5、理解top-level和low-level const
这是模板推导、auto 推导、函数重载的基础。不理解这个,你写的现代C++代码就是一堆 bug。
6、小心const相关的常见错误
悬挂引用、const 成员赋值、const_cast 滥用。
更多推荐

所有评论(0)