C++中的 const 与 volatile:比C强大十倍

文章目录

  • 引言
  • [一、`const` 在 C 和 C++ 中的第一个区别:链接](#一、const 在 C 和 C++ 中的第一个区别:链接)
    • [1.1 默认作用域不同](#1.1 默认作用域不同)
    • [1.2 C++ 的 `const` 可以作为编译期常量](#1.2 C++ 的 const 可以作为编译期常量)
  • 二、`constexpr`:明确要求编译期计算
    • [2.1 从 `const` 到 `constexpr`](#2.1 从 constconstexpr)
    • [2.2 constexpr 函数:C++17 vs C++14 vs C++11](#2.2 constexpr 函数:C++17 vs C++14 vs C++11)
  • [三、const 成员函数:给函数签上"不动数据"的契约](#三、const 成员函数:给函数签上"不动数据"的契约)
    • [3.1 基本语法](#3.1 基本语法)
    • [3.2 底层原理:`const this`](#3.2 底层原理:const this)
    • [3.3 const 重载:同名函数的 const 与非 const 版本](#3.3 const 重载:同名函数的 const 与非 const 版本)
  • [四、`mutable`:const 成员函数的"后门"](#四、mutable:const 成员函数的"后门")
  • [五、const 的指针层级:顶层 const 与底层 const](#五、const 的指针层级:顶层 const 与底层 const)
  • [六、`const_cast`:强制去除 const 的利刃](#六、const_cast:强制去除 const 的利刃)
    • [6.1 唯一的正当用途:兼容遗留 C API](#6.1 唯一的正当用途:兼容遗留 C API)
    • [6.2 危险用法:修改原本 const 的对象](#6.2 危险用法:修改原本 const 的对象)
  • 七、`volatile`:不是多线程关键字
    • [7.1 volatile 的真正用途](#7.1 volatile 的真正用途)
    • [7.2 volatile 不能用于多线程同步](#7.2 volatile 不能用于多线程同步)
  • [八、const 正确性:C++ 程序员的肌肉记忆](#八、const 正确性:C++ 程序员的肌肉记忆)
    • [8.1 三条黄金法则](#8.1 三条黄金法则)
    • [8.2 const 帮助编译器帮你找 bug](#8.2 const 帮助编译器帮你找 bug)
  • 总结

本系列为《C++深度修炼:基础、STL源码与多线程实战》第8篇

前置条件:理解 C 语言的 const 基本用法,了解 C++ 类与成员函数(第2篇、第4篇)

引言

C 语言里,const 基本上就是个"不能修改"的标注:

c 复制代码
const int max = 100;     // 只读变量
const char *msg = "hi";  // 指向的内容不可改

到了 C++,const 被武装到了牙齿------它不只是"不能改",而是渗透到了类型系统、编译期计算、成员函数契约、甚至优化决策中。C++ 的 const 比 C 强大十倍,不夸张。

volatile 在 C++ 中的角色更加微妙------它常常被误解为"多线程关键字",其实根本不是。本文把这两个看似简单、实则暗藏玄机的关键字讲透。


一、const 在 C 和 C++ 中的第一个区别:链接

1.1 默认作用域不同

c 复制代码
// C 语言中
// file1.c
const int MAX = 100;  // 默认外部链接,其他 .c 文件可以用 extern 访问

// file2.c
extern const int MAX;  // 可以链接到 file1.c 的 MAX
cpp 复制代码
// C++ 中
// file1.cpp
const int MAX = 100;  // 默认内部链接!等价于 C 的 static const int

// file2.cpp
extern const int MAX;  // ❌ 链接错误:找不到 MAX

C++ 中,const 全局变量默认是内部链接 (internal linkage),每个翻译单元有自己的副本。这个看似微小的差异源于一个设计目标:const 变量可以安全地放在头文件里

cpp 复制代码
// constants.h --- 在 C 中这么做会链接报错(多重定义),C++ 中完全合法
#pragma once
const int MAX_PLAYERS = 100;
const double PI = 3.141592653589793;
const char APP_NAME[] = "MyGame";

如果想要 C 那样的外部链接,加 extern

cpp 复制代码
// constants.h
extern const int MAX_PLAYERS;  // 声明:外部链接

// constants.cpp
extern const int MAX_PLAYERS = 100;  // 定义:只有一份

1.2 C++ 的 const 可以作为编译期常量

c 复制代码
// C 语言:const 变量不能用作数组大小(VLA 是另一回事)
const int N = 10;
int arr[N];  // ❌ C89/C90:N 不是编译期常量(C99 VLA 可用,但有条件)
cpp 复制代码
// C++:const 初始化为常量表达式时,它就是编译期常量
const int N = 10;
int arr[N];   // ✅ C++:完全合法,N 是编译期常量
std::array<int, N> arr2;  // ✅ 也可以用于模板参数

二、constexpr:明确要求编译期计算

2.1 从 constconstexpr

const 的含义是"我不会改这个值"------但它的初始化可能在运行期:

cpp 复制代码
int get_size_from_config() { return read_config_file(); }

const int size1 = 100;                   // 编译期常量
const int size2 = get_size_from_config(); // 运行期常量!不能用于数组大小

int arr1[size1];  // ✅
int arr2[size2];  // ❌ size2 不是编译期常量

constexpr 则强制要求在编译期就能算出值

cpp 复制代码
constexpr int size1 = 100;                   // ✅ 编译期
constexpr int size2 = get_size_from_config(); // ❌ 编译错误:函数不是 constexpr

// constexpr 函数也能在编译期执行
constexpr int square(int x) { return x * x; }
constexpr int area = square(10);  // 100,编译期算出
int arr[area];                    // ✅

2.2 constexpr 函数:C++17 vs C++14 vs C++11

cpp 复制代码
// C++11:constexpr 函数基本只能写 return
constexpr int factorial(int n) {
    return n <= 1 ? 1 : n * factorial(n - 1);
}

// C++14:constexpr 函数可以写循环和多语句
constexpr int factorial14(int n) {
    int result = 1;
    for (int i = 2; i <= n; ++i)
        result *= i;
    return result;
}

// C++17:constexpr 可以用于 lambda
auto fact = [](int n) constexpr {
    int r = 1;
    for (int i = 2; i <= n; ++i) r *= i;
    return r;
};
constexpr int f5 = fact(5);  // 120
版本 constexpr 函数能力
C++11 单个 return 语句
C++14 多语句、循环、局部变量
C++17 lambda、更多标准库函数标注 constexpr
C++20 动态分配(new/delete 在编译期)、std::vector 的部分支持

三、const 成员函数:给函数签上"不动数据"的契约

这是 C++ 中 const 最独特、应用最广的能力------C 语言完全没有对应物。

3.1 基本语法

cpp 复制代码
#include <iostream>
#include <string>

class Person {
public:
    Person(const std::string &name, int age) : name_(name), age_(age) {}

    // const 成员函数:承诺不修改对象的数据成员
    std::string name() const { return name_; }
    int age() const { return age_; }

    // 非 const 成员函数:可能修改
    void set_age(int a) { age_ = a; }

private:
    std::string name_;
    int age_;
};

int main() {
    const Person p("张三", 30);  // const 对象!

    std::cout << p.name() << ' ' << p.age() << '\n';  // ✅ const 成员函数可以调用
    // p.set_age(31);  // ❌ 编译错误:const 对象不能调非 const 成员函数
}

3.2 底层原理:const this

const 成员函数的本质是 this 指针的类型变成了 const T*

cpp 复制代码
class Person {
public:
    // 非 const 成员函数 → this 类型是 Person*
    void set_age(int a) { /* this->age_ = a; */ }

    // const 成员函数 → this 类型是 const Person*
    int age() const { /* this->age_  只读 */ }
};

3.3 const 重载:同名函数的 const 与非 const 版本

cpp 复制代码
#include <iostream>
#include <vector>

class Container {
public:
    // const 版本:const 对象调用
    const int& at(size_t i) const {
        std::cout << "const at()\n";
        return data_[i];
    }

    // 非 const 版本:非 const 对象调用
    int& at(size_t i) {
        std::cout << "non-const at()\n";
        return data_[i];
    }

private:
    std::vector<int> data_{1, 2, 3, 4, 5};
};

int main() {
    Container c;
    c.at(0) = 10;  // 调用非 const 版本,返回 int&

    const Container &ref = c;
    int v = ref.at(1);  // 调用 const 版本,返回 const int&
    // ref.at(1) = 20;  // ❌ const int& 不可修改
}

这是 C++ 标准库的惯用法------std::vector::operator[]std::vector::at() 都有 const 和非 const 两个重载。


四、mutable:const 成员函数的"后门"

有时候,const 成员函数在逻辑上不改变对象状态,但需要修改某些辅助数据(如缓存、互斥锁):

cpp 复制代码
#include <string>
#include <mutex>

class Config {
public:
    std::string get_value(const std::string &key) const {
        std::lock_guard<std::mutex> lock(mutex_);  // 需要加锁------但锁是 mutable
        // 从缓存查找...
        return cache_[key];
    }

private:
    mutable std::mutex mutex_;                      // 即使在 const 函数中也能修改
    mutable std::map<std::string, std::string> cache_; // 缓存也是 mutable
};

mutable 的含义:"这个成员不影响对象的逻辑 const 性"。互斥锁、缓存、引用计数是典型的使用场景。

⚠️ 使用原则mutable 是给"逻辑上不改状态、物理上需要改"的场景用的,不是给"我想在 const 函数里偷改数据"用的。滥用 mutable 等于取消了 const 的保护。


五、const 的指针层级:顶层 const 与底层 const

C 程序员对 const int *pint * const p 的区别可能已经熟悉,但 C++ 中有更系统的理解框架:

cpp 复制代码
int x = 10;

const int *p1 = &x;     // 底层 const:指向的内容不可改
int const *p2 = &x;     // 同上(两种写法等价)
int * const p3 = &x;    // 顶层 const:指针本身不可改
const int * const p4 = &x;  // 双重 const:指针和内容都不可改
写法 含义 p 本身可改? *p 可改?
int *p 普通指针
const int *p 指向 const 的指针
int * const p const 指针
const int * const p const 指针指向 const

一眼看懂的方法:从右往左读

  • const int * p → p is a pointer to int that is const
  • int * const p → p is a const pointer to int

六、const_cast:强制去除 const 的利刃

6.1 唯一的正当用途:兼容遗留 C API

cpp 复制代码
// C 的库函数签名:
// void legacy_log(char *msg);  // 不会修改 msg,但没写 const

void safe_log(const char *msg) {
    legacy_log(const_cast<char *>(msg));  // 安全:legacy_log 实际上不修改 msg
}

const_cast 是 C++ 中唯一能改变对象 const 性的转型操作。static_castreinterpret_castdynamic_cast 都不能去掉 const。

6.2 危险用法:修改原本 const 的对象

cpp 复制代码
const int x = 42;
int &ref = const_cast<int &>(x);
ref = 100;  // 未定义行为!x 是真正的 const,可能被放在只读内存区

如果对象本身是 const,强制去掉 const 再修改是未定义行为 。只有原始对象不是 const 时,const_cast 去 const 再修改才是安全的:

cpp 复制代码
int y = 42;          // y 本身不是 const
const int &cref = y;  // 只是通过 const 引用访问
int &ref2 = const_cast<int &>(cref);
ref2 = 100;           // ✅ 安全:y 本身不是 const

💡 经验法则 :代码中出现 const_cast 时,停下来问自己------是不是接口设计有问题?除了兼容 C API,大部分 const_cast 都意味着设计缺陷。


七、volatile:不是多线程关键字

7.1 volatile 的真正用途

volatile 告诉编译器:"这个变量的值可能在你不知道的时候被改变,每次访问都必须从内存读取,不允许优化掉"

它的三个正当用途:

用途一:内存映射 I/O(MMIO)

cpp 复制代码
// 嵌入式/驱动开发:硬件寄存器映射到特定内存地址
volatile uint32_t *const status_reg = reinterpret_cast<volatile uint32_t *>(0x40021000);

void wait_ready() {
    while (!(*status_reg & 0x01)) {  // 每次循环都从内存重新读取
        // 不加 volatile,编译器可能把 *status_reg 提到循环外------死循环!
    }
}

用途二:信号处理函数中的标志位

cpp 复制代码
#include <csignal>
#include <atomic>

volatile sig_atomic_t signaled = 0;  // 信号处理函数中安全使用的类型

void handler(int sig) {
    signaled = 1;  // 异步信号处理函数可以安全写入
}

int main() {
    signal(SIGINT, handler);
    while (!signaled) {  // 每次循环都读内存
        // 正常工作...
    }
}

用途三:跨 setjmp/longjmp 的变量保护

cpp 复制代码
#include <csetjmp>

volatile int progress = 0;  // longjmp 后需要保持正确的值

void risky_work(jmp_buf env) {
    progress = 1;
    // ... 可能 longjmp 回去
    progress = 2;
}

7.2 volatile 不能用于多线程同步

这是最常见的误解:

cpp 复制代码
// ❌ 错误:用 volatile 做多线程同步------不保证原子性,不保证内存顺序
volatile int shared_flag = 0;

// 线程 A
shared_flag = 1;  // 不是原子操作!线程 B 可能读到中间状态

// 线程 B
while (shared_flag == 0) {}  // volatile 不保证线程 B 能看到线程 A 的写入

多线程同步的正确工具是 std::atomic

cpp 复制代码
#include <atomic>

// ✅ 正确:用 std::atomic
std::atomic<int> shared_flag{0};

// 线程 A
shared_flag.store(1, std::memory_order_release);

// 线程 B
while (shared_flag.load(std::memory_order_acquire) == 0) {}
特性 volatile std::atomic
防止编译器优化掉访问
保证原子性
保证内存顺序
防止 CPU 重排
适合多线程同步
适合 MMIO/信号处理

八、const 正确性:C++ 程序员的肌肉记忆

8.1 三条黄金法则

法则一:参数能传 const 引用就传 const 引用

cpp 复制代码
// ❌ 不好
void process(std::string name, std::vector<int> data) {  // 拷贝两个大对象
// ✅ 好
void process(const std::string &name, const std::vector<int> &data) {  // 不拷贝

法则二:成员函数不修改数据就标 const

cpp 复制代码
// ❌ 不好------调用者不知道这个函数是否修改对象
class Account {
public:
    double balance() { return balance_; }  // 应该标 const
};

// ✅ 好------接口自文档化
class Account {
public:
    double balance() const { return balance_; }  // 明确承诺:读取操作
};

法则三:变量声明时能不写成可变的就先写 const

cpp 复制代码
// ❌
int threshold = compute_threshold();
if (threshold > 100) { /* ... */ }

// ✅ 先写 const,需要改时再去掉
const int threshold = compute_threshold();
if (threshold > 100) { /* ... */ }

这个习惯来自一种约束哲学------**"先问能不能是 const"**比"等报错了再加 const"更可靠。

8.2 const 帮助编译器帮你找 bug

cpp 复制代码
class Matrix {
public:
    Matrix(int rows, int cols) : rows_(rows), cols_(cols), data_(rows * cols) {}

    double& at(int r, int c) { return data_[r * cols_ + c]; }

    int rows() const { return rows_; }
    int cols() const { return cols_; }

private:
    int rows_, cols_;
    std::vector<double> data_;
};

// 如果函数不该修改参数,就标 const &
void print_matrix(const Matrix &m) {
    for (int i = 0; i < m.rows(); ++i) {
        for (int j = 0; j < m.cols(); ++j) {
            // m.at(i, j) = 0;  // ❌ 编译错误:非 const 函数不能通过 const 引用调用
            std::cout << m.at(i, j) << ' ';  // ❌ 等等... at() 不是 const!
        }
    }
}

上面的代码暴露了一个问题:Matrix::at() 不应该是非 const 的。正确的设计是像标准库那样提供 const 和非 const 双版本。


总结

C++ 中的 const 远不止"标记只读"------它是一个贯穿类型系统、编译期计算、成员函数契约的核心机制:

  1. 链接差异 :C++ 的 const 全局变量默认内部链接,可以安全放在头文件中;C 中不行
  2. 编译期常量 :C++ 的 const + 常量表达式初始化 = 编译期常量,可用作数组大小和模板参数;用 constexpr 更明确
  3. const 成员函数 = "不改对象状态"的契约声明,通过 const this 实现。同名函数可有 const/非 const 重载
  4. mutable 允许 const 成员函数修改非逻辑状态的成员(锁、缓存)
  5. const_cast 的唯一正当用途是兼容遗留 C API;修改原本 const 的对象是未定义行为
  6. volatile ≠ 多线程同步------它用于 MMIO、信号处理、longjmp 保护。多线程用 std::atomic
  7. const 正确性是一种先于犯错的设计习惯:参数传 const &,成员函数标 const,变量先写 const

下一篇,我们来谈 C++ 的引用------它不只是"更安全的指针",还引出了临时对象生命周期延长、完美转发、右值引用等一系列现代 C++ 的核心机制。


📝 动手练习

  1. 把之前写的 BankAccount 类的 balance()owner() 等查询函数标上 const
  2. 写一个 constexpr 函数计算斐波那契数列,在编译期生成前 20 项
  3. 写两段代码:一段用 volatile int 做"线程同步"(故意错),一段用 std::atomic<int>(正确),用 perf 观察差异
  4. 找出项目中一个非 const 但不修改数据的成员函数,加上 const------观察是否有连锁的编译错误(如果有,说明 const 正确性在传播)
相关推荐
lihao lihao3 小时前
MFC知识点
c++·mfc
Shadow(⊙o⊙)3 小时前
进程分析2.0——进程退出、进程等待-Linux重要经典模块
linux·运维·服务器·开发语言·c++·学习
奔跑的Ma~3 小时前
第6篇:蓝桥杯C++进阶突破(难题拆解+算法优化,冲刺国赛高奖)
c++·算法·蓝桥杯·#蓝桥杯备战·#c++编程·编程竞赛
草莓熊Lotso4 小时前
【Linux系统加餐】从原理到实战:System V消息队列全解析 + 基于责任链模式的工业级封装
linux·运维·服务器·c语言·c++·人工智能·责任链模式
rGzywSmDg4 小时前
如何在Dev-C++中配置TDM-GCC编译器
开发语言·c++·算法
邪修king4 小时前
C++ 二叉搜索树 (BST) 超全详解:核心原理、完整实现、性能分析与使用场景
数据结构·c++·bst·二叉树搜索树
诙_4 小时前
C++数据结构学习总结
数据结构·c++·学习
芜湖_4 小时前
LeetCode Hot 100 01 - 哈希
c++·算法·leetcode·哈希算法
浅念-4 小时前
LeetCode回溯算法从入门到精通完整解析
开发语言·数据结构·c++·算法·leetcode·dfs·深度优先遍历