☰
C++ 常量指针与指针常量的实现示例
2026/10/8 8:33:35 网站建设 项目流程

前言

"常量指针"和"指针常量"这一对术语,是 C++ 面试与日常代码里最经典的命名陷阱。它们在中文里只差一个字的顺序,在英文里也是同一批单词的重新排列:


  • 常量指针= pointer to const(指向常量的指针):const int* p

  • 指针常量= const pointer(指针本身是常量):int* const p


再加一个组合版:指向常量的常量指针:const int* const p。

问题在于,光记住中文名对写代码毫无帮助——真到写的时候,你依然会犹豫"const该放前面还是后面"。本文换一个思路:放弃中文术语的记忆,改用"从右往左读"(right-to-left rule)的解析方法,配合可编译的完整示例和真实坑点,让你一眼就能读懂任意复杂的const声明。

一、先用一句话破题:const修饰它左边的类型;如果左边没有类型,就修饰右边的

这是const的位置规则(也适用于volatile):

const出现在类型说明符之后,修饰的是它左边最近的那个类型;若const出现在声明的最前面(左边没有类型),则修饰右边的类型。

const int a = 1; // const 在 int 左边 → 等价于 int const a int const b = 2; // 与上一行完全等价 const int* p1; // const 修饰 int → 指向常量的指针(可改指向,不可改值) int const* p2; // 与 p1 完全等价 int* const p3; // const 修饰 p3 本身 → 指针常量(不可改指向,可改值) const int* const p4; // 两者都 const

二、从右往左读(right-to-left rule)

解析任何复杂声明的通用方法:从变量名开始,先往右读,遇到括号就掉头往左读。

const int* const p = &x; // ↑ 从这里开始

步骤:


  1. 找到变量名p。

  2. 往右看:const→ "p is a const..."(p 是一个常量)。

  3. 往左看:*→ "...pointer to..."(指针,指向)。

  4. 继续往左:const int→ "...const int"(常量 int)。


合起来:p 是一个常量指针,指向 const int。

再练一个:

int* const* pp; // p 是(往左 *)指针,指向(往左 const *)常量指针,指向 int // 即:pp 是"指向 [常量指针] 的指针" const int** q; // q 是指针,指向指针,指向 const int

务必记住:const从不"跳过"*去修饰更内层。判断某个const到底约束谁,就看它夹在哪两个*之间。

三、三种写法对照表

声明中文名能否改指向能否改所指的值适用场景
const int* p指向常量的指针(常量指针)✅ 能❌ 不能只读遍历、只读函数参数
int* const p指针常量❌ 不能✅ 能固定绑定某对象,如类成员、句柄
const int* const p指向常量的常量指针❌ 不能❌ 不能常量表中的固定条目
int* p普通指针✅ 能✅ 能一般情况

用中文术语记忆时,一个可靠的技巧是:术语的最后一个名词才是变量的本质。


  • "常量指针" → 本质是指针 →const int*(指针可以动,值不能动)

  • "指针常量" → 本质是常量 →int* const(指针不能动)


也就是说:"常量指针"是"指针","指针常量"是"常量"。这个顺序规则和"红苹果"是苹果、"苹果红"是颜色的道理一样。

四、完整可编译示例

#include <iostream> int main() { int a = 10; int b = 20; // ---------- 1. 指向常量的指针 const int* ---------- const int* p1 = &a; std::cout << "*p1 = " << *p1 << '\n'; // 10 // *p1 = 99; // ❌ 编译错误:不能通过 p1 修改 p1 = &b; // ✅ 可以改指向 std::cout << "*p1 = " << *p1 << '\n'; // 20 // ---------- 2. 指针常量 int* const ---------- int* const p2 = &a; *p2 = 99; // ✅ 可以改值 std::cout << "a = " << a << '\n'; // 99 // p2 = &b; // ❌ 编译错误:不能改指向 // ---------- 3. 指向常量的常量指针 ---------- const int* const p3 = &b; // p3 = &a; // ❌ 不能改指向 // *p3 = 1; // ❌ 不能改值 std::cout << "*p3 = " << *p3 << '\n'; // 20 // ---------- 4. 普通指针可以隐式转到"指向常量" ---------- int* p4 = &a; const int* p5 = p4; // ✅ 权限收紧(safe) // int* p6 = p5; // ❌ 权限放宽,编译错误 // int* p7 = p3; // ❌ 同上 return 0; }

编译验证命令:

g++ -std=c++17 -Wall -Wextra -Wpedantic const_demo.cpp -o const_demo ./const_demo

五、为什么"指向常量的指针"能隐式转换,反过来不行

这是const正确性(const-correctness)的核心规则:限制只能收紧,不能放宽。

