文章目录
- 引言
- [一、`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 从
const到constexpr) - [2.2 constexpr 函数:C++17 vs C++14 vs C++11](#2.2 constexpr 函数:C++17 vs C++14 vs C++11)
- [2.1 从 `const` 到 `constexpr`](#2.1 从
- [三、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 从 const 到 constexpr
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 *p 和 int * 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 constint * 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_cast、reinterpret_cast、dynamic_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 远不止"标记只读"------它是一个贯穿类型系统、编译期计算、成员函数契约的核心机制:
- 链接差异 :C++ 的
const全局变量默认内部链接,可以安全放在头文件中;C 中不行 - 编译期常量 :C++ 的
const+ 常量表达式初始化 = 编译期常量,可用作数组大小和模板参数;用constexpr更明确 - const 成员函数 = "不改对象状态"的契约声明,通过
const this实现。同名函数可有 const/非 const 重载 mutable允许 const 成员函数修改非逻辑状态的成员(锁、缓存)const_cast的唯一正当用途是兼容遗留 C API;修改原本 const 的对象是未定义行为volatile≠ 多线程同步------它用于 MMIO、信号处理、longjmp保护。多线程用std::atomic- const 正确性是一种先于犯错的设计习惯:参数传 const &,成员函数标 const,变量先写 const
下一篇,我们来谈 C++ 的引用------它不只是"更安全的指针",还引出了临时对象生命周期延长、完美转发、右值引用等一系列现代 C++ 的核心机制。
📝 动手练习:
- 把之前写的
BankAccount类的balance()、owner()等查询函数标上 const- 写一个
constexpr函数计算斐波那契数列,在编译期生成前 20 项- 写两段代码:一段用
volatile int做"线程同步"(故意错),一段用std::atomic<int>(正确),用 perf 观察差异- 找出项目中一个非 const 但不修改数据的成员函数,加上 const------观察是否有连锁的编译错误(如果有,说明 const 正确性在传播)