C++中的 const 与 volatile:比C强大十倍
文章目录引言一、const 在 C 和 C 中的第一个区别链接1.1 默认作用域不同1.2 C 的 const 可以作为编译期常量二、constexpr明确要求编译期计算2.1 从 const 到 constexpr2.2 constexpr 函数C17 vs C14 vs C11三、const 成员函数给函数签上不动数据的契约3.1 基本语法3.2 底层原理const this3.3 const 重载同名函数的 const 与非 const 版本四、mutableconst 成员函数的后门五、const 的指针层级顶层 const 与底层 const六、const_cast强制去除 const 的利刃6.1 唯一的正当用途兼容遗留 C API6.2 危险用法修改原本 const 的对象七、volatile不是多线程关键字7.1 volatile 的真正用途7.2 volatile 不能用于多线程同步八、const 正确性C 程序员的肌肉记忆8.1 三条黄金法则8.2 const 帮助编译器帮你找 bug总结本系列为《C深度修炼基础、STL源码与多线程实战》第8篇前置条件理解 C 语言的const基本用法了解 C 类与成员函数第2篇、第4篇引言C 语言里const基本上就是个不能修改的标注constintmax100;// 只读变量constchar*msghi;// 指向的内容不可改到了 Cconst被武装到了牙齿——它不只是不能改而是渗透到了类型系统、编译期计算、成员函数契约、甚至优化决策中。C 的const比 C 强大十倍不夸张。而volatile在 C 中的角色更加微妙——它常常被误解为多线程关键字其实根本不是。本文把这两个看似简单、实则暗藏玄机的关键字讲透。一、const在 C 和 C 中的第一个区别链接1.1 默认作用域不同// C 语言中// file1.cconstintMAX100;// 默认外部链接其他 .c 文件可以用 extern 访问// file2.cexternconstintMAX;// 可以链接到 file1.c 的 MAX// C 中// file1.cppconstintMAX100;// 默认内部链接等价于 C 的 static const int// file2.cppexternconstintMAX;// ❌ 链接错误找不到 MAXC 中const全局变量默认是内部链接internal linkage每个翻译单元有自己的副本。这个看似微小的差异源于一个设计目标让const变量可以安全地放在头文件里。// constants.h — 在 C 中这么做会链接报错多重定义C 中完全合法#pragmaonceconstintMAX_PLAYERS100;constdoublePI3.141592653589793;constcharAPP_NAME[]MyGame;如果想要 C 那样的外部链接加extern// constants.hexternconstintMAX_PLAYERS;// 声明外部链接// constants.cppexternconstintMAX_PLAYERS100;// 定义只有一份1.2 C 的const可以作为编译期常量// C 语言const 变量不能用作数组大小VLA 是另一回事constintN10;intarr[N];// ❌ C89/C90N 不是编译期常量C99 VLA 可用但有条件// Cconst 初始化为常量表达式时它就是编译期常量constintN10;intarr[N];// ✅ C完全合法N 是编译期常量std::arrayint,Narr2;// ✅ 也可以用于模板参数二、constexpr明确要求编译期计算2.1 从const到constexprconst的含义是我不会改这个值——但它的初始化可能在运行期intget_size_from_config(){returnread_config_file();}constintsize1100;// 编译期常量constintsize2get_size_from_config();// 运行期常量不能用于数组大小intarr1[size1];// ✅intarr2[size2];// ❌ size2 不是编译期常量constexpr则强制要求在编译期就能算出值constexprintsize1100;// ✅ 编译期constexprintsize2get_size_from_config();// ❌ 编译错误函数不是 constexpr// constexpr 函数也能在编译期执行constexprintsquare(intx){returnx*x;}constexprintareasquare(10);// 100编译期算出intarr[area];// ✅2.2 constexpr 函数C17 vs C14 vs C11// C11constexpr 函数基本只能写 returnconstexprintfactorial(intn){returnn1?1:n*factorial(n-1);}// C14constexpr 函数可以写循环和多语句constexprintfactorial14(intn){intresult1;for(inti2;in;i)result*i;returnresult;}// C17constexpr 可以用于 lambdaautofact[](intn)constexpr{intr1;for(inti2;in;i)r*i;returnr;};constexprintf5fact(5);// 120版本constexpr 函数能力C11单个 return 语句C14多语句、循环、局部变量C17lambda、更多标准库函数标注 constexprC20动态分配new/delete在编译期、std::vector的部分支持三、const 成员函数给函数签上不动数据的契约这是 C 中const最独特、应用最广的能力——C 语言完全没有对应物。3.1 基本语法#includeiostream#includestringclassPerson{public:Person(conststd::stringname,intage):name_(name),age_(age){}// const 成员函数承诺不修改对象的数据成员std::stringname()const{returnname_;}intage()const{returnage_;}// 非 const 成员函数可能修改voidset_age(inta){age_a;}private:std::string name_;intage_;};intmain(){constPersonp(张三,30);// const 对象std::coutp.name() p.age()\n;// ✅ const 成员函数可以调用// p.set_age(31); // ❌ 编译错误const 对象不能调非 const 成员函数}3.2 底层原理const thisconst成员函数的本质是this指针的类型变成了const T*classPerson{public:// 非 const 成员函数 → this 类型是 Person*voidset_age(inta){/* this-age_ a; */}// const 成员函数 → this 类型是 const Person*intage()const{/* this-age_ 只读 */}};3.3 const 重载同名函数的 const 与非 const 版本#includeiostream#includevectorclassContainer{public:// const 版本const 对象调用constintat(size_t i)const{std::coutconst at()\n;returndata_[i];}// 非 const 版本非 const 对象调用intat(size_t i){std::coutnon-const at()\n;returndata_[i];}private:std::vectorintdata_{1,2,3,4,5};};intmain(){Container c;c.at(0)10;// 调用非 const 版本返回 intconstContainerrefc;intvref.at(1);// 调用 const 版本返回 const int// ref.at(1) 20; // ❌ const int 不可修改}这是 C 标准库的惯用法——std::vector::operator[]和std::vector::at()都有 const 和非 const 两个重载。四、mutableconst 成员函数的后门有时候const 成员函数在逻辑上不改变对象状态但需要修改某些辅助数据如缓存、互斥锁#includestring#includemutexclassConfig{public:std::stringget_value(conststd::stringkey)const{std::lock_guardstd::mutexlock(mutex_);// 需要加锁——但锁是 mutable// 从缓存查找...returncache_[key];}private:mutablestd::mutex mutex_;// 即使在 const 函数中也能修改mutablestd::mapstd::string,std::stringcache_;// 缓存也是 mutable};mutable的含义“这个成员不影响对象的逻辑 const 性”。互斥锁、缓存、引用计数是典型的使用场景。⚠️使用原则mutable是给逻辑上不改状态、物理上需要改的场景用的不是给我想在 const 函数里偷改数据用的。滥用mutable等于取消了 const 的保护。五、const 的指针层级顶层 const 与底层 constC 程序员对const int *p和int * const p的区别可能已经熟悉但 C 中有更系统的理解框架intx10;constint*p1x;// 底层 const指向的内容不可改intconst*p2x;// 同上两种写法等价int*constp3x;// 顶层 const指针本身不可改constint*constp4x;// 双重 const指针和内容都不可改写法含义p 本身可改*p 可改int *p普通指针✅✅const int *p指向 const 的指针✅❌int * const pconst 指针❌✅const int * const pconst 指针指向 const❌❌一眼看懂的方法从右往左读。const int * p→ p is a pointer to int that is constint * const p→ p is a const pointer to int六、const_cast强制去除 const 的利刃6.1 唯一的正当用途兼容遗留 C API// C 的库函数签名// void legacy_log(char *msg); // 不会修改 msg但没写 constvoidsafe_log(constchar*msg){legacy_log(const_castchar*(msg));// 安全legacy_log 实际上不修改 msg}const_cast是 C 中唯一能改变对象 const 性的转型操作。static_cast、reinterpret_cast、dynamic_cast都不能去掉 const。6.2 危险用法修改原本 const 的对象constintx42;intrefconst_castint(x);ref100;// 未定义行为x 是真正的 const可能被放在只读内存区如果对象本身是const强制去掉 const 再修改是未定义行为。只有原始对象不是 const 时const_cast去 const 再修改才是安全的inty42;// y 本身不是 constconstintcrefy;// 只是通过 const 引用访问intref2const_castint(cref);ref2100;// ✅ 安全y 本身不是 const经验法则代码中出现const_cast时停下来问自己——是不是接口设计有问题除了兼容 C API大部分const_cast都意味着设计缺陷。七、volatile不是多线程关键字7.1 volatile 的真正用途volatile告诉编译器“这个变量的值可能在你不知道的时候被改变每次访问都必须从内存读取不允许优化掉”。它的三个正当用途用途一内存映射 I/OMMIO// 嵌入式/驱动开发硬件寄存器映射到特定内存地址volatileuint32_t*conststatus_regreinterpret_castvolatileuint32_t*(0x40021000);voidwait_ready(){while(!(*status_reg0x01)){// 每次循环都从内存重新读取// 不加 volatile编译器可能把 *status_reg 提到循环外——死循环}}用途二信号处理函数中的标志位#includecsignal#includeatomicvolatilesig_atomic_t signaled0;// 信号处理函数中安全使用的类型voidhandler(intsig){signaled1;// 异步信号处理函数可以安全写入}intmain(){signal(SIGINT,handler);while(!signaled){// 每次循环都读内存// 正常工作...}}用途三跨setjmp/longjmp的变量保护#includecsetjmpvolatileintprogress0;// longjmp 后需要保持正确的值voidrisky_work(jmp_buf env){progress1;// ... 可能 longjmp 回去progress2;}7.2 volatile 不能用于多线程同步这是最常见的误解// ❌ 错误用 volatile 做多线程同步——不保证原子性不保证内存顺序volatileintshared_flag0;// 线程 Ashared_flag1;// 不是原子操作线程 B 可能读到中间状态// 线程 Bwhile(shared_flag0){}// volatile 不保证线程 B 能看到线程 A 的写入多线程同步的正确工具是std::atomic#includeatomic// ✅ 正确用 std::atomicstd::atomicintshared_flag{0};// 线程 Ashared_flag.store(1,std::memory_order_release);// 线程 Bwhile(shared_flag.load(std::memory_order_acquire)0){}特性volatilestd::atomic防止编译器优化掉访问✅✅保证原子性❌✅保证内存顺序❌✅防止 CPU 重排❌✅适合多线程同步❌✅适合 MMIO/信号处理✅❌八、const 正确性C 程序员的肌肉记忆8.1 三条黄金法则法则一参数能传 const 引用就传 const 引用// ❌ 不好voidprocess(std::string name,std::vectorintdata){// 拷贝两个大对象// ✅ 好voidprocess(conststd::stringname,conststd::vectorintdata){// 不拷贝法则二成员函数不修改数据就标 const// ❌ 不好——调用者不知道这个函数是否修改对象classAccount{public:doublebalance(){returnbalance_;}// 应该标 const};// ✅ 好——接口自文档化classAccount{public:doublebalance()const{returnbalance_;}// 明确承诺读取操作};法则三变量声明时能不写成可变的就先写 const// ❌intthresholdcompute_threshold();if(threshold100){/* ... */}// ✅ 先写 const需要改时再去掉constintthresholdcompute_threshold();if(threshold100){/* ... */}这个习惯来自一种约束哲学——**“先问能不能是 const”**比等报错了再加 const更可靠。8.2 const 帮助编译器帮你找 bugclassMatrix{public:Matrix(introws,intcols):rows_(rows),cols_(cols),data_(rows*cols){}doubleat(intr,intc){returndata_[r*cols_c];}introws()const{returnrows_;}intcols()const{returncols_;}private:introws_,cols_;std::vectordoubledata_;};// 如果函数不该修改参数就标 const voidprint_matrix(constMatrixm){for(inti0;im.rows();i){for(intj0;jm.cols();j){// m.at(i, j) 0; // ❌ 编译错误非 const 函数不能通过 const 引用调用std::coutm.at(i,j) ;// ❌ 等等... at() 不是 const}}}上面的代码暴露了一个问题Matrix::at()不应该是非 const 的。正确的设计是像标准库那样提供 const 和非 const 双版本。总结C 中的const远不止标记只读——它是一个贯穿类型系统、编译期计算、成员函数契约的核心机制链接差异C 的const全局变量默认内部链接可以安全放在头文件中C 中不行编译期常量C 的const 常量表达式初始化 编译期常量可用作数组大小和模板参数用constexpr更明确const 成员函数 不改对象状态的契约声明通过const this实现。同名函数可有 const/非 const 重载mutable允许 const 成员函数修改非逻辑状态的成员锁、缓存const_cast的唯一正当用途是兼容遗留 C API修改原本 const 的对象是未定义行为volatile≠ 多线程同步——它用于 MMIO、信号处理、longjmp保护。多线程用std::atomicconst 正确性是一种先于犯错的设计习惯参数传 const 成员函数标 const变量先写 const下一篇我们来谈 C 的引用——它不只是更安全的指针还引出了临时对象生命周期延长、完美转发、右值引用等一系列现代 C 的核心机制。动手练习把之前写的BankAccount类的balance()、owner()等查询函数标上 const写一个constexpr函数计算斐波那契数列在编译期生成前 20 项写两段代码一段用volatile int做线程同步故意错一段用std::atomicint正确用 perf 观察差异找出项目中一个非 const 但不修改数据的成员函数加上 const——观察是否有连锁的编译错误如果有说明 const 正确性在传播