尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

C++ const与指针组合规则详解:从const int *p到int *const的彻底理解

发布时间:2026/9/26 21:23:05

资讯中心
01
ARTICLE

C++ const与指针组合规则详解:从const int *p到int *const的彻底理解

C++ const与指针组合规则详解:从const int *p到int *const的彻底理解
刚学C的时候身边好几个朋友都跟我说过同一句话“别的我都还能忍一碰到const 限定符和指针组合在一起脑子就嗡的一声。” 这话我特别理解因为const int *p、int *const p、const int *const p这三副面孔长得像三胞胎脾气却完全不同新手在这个地方栽跟头几乎是必然的。这篇东西就是写给“被指针和 const 绕晕的人”看的无论你是在校学生、转行自学还是正在准备面试想把这部分彻底讲清楚都可以放心往下读。我尽量不用教科书说话的方式而是把C 里 const 与指针的组合规则拆成一个个能看得见摸得着的小例子看完你再去写代码至少不会再对着编译器的红色报错发呆。1. const 到底在“限定”什么它不是锁住数据而是锁住通道很多人刚开始学 const 时会以为它像冰箱里冷冻层一样把某个变量的值“冻住”以后谁也动不了。这个理解不精确会直接导致后面学指针时混乱。1.1 const 修饰的是“变量的名义权限”先看最简单的场景const int x 10; x 20; // 编译错误x 是只读的这里的x确实不能被修改。但麻烦的是同样一份数据完全可以通过另一个非 const 的变量名去操作int y 10; const int ref y; // ref 只是“名义上只读” int* p y; // p 可以直接改 y *p 99; std::cout y std::endl; // 输出 99ref 也跟着变成 99看出问题了吗ref是 const 的但y不是。const 并没有把内存里的数据“冻住”它只是给了你一个承诺从 ref 这条路径访问时不允许写。数据本身该怎样还怎样别的非 const 路径照样可以改它。这个区别特别重要。const 限定的从来不是“物理内存”而是“当前这个变量名/这条路径的访问权限”。用大白话说const 相当于在变量名外面包了一层“只读壳”而不是把内存焊死。1.2 为什么 C 要给你这个“形式上的锁”初学者最容易问的问题是既然 const 可以被绕过那它还有什么用实际上 const 的价值不在于防黑客、防恶意代码而在于自我约束和接口承诺。举个生活例子你去食堂打菜阿姨给了你一个餐盘。餐盘上贴了纸条“今日特供只许看菜名不许夹菜”这是对你这个访问者的约束但食堂大厨自己可以随时往锅里加盐。当你在项目里把一个函数参数声明成const std::string时你是在对所有调用者说“我这里只读不修改你的字符串你放心传。” 更重要的是编译器会帮你盯住如果有人在函数体里偷偷s.push_back(!)编译直接报错不让坏习惯悄悄溜进代码里。这也是为什么后来学类、学函数重载、学移动语义都会反复强调 const 的重要性——它不是语法装饰品而是 C 设计者给你的一套“契约工具”。1.3 const 变量必须初始化这不是故意为难你还有个小细节const int x;不初始化直接编译错误。因为一个只有只读权限的名字如果连初始值都没给那它里面的值就永远无法被设置了这毫无意义。类比来说不许别人改的保险箱你总要先把东西放进去再锁门。而普通变量int a;没初始化只会得到一个不确定的垃圾值但语法上还能通过。到这里const 的基础含义已经说清楚const 限定的是通过这个变量名修改内容的资格。下面就可以上主菜了——加上指针之后问题会多出一层维度。2. 指针的两层身份纸条和房间const 到底贴在哪儿为什么const一碰到指针就容易乱因为指针这个东西是“两层”的。我们必须先把这两层剥开所有规则都会变得顺理成章。2.1 指针变量本身和它指向的对象是两码事把指针想成一张写着房间号的纸条。房间是数据纸条是装着地址的指针变量。纸条本身可以不换但它写的房间号可以变——对应“指针变量可以指向不同的地址”房间里的东西可以换也可以不换——对应“目标对象可以被修改也可以是只读”。const 到底谁锁锁纸条还是锁房间这就是所有混乱的来源。来看两个最简单的对比int a 10, b 20; const int* p1 a; // 房间是只读的P1 不能通过它改 a 的值但纸条可以改指向 p1 b; // 合法换个房间 // *p1 30; // 非法这个纸条上写着“只许看不许改” int* const p2 a; // 纸条本身是固定的p2 永远指向 a但房间值可以改 *p2 30; // 合法a 变成 30 // p2 b; // 非法这个纸条被胶水粘死了不能换对照一下const放在*的前面和后面效果完全不同。const int *p里的*p是“房间里的东西”const 修饰它就是房间只读int *const p里的p是“纸条本身”const 修饰它就是纸条不能换。2.2 记忆诀窍从右往左读看 const 离谁更近多年以后你可以把各种复杂声明背下来但刚开始最实用的招式是从右往左读const int *pp是指针指向const int。哈这里的“指向 const int”本质上就是说目标类型带有只读属性。int *const pp本身是 const 的指针值不可变。const int *const pp是 const 的它指向的int也是 const 的。想更省事一点还有个口诀const 修饰的是离它最近的“实质性变量”。在const int *p中const 跟着int两者搭配成“const int 类型的对象”所以被锁的是目标在int *const p中const 跟在*后面它黏着的是指针变量p自己所以被锁的是指针本身。2.3 为什么文本类资料里叫法还不统一你上网搜索会发现同样的东西有不同叫法const int *p有人叫“常量指针”也有人叫“指向常量的指针”int *const p有人叫“指针常量”还有人叫“常量指针”。这纯粹是翻译和教材习惯造成的坑非常讨厌。所以我建议你不要死记中文名词而是记住一个原始英文描述pointer to const int指向const int的指针 → 目标只读const pointerto int本身是 const 的指针 → 指针值只读。面试时你用英文短语描述都不容易出错。日常交流里如果只说中文先跟对方确认指的是哪一种免得吵架。3. 三副面孔逐一拆解const int *、int *const、const int *const光看懂原理不够咱们把三种组合放到真实场景里一个一个过顺便给出可运行代码和典型用途。3.1 第一种const int *p—— 看得见摸不得这种写法最常见于“只读遍历”和“字符串常量”。比如你要写一个函数打印一个 int 数组的内容void printArray(const int* arr, int size) { for (int i 0; i size; i) { std::cout arr[i] ; } std::cout std::endl; }这里的const int* arr保证了在函数内部不会意外修改数组元素。如果你手贱写了arr[0] 100编译器立刻报错避免你在排查诡异 bug 时浪费两个小时。这种指针还可以随意指向另一个 int 变量const int* p a; p b; // 没问题注意一种常见场景把字符串字面量赋给指针。在 C 中字符串字面量hello的类型是const char[6]所以在较新的 C 标准里你应该用const char* str hello;来接收。用char* str hello;会得到编译警告甚至错误因为通过这个指针去修改字符串字面量是未定义行为。记住凡是可能指向“只读区域”的指针声明时就要带上 const。3.2 第二种int *const p—— 铁了心不换地址这种“指针本身只读”的写法适合你要把一个地址固定在某个变量上确保整个生命周期里它不乱指。比如嵌入式开发里你可能会定义指向某个寄存器地址的常量指针int* const REG_STATUS reinterpret_castint*(0x40000000); // REG_STATUS otherReg; // 编都不让编地址不能换 *REG_STATUS 1; // 但寄存器内容可以改在普通业务代码里int* const出现的频率不如第一种高因为局部变量如果不想换地址直接用引用int ref a;更自然。但了解它仍然是必要的特别是面试时喜欢拿它和引用作对比。3.3 第三种const int *const p—— 两头都锁死最安全这个组合表示指针变量不能更换指向指向的对象也不能通过这个指针修改。int value 7; const int* const p value; // p other; // 不行指针只读 // *p 10; // 不行目标只读哪个场景会用到比如全局配置表const int* const kConfig globalConfigValue;让别人拿到的是一把“只读且不能换门牌”的钥匙彻底封死误操作的可能。写底层库、操作系统模块时这种“双重 const”非常常见因为它把设计者的意图表达得无比清晰。3.4 三种组合一张表看清声明写法指针本身纸条指向对象房间典型用途const int *p可改只读只读遍历、字符串字面量接收int *const p固定可改绑定寄存器地址、固定入口指针const int *const p固定只读全局常量引用、只读配置入口3.5 别忽略“多个 const 变量”的声明陷阱一个小测验经常在面试中出现const int *p1, p2;请问p2是什么类型很多人以为是const int *错了。p2只是const int不是指针。因为*只作用于紧跟它的p1并不会波及p2。想让两个变量都是 const 指针得写成int *const p1, *const p2;这个细节很反直觉实际工作中最好一行只声明一个指针变量既清晰也不会踩这个坑。4. 函数参数里的 const为什么老手都爱写 const 引用和 const 指针指针和 const 最大的战场不在变量声明而在函数参数。你和别人协作写代码时函数的“接口长相”直接影响到整个项目好不好维护。4.1 值传递的代价可能比你想象的大假设你要写一个函数读取一份大文件的内容然后统计里面的单词数。文件内容存在std::string里这个对象可能几 MB 甚至更大。int countWords(std::string s) { ... }这么写每次调用都会做一次完整的字符串拷贝拷贝一个几 MB 的字符串又慢又费内存。如果你改成传指针int countWords(std::string* s) { ... }拷贝倒是省了但函数内部既可以读也可以写。万一函数里不小心改了字符串调用者完全不知道排查起来很麻烦而且调用者还要担心空指针问题。所以真正优雅的做法是传 const 引用int countWords(const std::string s) { ... }这里const std::string到底做了什么它相当于语法糖版的“const std::string* 指针”没有拷贝不能修改也不会出现空指针。老手看到函数签名写const std::string瞬间就知道这个函数不会动数据看到std::string就知道这个函数大概率要原地修改内容。光靠阅读签名就能读懂接口语义这是 C 的项目级素养。4.2 指针参数也一样能 const 就 const如果这个参数本质上需要传“一组数据”的起始地址比如数组那函数签名里通常写成指针void processData(const int* data, size_t count);const int* data对调用者来说是一个承诺“我只读你的数据不会改坏。” 万一内部有 bug 尝试写入编译器会当场拦住。我自己见过不少新人写库函数时参数一律不带 const理由是“先跑通再说”。结果到了后期合约问题接二连三A 模块删了 B 模块的数据C 模块想调用却发现接口不匹配只能被迫拷贝一份再传。等你在一个中型项目里吃过两次亏就会明白“参数能加 const 就加 const”基本是 C 工程界的准则不是强迫症。4.3 const 引用和 const 指针在类型上的一个小门槛再提醒一个容易被编译器报错搞懵的点普通变量的地址a可以赋给const int* p这没问题但反过来一个 const 变量的地址ca不能赋给int* p因为那样别人就可以通过这个指针去修改一个本不该修改的 const 对象。const int ca 3; const int* pc ca; // 可以 int* p ca; // 编译错误无法将 const int* 转为 int*这条规则的本质就是权限不能随便放大你手里是一张“只读通行证”不能交给想要“读写通行证”的人去干危险的事。反过来读写通行证降级为只读通行证完全合法。理解这个“权限只许缩小不许放大”的道理你以后看编译错误会顺很多。5. 最容易翻车的四个现场踩坑记录、原因分析和排查方法这部分我直接写我见过最多的翻车现场。每个从现象说到根因帮你建立自己的“排错直觉”。5.1 翻车现场一试图通过 const 指针修改目标编译器骂人const int* p x; *p 100; // error: assignment of read-only location这是最直白的错误。新手第一反应经常是“那我不加 const 不就行了”实际上应该想的是“我是不是真的需要在这里修改数据”。如果修改是有意的说明这里本来就不该用const int*如果只是手滑改成只读版本才是正解。排查思路很简单看错误信息里read-only这个词。一旦出现说明某条访问路径被 const 锁了。沿着代码看这条路径是从哪来的——是函数签名里的 const 参数还是变量声明里自带的 const。5.2 翻车现场二const_cast 强行撬锁引发的运行时崩溃情况也很经典别人给了你一个const int*你特别想改它指向的数据于是用const_castint*(p)强行去掉 const。如果原对象本来就不是 const这通常能编译通过并且工作正常int a 42; const int* cp a; int* p const_castint*(cp); *p 0; // 还行a 原本就是普通变量但如果原对象真的是 constconst int b 42; const int* cp b; int* p const_castint*(cp); *p 0; // 未定义行为可能崩溃可能悄悄有问题用const_cast等于你强行撕掉只读标签去改一个编译器认为“绝不会变”的数据。如果这块内存被放进了只读页或者编译器做了各种优化假设结果纯粹看运气。我见过一个同学在嵌入式的 flash 区域用 const_cast 写数据程序秒崩。绝大多数情况下const_cast都不该出现在代码里如果真出现了先问问设计上是不是出了偏差而不是引以为傲。5.3 翻车现场三把“底层 const”和“顶层 const”混为一谈这两句话听起来很学术说穿了就是顶层 const指针本身只读如int *const p底层 const指向的对象只读如const int *p。为什么必须区分因为它们在赋值和类型匹配时规则完全不同。底层 const 可以“升格”int a 5; int* p a; const int* cp p; // 合法可写指针能降级为只读指针但反过来不行const int* cp a; int* p cp; // 非法权限放大被拒绝顶层 const 的传递则无所谓因为它只限制指针本身不影响目标。理解了这两个词你看到复杂声明就不会再慌。5.4 翻车现场四把 const 指针和“指针指向的对象被 const 修饰”混在一起导致接口不匹配这是协作开发里最讨厌的坑。你的函数签名写的是const int* data但某些调用方手里拿的是int*明明可以传却总有人写const_cast来“适配类型”。其实完全没必要int*传给const int*参数是隐式转换编译器会自动通过。反过来如果你的函数签名写int* data另一个模块只有const int*指针那调用方就会被卡住。这时候与其用const_cast硬转不如想想是不是函数本身根本不需要修改数据改成const int* data才是正解。这种在接口层的 const 设计往往比算法实现更能影响一个项目的代码质量。写在最后的一点个人经验我从学 C 到现在带过很多新人自己也写过不少把 const 位置搞反然后被编译器教育的代码。后来我把这套规则简化成一句话先看*在谁的前面再看 const 在谁的右边。const 贴着谁谁就只有只读权限。还有个小习惯写接口和函数声明时我会先想想“这个数据交出之后是不是还要改”。如果不用改立刻加 const 引用或 const 指针。这个习惯让我的代码少了很多无谓的拷贝和奇怪的 bug也让后来接手的人少骂我好几句。以后你遇到报错信息里出现discards qualifiers这类字眼第一反应不要慌大概率就是 const 的“只读权限”和某个非只读操作冲突了按本文的方法逐层追下去很快能找到出口。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。