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 之后变得更加重要,因为constexprconstinitconsteval这些新关键字的加入,让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 相关的编译错误时,按照以下步骤排查:

  1. 检查函数签名: 函数参数是否少了 const?返回类型是否应该是 const?

  2. 检查对象类型: 对象是否是 const的?如果是,只能调用 const 成员函数。

  3. 检查成员变量: 是否需要 mutable?是否试图在 const 成员函数中修改非 mutable 成员?

  4. 检查模板推导: 模板参数是否推导成了非 const 类型?用typeid(T).name()或编译器错误信息查看推导结果。

  5. 检查赋值操作: 是否试图对 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 滥用。

更多推荐