int x = 1; const int* cp = &x; // ✅ int* → const int* (收紧权限) // int* up = cp; // ❌ const int* → int* (放宽权限,禁止)

如果允许放宽,就等于绕开了const承诺:

const int c = 42; // int* bad = &c; // ❌ 若允许,就能通过 bad 修改真正的常量对象 → UB

同样的规则适用于引用、std::span、智能指针:

int y = 1; const int& cr = y; // ✅ // int& ur = cr; // ❌ const std::shared_ptr<int> sp; // 指向关系本身不可改 std::shared_ptr<const int> scp; // 指向的对象不可改

注意:const std::shared_ptr<T>与std::shared_ptr<const T>是两回事——前者是"指针常量"(不能改指向),后者是"指向常量的指针"(不能改值)。这与裸指针完全对应。

六、函数参数与返回值中的 const 指针

6.1 参数

#include <cstddef> struct Packet { char header[16]; std::size_t length; }; // 只读入参:调用者不会被改,且语义上承诺不改 void parse(const Packet* pkt) { std::size_t n = pkt->length; // 只能读 (void)n; } // 需要产出:用"指针常量"其实没有意义(形参本就是副本),应写 Packet* void fill(Packet* out) { out->length = 0; } // 需要改"指针本身":指针的引用(比 Packet** 更安全) void reset(Packet*& p) { p = nullptr; }

一条实践建议:函数签名里int* const p做形参几乎没有价值。因为形参p是调用者实参的副本,改不改p本身调用者都感知不到。所以顶层const(top-level const)在形参位置会被编译器忽略:

void f(int* const p); void f(int* p); // ❌ 重复定义!顶层 const 在形参中被忽略,两者是同一函数

同样地:

void g(const int x); // 顶层 const,与 void g(int x) 是同一函数

6.2 返回值

