3.2 const 正确性:冻结的账目


3.2 const 正确性:冻结的账目

本节摘要:const 把"只读不改"从注释升级成类型条款,由编译器逐条执行。指针声明里 const 的位置决定冻结对象还是冻结指针(底层与顶层),const 成员函数承诺不改账、mutable 豁免缓存类例外。const 正确性做得彻底的接口,读者扫一眼签名就能审计出数据流向。

上一节选好了票据,本节给票据盖章:哪些操作被允许、哪些被冻结。const 是 C++ 里少有的"零运行时成本、纯收益"的机制——它不产生任何指令,却把大量错误拦在编译期。对内存账本而言,const 还有一层特别意义:被 const 引用借走的账,读的人再多也不会有人暗中改账,这在第 5 章并发场景下会显出额外价值。

先看一笔账

指针声明里的 const 位置是面试常客,但与其背口诀,不如看它冻结了账本上的哪一栏:

#include <cstdio> int main() { int account = 7; int another = 9; // 情形一:底层 const——冻结"账",不准经此指针改账 const int* p1 = &account; // *p1 = 100; // 编译错误:账被冻结 p1 = &another; // 指针本身可以改签 // 情形二:顶层 const——冻结"指针",不准改签 int* const p2 = &account; *p2 = 100; // 账可以改 // p2 = &another; // 编译错误:指针被冻结 // 情形三:双冻——都改不了 const int* const p3 = &account; // *p3 = 1; // 编译错误 // p3 = &another; // 编译错误 std::printf("account = %d(情形二改的)\n", account); std::printf("三条声明读法:指向常量的指针 / 常量指针 / 双重常量指针\n"); return 0; }

输出:

account = 100(情形二改的) 三条声明读法:指向常量的指针 / 常量指针 / 双重常量指针

读法有一个机械规则:const 在星号左边,冻的是账(对象);const 在星号右边,冻的是凭证(指针)。审计一段代码时同理——想知道"这笔账经不经过这个名字动得了",只看 const 相对星号的位置,不用猜意图。

图 3-2 const 的两个冻结方向

图 3-2 const 的两个冻结方向

一、const 成员函数与 mutable 的豁免栏

类接口上的 const 是审计收益最大的地方。一个 const 成员函数向所有调用者承诺"我不会改这个对象的账本",编译器逐字段监督:

#include <cstdio> #include <string> class Account { public: explicit Account(std::string owner) : owner_(std::move(owner)) {} // 只读接口:承诺不改账本 std::string owner() const { return owner_; } long balance() const { if (cached_ < 0) { // 缓存失效才重算 cached_ = recompute(); // 修改 mutable 成员:不违反承诺 } return cached_; } // 写接口:没有 const 修饰 void deposit(long amount) { balance_ += amount; cached_ = -1; } private: long recompute() const { return balance_; } std::string owner_; long balance_ = 0; mutable long cached_ = -1; // 豁免栏:逻辑上不影响"账目内容" }; int main() { const Account frozen{"审计科"}; // 冻结的账户 std::printf("冻结账户余额 %ld\n", frozen.balance()); // 合法:只读接口 // frozen.deposit(100); // 编译错误:冻结账户不准入账 return 0; }

输出:

冻结账户余额 0

mutable 缓存是 const 体系里唯一的正当豁免:它改变的是"实现的内部细节"(缓存、互斥量、统计计数),不是"对象的逻辑账目"。判断一个成员能不能 mutable,标准就一条——改了它,观察者从公开接口能看到任何区别吗? 看不到才豁免。缓存命中与否外部不可见,所以可以;余额显然不行。

二、案例:一份 API 的 const 审计

背景:接手一个数据访问模块,接口签名形形色色:有的参数是裸值指针,有的是值,有的是引用,调用方不知道哪些接口会改数据。

操作:按三步做 const 审计。第一步,把所有"逻辑上只读"的参数改成 const 引用,编译器随即指出所有偷偷写账的调用点——两处;第二步,类里所有只读成员函数补上尾缀 const,编译器又揪出三个"只读函数里改统计计数"的暗改,计数改 mutable 后归位;第三步,全量扫一遍返回值,把"返回可写引用暴露内部缓冲"的接口(这是 const 体系的常见漏洞——账从后门流出去了)改成返回 const 引用或值拷贝。

结果:模块的每个签名从此自文档化:const 参数与 const 成员函数构成只读面,非 const 即写入面。新成员看签名就能判断数据流向,评审时只盯非 const 接口。

解读:这次审计的收获超出预期的原因在于 const 的传递性——一个 const 引用拿到的对象,其非 const 成员函数一律不可调,只读承诺沿类型自动扩散,不需要逐点防御。const 体系做得越彻底,"只读"就越是一条不会断的链。唯一的漏洞在于后门:返回内部缓冲的可写引用、const_cast 强行解冻,审计时都要专门排查。

变式:逻辑 const 与物理 const 的边界场景(mutable 互斥量)在第 5 章会出现:一个 const 成员函数里要加锁,锁操作修改了互斥量,所以互斥量成员必须 mutable——改的是同步设施不是账目,豁免正当。C++ 标准库的多线程设施普遍按此设计。

⚠️ 常见坑:const_cast 解冻后改真 const 对象。把当初就定义成 const 的对象解冻再写是未定义行为,编译器有权把它放进只读段,运行即崩。const_cast 的正当用途只有一个方向:对接不规范的旧接口(旧接口收非 const 指针但实际不改),且前提是原对象本身不是 const。

本节要点回顾

  • const 是类型条款:零运行时成本,把"只读"交给编译器逐条执行。
  • 位置定语义:星号左冻账(底层)、星号右冻凭证(顶层),读复杂声明从右向左。
  • 成员函数尾缀 const:接口层面的只读承诺,对象为 const 时只放行 const 成员。
  • mutable 是受控豁免:只豁免"外部不可观察"的实现细节(缓存、锁、计数)。
  • 只读链有传递性:const 引用沿调用链扩散只读属性,体系越完整防线越连续。
  • 警惕后门:返回内部缓冲的非 const 引用会绕过整条防线,审计必查。

账目被冻结、票据选好了,下一章主峰登场:移动语义。当流转不可避免要"拿走"一笔账时,过户(指针交接)为什么远优于抄账(深拷贝),以及 noexcept 一个标注如何决定容器扩容的真实成本。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U