经典题解析:const int* 和 int* const,究竟谁不能改

图中把指针对象与所指对象分成两列,const 限制落在哪一层就检查哪一层。

const int* p 和 int* const p 只差一个位置,却限制了不同的修改动作。最容易犯的错误,是把“不能通过这个指针改对象”,听成“这个对象在任何地方都不能变”;或者把指针本身固定,误认为它指向的值也固定。

先找指针本身的名字

看 int* const p,可以从名字 p 附近开始读:p 是一个 const 指针,它指向 int。指针对象本身在初始化之后不能重新指向别处,但如果所指对象原本可修改,就仍能通过解引用修改它。

看 const int* p,p 是一个普通可修改的指针,它指向 const int。可以把 p 改为另一个合适对象的地址,但不能通过 p 的这条访问路径写入所指值。int const* p 与这种写法含义相同,不要因为 const 在 int 后面就把它读成固定指针。

如果两层都要限制,可以写 const int* const p。此时既不能重新赋值这个指针,也不能通过它修改所指对象。两层约束分别成立,不需要创造一种“最强 const”来解释。

用两次赋值看清区别

下面是一个只包含合法操作的小例子:

#include <iostream>
int main() {
    int a = 3, b = 8;
    const int* p = &a;
    p = &b;
    int* const q = &a;
    *q = 5;
    std::cout << *p << ' ' << *q << std::endl;
}

最后输出八和五。p 重新指向 b,这没有修改任何被它当成 const 访问的对象;q 保持指向 a,写入的是原本可修改的 a,而不是改变 q 保存的地址。若改成给 *p 赋值,或把 q 赋成 &b,普通类型检查就会拒绝相应操作。

一个地址,两层不同的修改权限

只读视图不代表对象永远不变

假设 a 是普通 int,p 指向 a 并把它当作 const 访问。其他合法代码仍可以直接修改 a,之后通过 p 读取时会看到修改后的值。const 在这里限制的是这条访问路径,并没有把整个对象的所有别名一并冻结。

因此,一个接收 const 指针的函数,表达的是它不通过这个入口修改相应对象的约束,不是给调用者提供不可变快照。若其他线程同时访问,仍要遵守相应的同步规则;const 本身不提供跨线程可见性或互斥保证。

这点也解释了为什么“函数参数是 const,就一定线程安全”不成立。对象可能包含其他可达状态,也可能经由别名被改变。接口可写性与并发正确性是不同层面。

去掉 const 不是通行证

类型转换在特定场景下可以去掉访问路径上的 const,但是否允许真正写入,还要看底层对象原本是不是 const。如果对象原本被定义为 const,强行绕过类型限制去修改,会触发未定义行为,不能靠一次运行没有崩溃证明合法。

相反,如果底层对象原本可修改,只是通过 const 视图传递,去掉限定后的某些操作可能具有不同结论。这类代码也应有明确接口理由,不能把转换当作修复编译错误的默认动作。编译器阻止写入时,先重新检查设计意图。

多级指针还存在额外的限定转换规则。例如某些看起来只是“加一层只读”的转换会破坏类型安全,并非都被允许。回答基础题时可以指出这一边界,不必凭一层指针的直觉推导全部多级情况。

还要注意生命周期:只读指针并不能延长被指对象的寿命。如果对象已经销毁,const 再严格也不能让解引用重新变得合法。可写性和有效性必须分别检查。

参数与成员分别看顶层限定

按值传递一个指针时,函数拿到的是地址值的副本。把这个局部副本设为 const,可以防止函数内部重新赋值它,却不会让调用方的指针变量随之固定。所指对象的限定才决定这条间接访问路径能否写入数据。

设计接口时,如果函数只读取一段对象,指向 const 的指针能帮助表达意图;如果只是防止实现者不小心改错本地地址,顶层 const 则更多约束函数内部实现。两者用途不同。

遇到声明先问两次:地址能不能换,沿该地址能不能写。你能否构造一个例子,让 const int* 的读取结果发生变化,同时整个过程没有任何去掉 const 的强制转换?