class Cache { public: const int* find(int key) const; // ✅ 返回只读指针,防止调用者修改内部数据 // int* find(int key) const; // ❌ 破坏封装:调用者能改内部存储 private: int data_[8]{}; }; const int* Cache::find(int key) const { return (key >= 0 && key < 8) ? &data_[key] : nullptr; }

对于返回指针本身不可改的诉求,返回int* const是无意义的(返回值是右值,本来就不可赋值),真正有意义的是返回引用:

// 返回"不能改指向"的别名 → 用引用表达 const int& at(std::size_t i) const;

七、const成员函数里的指针成员

这是类设计中最常见的一处误解:const成员函数只把非static数据成员变成 const,成员指针的"指向"变成不可改,但"指向的对象"仍可改。

#include <iostream> #include <cstddef> class Widget { public: Widget(int* buf, std::size_t n) : buf_(buf), size_(n) {} // const 成员函数 std::size_t size() const { return size_; } // ✅ 只读 // ❌ 编译错误:在 const 成员函数中,buf_ 的类型是 int* const, // 即"指针常量"——不能改指向。这里想改指向,报错。 // void rebind(int* p) const { buf_ = p; } // ✅ 指向的数据本身仍可修改(除非成员声明为 const int*) void poke(std::size_t i, int v) const { buf_[i] = v; } // ✅ 真正的只读:成员类型为 const int* int peek(std::size_t i) const { return buf_[i]; } private: int* buf_; // 指向可变数据 std::size_t size_; }; int main() { int raw[4] = {1, 2, 3, 4}; const Widget w(raw, 4); w.poke(0, 100); // ✅ const 对象也能改指向的数据 std::cout << raw[0] << '\n'; // 100 std::cout << w.size() << '\n'; return 0; }

如果想让"对象 const 时数据也不可改",把成员声明为const int* buf_;。这叫做逻辑常量性(logical constness)与物理常量性(physical constness)的区别。C++ 的const是按位常量(bitwise const):只要没有修改成员的位,就算合法,哪怕指向的对象被改了。

用mutable打破 const

class Lazy { public: int value() const { if (!computed_) { // ✅ 在 const 函数里改 mutable 成员合法 cache_ = 42; computed_ = true; } return cache_; } private: mutable int cache_ = 0; mutable bool computed_ = false; };

八、const与指针混用的正确转型方式

只有在确实需要"取消 const"时,才用const_cast,并且前提是原对象本身不是 const:

#include <iostream> #include <cstring> // 假设要调用一个老式 C API,它参数没写 const 但实际不修改 void legacyApi(char* s); // 旧接口,签名不 const void wrapper(const char* s) { legacyApi(const_cast<char*>(s)); // ✅ 前提:s 指向的对象本来就不是 const 的 } int main() { char buf[] = "hello"; wrapper(buf); // ✅ 底层是数组,非 const,合法 // const char* lit = "world"; // wrapper(lit); // ❌ 若走到这里并真被修改,就是 UB return 0; }

规则:只有当底层对象非 const 时,const_cast后修改才是合法的。修改一个真正的常量对象是未定义行为,可能出现在只读段(.rodata)而直接段错误。

另外,存在一种著名 UB 场景:C++ 里对const成员加mutable是为了合法破例,而在const成员函数内const_cast掉this去修改成员,只有当原对象本身非 const 时才合法:

struct S { int v = 0; void f() const { // ❌ 若原对象是 const S,这里是 UB // const_cast<S*>(this)->v = 1; } };

常见坑点

坑点 1:把const int*当成int* const用

const int* p = &a; // *p = 5; // ❌ 编译错误(想改值) p = &b; // ✅ (其实是这个能改)

记忆口诀:const离谁近就管谁。const int*中const紧挨int,管的是值;int* const中const紧挨*(即指针名),管的是指针。

坑点 2:这是"能改指向"还是"能改值"——用"读法"验证

const int* p; // 从 p 往左读:p is a pointer to const int // → 指针可变,值不可变 int* const q; // 从 q 往右读到 const,再往左读 *:q is a const pointer to int // → 指针不可变,值可变

坑点 3:形参里的顶层const会被忽略,导致重定义

void f(int* p); // void f(int* const p); // ❌ redefinition:与上一条是同一函数

坑点 4:const成员函数不能改成员,但可以改mutable成员与指向的数据

struct S { int a = 0; mutable int b = 0; int* p = nullptr; void f() const { // a = 1; // ❌ b = 1; // ✅ mutable if (p) *p = 1; // ✅ 改的是 p 指向的对象,不是 p 本身 } };

坑点 5:char*与字符串字面量

// ❌ C++11 起非法:字符串字面量类型是 const char[N] // char* s = "hello"; const char* s1 = "hello"; // ✅ char s2[] = "hello"; // ✅ 数组拷贝,可修改

如果硬写char* s = "hello";,在 C++11 前能编译(有警告),但修改s[0]会崩溃,因为字面量在只读段。

坑点 6:把const T*强转成T*去修改真正的常量

const int c = 42; int* bad = const_cast<int*>(&c); // *bad = 100; // ❌ UB:修改真正的常量对象 std::cout << c << '\n'; // 可能仍输出 42(编译器常量折叠)

这个坑最阴险的地方在于:你改了内存,但打印出来还是旧值,因为编译器把c直接折叠成常量 42。这会让调试变得极其困惑。

坑点 7:智能指针的 const 位置

const std::shared_ptr<int> p1; // p1 是常量:不能改指向,能改 *p1 std::shared_ptr<const int> p2; // 能改指向,不能改 *p2 std::shared_ptr<int> const p3; // 与 p1 等价 // 与裸指针对应关系: // int* const ↔ const std::shared_ptr<int> // const int* ↔ std::shared_ptr<const int>

另外注意,std::unique_ptr<T>的const版本几乎所有操作都不可用(reset、release、swap均非 const),所以不要写const std::unique_ptr<T>,除非成员函数且只读get()。

坑点 8:operator[]只提供 const 版本会失去写入能力

struct A { int v[4]{}; // ❌ 只有 const 版本 → 非 const 对象也无法写入 const int& operator[](std::size_t i) const { return v[i]; } // ✅ 补上非 const 版本 int& operator[](std::size_t i) { return v[i]; } };

坑点 9:const与typedef/别名结合时不受"从右往左"控制

using IntPtr = int*; const IntPtr p = &a; // ❗这里 const 修饰的是 IntPtr 整体 → int* const // 等价于 int* const p,而不是 const int* p // 因为 typedef 名是不可分割的整体

这是极少有人注意的规则:const加在 typedef 名上时,修饰的是整个别名类型。想得到const int*必须写using ConstIntPtr = const int*;或直接写const int*。

总结

判断方法说明
从右往左读从变量名出发,先右后左,const就近修饰
就近原则const紧挨类型 → 管值;紧挨*→ 管指针
术语尾字"常量指针"是指针,"指针常量"是常量
权限规则收紧(T*→const T*)可隐式,放宽必须const_cast且需底层非 const
顶层 const 忽略形参的int* const、const int等价于无 const

三句话收尾:


  1. const int* p——不能改*p,能改p。

  2. int* const p——能改*p,不能改p。

  3. const int* const p——两个都不能改。


遇到新的复杂写法时不要背结论,直接用"从右往左读"现场解析;遇到转型时不要靠const_cast绕过去,而要问自己:这里破坏 const 正确性,是否说明接口设计错了?绝大多数情况下答案是"是"。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询