The C++ Programming Language学习:C++ 第11章:选择操作
核心主题:运算符细节、动态内存管理、Lambda 表达式、类型转换、函数的方方面面。
目录
- 第11章:选择操作
- 1.1 逻辑运算符与位运算符
- 1.2 条件表达式与自增自减
- 1.3 自由存储(堆内存)
- 1.4 列表初始化
- 1.5 Lambda 表达式
- 1.6 显式类型转换
- 第12章:函数
- 2.1 函数声明的组成部分
- 2.2 返回值
- 2.3 inline 与 constexpr 函数
- 2.4 参数传递
- 2.5 函数重载
- 2.6 前置条件与后置条件
- 2.7 函数指针
- 2.8 宏
- 建议总结
第11章:选择操作
1.1 逻辑运算符与位运算符
逻辑运算符(&& || !)
核心特性:短路求值(Short-circuit Evaluation)
&&:左边为 false → 右边不执行
||:左边为 true → 右边不执行
#include <iostream>
#include <cctype>
using namespace std;
int main() {
// 短路求值的安全用法:先检查指针是否有效,再解引用
const char* p = "hello";
// p 为 nullptr 时,!whitespace(*p) 不会执行(避免空指针崩溃)
while (p && !isspace(*p)) {
cout << *p;
++p;
}
cout << endl;
// && 保证从左到右求值,|| 同理
int count = 5;
bool result = (count > 0) && (count < 10); // 两个条件都满足
cout << "result: " << result << endl;
return 0;
}
位运算符(& | ^ ~ << >>)
位运算用于操作整数的每一个二进制位,常见于硬件编程、标志位、集合操作。
& → 位与(AND):两位都是1才是1,常用于"交集"和"掩码"
| → 位或(OR): 有一位是1就是1,常用于"合并"标志
^ → 位异或(XOR):相同为0,不同为1,常用于"差异检测"
~ → 位取反(NOT):0变1,1变0
<< → 左移:等于乘以 2 的幂
>> → 右移:等于除以 2 的幂
#include <iostream>
using namespace std;
// 模拟 iostream 的状态标志(位向量)
enum iostate {
goodbit = 0, // 0000:一切正常
eofbit = 1, // 0001:到达文件末尾
failbit = 2, // 0010:操作失败
badbit = 4 // 0100:严重错误
};
int main() {
// 初始状态:正常
int state = goodbit;
// 设置 eofbit(用 |= 添加标志,不会清除其他位)
state |= eofbit;
// state 现在是 0001
// 检查是否处于"不良"状态(failbit 或 badbit)
if (state & (badbit | failbit)) {
cout << "流出现错误" << endl;
} else {
cout << "流状态:" << state << "(只有 eofbit)" << endl;
}
// state = eofbit 会清除其他所有位(危险!)
// state |= eofbit 才是正确的"添加"操作
// 检测两个状态的差异(用 ^ 异或)
int old_state = goodbit;
int new_state = eofbit | failbit;
if (new_state ^ old_state) {
cout << "状态发生了变化" << endl;
}
// 提取整数中间 16 位(位域提取)
// 从 32 位的 int 中提取第 8-23 位
int x = 0xFF00FF00;
// 先右移 8 位,再用掩码 0xFFFF 保留低 16 位
unsigned short middle = (x >> 8) & 0xFFFF;
cout << "中间16位:" << hex << middle << dec << endl; // 0x00FF
// 注意:位运算符 & 和逻辑运算符 && 完全不同!
// !0 = true(逻辑非)
// ~0 = -1(位取反,二进制全1)
cout << "!0 = " << !0 << ", ~0 = " << ~0 << endl;
return 0;
}
1.2 条件表达式与自增自减
条件表达式(三目运算符)?:
#include <iostream>
#include <stdexcept>
using namespace std;
int main() {
int a = 5, b = 3;
// if-else 的简洁替代
int max_val = (a > b) ? a : b; // 如果 a>b 则取 a,否则取 b
cout << "最大值:" << max_val << endl; // 5
// ? 的两个分支可以是不同的表达式,但必须有公共类型
// 其中一个分支可以是 throw 表达式
int* p = nullptr;
// int i = (p) ? *p : throw runtime_error("空指针");
// 如果 p 非空就取 *p,否则抛出异常
// 条件表达式在常量表达式中非常有用(constexpr 场景)
constexpr int x = (3 > 2) ? 10 : 20; // 编译期求值,x=10
cout << "常量表达式结果:" << x << endl;
return 0;
}
自增自减运算符 ++ --
前缀 vs 后缀的核心区别:
++x(前缀):先加1,返回新值
x++(后缀):先返回旧值,再加1(需要临时存储旧值,稍慢)
#include <iostream>
#include <cstring>
using namespace std;
int main() {
int x = 5;
// 前缀:返回新值
int a = ++x; // 等价于:x = x+1; a = x;
cout << "a=" << a << " x=" << x << endl; // a=6, x=6
// 后缀:返回旧值
x = 5;
int b = x++; // 等价于:t=x; x=x+1; b=t;
cout << "b=" << b << " x=" << x << endl; // b=5, x=6
// 指针上的 ++ 移动到下一个元素
int arr[] = {10, 20, 30};
int* ptr = arr;
cout << *ptr++ << endl; // 先取 *ptr(10),再移到下一元素
cout << *ptr << endl; // 现在指向 20
return 0;
}
// C 风格字符串复制的演化过程(教学示例)
void cpy_v1(char* p, const char* q) {
// 版本1:先算长度再复制(读了两次)
int len = strlen(q);
for (int i = 0; i <= len; i++) {
p[i] = q[i];
}
}
void cpy_v2(char* p, const char* q) {
// 版本2:一次遍历,但要单独处理终止符
while (*q != 0) {
*p = *q;
p++;
q++;
}
*p = 0; // 手动添加终止符
}
void cpy_v3(char* p, const char* q) {
// 版本3:利用后缀++,先用值再移指针
while (*q != 0) {
*p++ = *q++; // 先赋值,再同时移动两个指针
}
*p = 0;
}
void cpy_final(char* p, const char* q) {
// 最终版本:赋值后检查是否是 '\0'
// *q++ 的值是赋值前的 *q,即复制过来的字符
// 当复制 '\0' 后,条件为假,循环退出('\0'已被复制)
while (*p++ = *q++) ;
// 这一行完成了:复制 + 移动指针 + 检查终止符 三件事
}
理解 while (*p++ = *q++):
每次循环:
1. 把 *q 的值赋给 *p
2. p 和 q 各自 +1(指向下一个字符)
3. 整个赋值表达式的值就是被赋的值(即 *q 的原值)
4. 当 *q 是 '\0' 时,赋值后条件为 '\0'(即 0=false),循环结束
5. 此时 '\0' 已经被复制过去了,不需要额外处理
1.3 自由存储(堆内存)
栈(Stack)vs 堆(Heap):
栈变量:声明时自动创建,超出作用域自动销毁
int x = 5; ← 在栈上
堆变量:用 new 手动创建,用 delete 手动销毁
int* p = new int{5}; ← 在堆上
delete p; ← 手动释放
三大内存问题
#include <iostream>
#include <memory> // unique_ptr
#include <vector>
#include <string>
using namespace std;
// 示例1:内存泄漏和悬空指针(危险代码,仅作演示)
void dangerous_example() {
int* p1 = new int{99};
int* p2 = p1; // p2 指向同一块内存
delete p1; // 释放内存
p1 = nullptr; // p1 清空(但这给人假安全感)
// p2 现在是"悬空指针"——它指向已释放的内存!
// *p2 = 999; // 危险!写入已释放的内存,可能破坏其他数据
// 新分配的内存可能恰好占用了 p2 原来指向的地方
// char* p3 = new char{'x'};
// cout << *p3; // 可能不输出 'x'
}
// 示例2:重复释放(同样危险)
void double_delete_danger() {
int* p = new int[1000];
delete[] p; // 第一次释放:正确
// delete[] p; // 第二次释放:未定义行为,通常崩溃!
}
// 示例3:正确做法 - 使用 unique_ptr(RAII 原则)
void safe_example(int n) {
// 不安全:如果中途抛出异常,p1 的内存会泄漏
int* p1 = new int[n]; // 潜在危险
// 安全:unique_ptr 在离开作用域时自动 delete[]
unique_ptr<int[]> p2{new int[n]};
if (n % 2) {
throw runtime_error("奇数!");
// p1 的内存泄漏了,但 p2 会被自动释放
}
delete[] p1; // 如果上面抛异常,这行永远不会执行
}
// 示例4:正确使用 new/delete 的基本规则
void basic_new_delete() {
// 单个对象
int* p = new int{42}; // 分配并初始化为 42
cout << *p << endl; // 42
delete p; // 释放
// 使用 {} 确保初始化(不用 {} 可能未初始化!)
int* q = new int{}; // 初始化为 0
cout << *q << endl; // 0
delete q;
// 数组
char* arr = new char[10];
strcpy(arr, "hello");
cout << arr << endl;
delete[] arr; // 注意:数组必须用 delete[],不能用 delete
// new 失败时默认抛出 bad_alloc 异常
// nothrow 版本:失败时返回 nullptr
int* r = new(nothrow) int[1000000000];
if (r == nullptr) {
cout << "内存分配失败" << endl;
} else {
delete[] r;
}
}
// 示例5:placement new(在指定地址构造对象)
void placement_new_example() {
// 预先分配好的缓冲区
alignas(int) char buffer[sizeof(int)];
// 在 buffer 这块内存上构造 int 对象(不分配新内存)
int* p = new(buffer) int{99};
cout << *p << endl; // 99
// 不需要 delete,因为内存不是 new 分配的
// 但如果是类对象,需要显式调用析构函数:p->~T();
}
int main() {
basic_new_delete();
placement_new_example();
// 推荐:用 vector 和 string 代替裸 new/delete
vector<char> v;
string s = "hello";
// vector 和 string 内部自己管理内存,不会泄漏
return 0;
}
RAII 原则(资源获取即初始化):
核心思想:把资源的生命周期绑定到对象的生命周期
构造函数获取资源(new)
析构函数释放资源(delete)
对象超出作用域时,析构函数自动调用
典型例子:
vector<int> v; // v 管理内存,出作用域自动释放
unique_ptr<T> p; // p 管理一个堆对象,出作用域自动 delete
string s; // s 管理字符数组,出作用域自动释放
1.4 列表初始化 {}
{} 列表有两种形式:
T{...} → 带类型的(Qualified):明确说明要构造什么类型
{...} → 不带类型的(Unqualified):类型从上下文推断
#include <iostream>
#include <vector>
#include <string>
using namespace std;
struct S { int a; string s; };
void f(S x) {
cout << x.a << " " << x.s << endl;
}
int high_value(initializer_list<int> val) {
// initializer_list:处理任意数量的同类型参数
int high = INT_MIN;
for (auto x : val) {
if (x > high) high = x;
}
return high;
}
int main() {
// 带类型的列表(Qualified)
S v{7, "hello"}; // 直接初始化变量
v = S{7, "world"}; // 赋值时构造临时对象
S* p = new S{7, "heap"}; // 在堆上构造
delete p;
// 不带类型的列表(Unqualified)——类型从上下文推断
f({1, "MKS"}); // 等价于 f(S{1, "MKS"})
// f({1, 2}); // 如果有两个 f 的重载可能歧义,需要 S{1,2}
// vector 的 initializer_list 构造函数
vector<double> vd = {1, 2, 3.14};
// 等价于:
// const double temp[] = {1.0, 2.0, 3.14};
// initializer_list<double> tmp(temp, 3);
// vector<double> vd(tmp);
// initializer_list 是不可变的(immutable)
initializer_list<int> lst{1, 2, 3};
// *lst.begin() = 2; // 错误!不能修改
// 直接用 initializer_list 参数
int v1 = high_value({1, 2, 3, 4, 5, 6, 7});
cout << "最大值:" << v1 << endl; // 7
// auto 推导 initializer_list
auto x1 = {1}; // initializer_list<int>
auto x2 = {1, 2}; // initializer_list<int>
// auto x4 = {1, 2.0}; // 错误:类型不一致
return 0;
}
1.5 Lambda 表达式
Lambda 的本质:一个匿名的、可以捕获局部变量的函数对象(闭包)。
Lambda 的结构
[捕获列表](参数列表) mutable noexcept -> 返回类型 { 函数体 }
↑ ↑ ↑ ↑ ↑ ↑
必须有 可省略 可省略 可省略 可省略 必须有
(最小 lambda:[]{})
Lambda 的底层实现模型
Lambda 实际上是编译器自动生成的一个类(函数对象):
#include <iostream>
#include <vector>
#include <algorithm>
#include <functional>
using namespace std;
// ========== Lambda 和它等价的函数对象对比 ==========
// Lambda 版本
void lambda_version(const vector<int>& v, ostream& os, int m) {
for_each(begin(v), end(v),
[&os, m](int x) { // 捕获 os 的引用,m 的值
if (x % m == 0) os << x << '\n';
}
);
}
// 等价的函数对象版本(编译器内部生成的大概样子)
class Modulo_print {
ostream& os; // 引用捕获 → 成员引用
int m; // 值捕获 → 成员变量(拷贝)
public:
Modulo_print(ostream& s, int mm) : os(s), m(mm) {}
// operator() 默认是 const(不修改捕获的值)
void operator()(int x) const {
if (x % m == 0) os << x << '\n';
}
};
// ========== 捕获方式演示 ==========
int main() {
vector<int> v = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// 1. 无捕获:不使用任何局部变量
sort(v.begin(), v.end(),
[](int x, int y) { return x > y; } // 降序
);
// 2. 捕获局部变量(按值)
bool sensitive = true;
sort(v.begin(), v.end(),
[sensitive](int x, int y) {
return sensitive ? x < y : abs(x) < abs(y);
}
);
// 3. 按引用捕获(可以修改原变量)
int count = 0;
for_each(v.begin(), v.end(),
[&count](int x) { if (x % 2 == 0) ++count; }
);
cout << "偶数个数:" << count << endl;
// 4. mutable lambda:修改按值捕获的副本
int n = 10;
auto countdown = [n]() mutable {
// 没有 mutable,不能修改 n(的副本)
return --n; // 修改的是 n 的副本,不影响外部的 n
};
cout << countdown() << endl; // 9
cout << countdown() << endl; // 8
cout << n << endl; // 10(外部 n 没变)
// 5. 返回类型推断
auto z1 = [&](int x) -> double { return x * 1.5; }; // 显式指定返回类型
auto z2 = [&](int x) { return x * 1.5; }; // 自动推断为 double
// 6. Lambda 的类型:用 auto 或 std::function 存储
auto simple = [](int x) { return x * 2; }; // auto 最简单
function<int(int)> f = [](int x) { return x * 2; }; // 更灵活,支持递归
// 7. 递归 Lambda(必须用 std::function)
function<int(int)> factorial = [&factorial](int n) -> int {
return n <= 1 ? 1 : n * factorial(n - 1);
};
cout << "5! = " << factorial(5) << endl; // 120
// 8. 没有捕获的 Lambda 可以转换为函数指针
double (*fp)(double) = [](double a) { return a * 2.0; };
cout << fp(3.5) << endl; // 7.0
return 0;
}
Lambda 的生命周期陷阱
#include <iostream>
#include <functional>
using namespace std;
// 危险示例(不要运行):Lambda 捕获了局部变量的引用,但 Lambda 比局部变量活得更长
// struct Menu { ... };
// void setup(Menu& m) {
// int x = 10, y = 20;
// m.add("action", [&]{ use(x, y); }); // 危险!setup返回后 x,y 消失
// }
// 安全做法:按值捕获,复制数据到 Lambda 内部
// void setup_safe(Menu& m) {
// int x = 10, y = 20;
// m.add("action", [=]{ use(x, y); }); // 安全!x,y 被复制到闭包对象里
// }
// 捕获 this(访问成员变量)
class Request {
int value = 42;
public:
function<int()> make_getter() {
// [this] 捕获当前对象的指针,通过 this 访问成员
return [this]() { return value; };
// 注意:[this] 是引用语义,不是拷贝成员!
// 如果对象在 lambda 使用前被销毁,会出问题
}
};
int main() {
Request req;
auto getter = req.make_getter();
cout << getter() << endl; // 42(req 还活着,安全)
return 0;
}
| 捕获方式 | 写法 | 含义 | 适用场景 |
|---|---|---|---|
| 空 | [] |
不捕获任何变量 | 纯计算,无外部依赖 |
| 全部引用 | [&] |
所有局部变量按引用 | Lambda 不超过调用者生命周期 |
| 全部值 | [=] |
所有局部变量按值复制 | Lambda 可能超过调用者生命周期 |
| 指定变量 | [x, &y] |
x 按值,y 按引用 | 精细控制 |
| 成员 | [this] |
访问类成员 | 成员函数中的 lambda |
1.6 显式类型转换
转换方法的安全性从高到低:
#include <iostream>
#include <stdexcept>
#include <cmath>
using namespace std;
// narrow_cast:带检查的窄化转换(书中推荐的工具函数)
template<class Target, class Source>
Target narrow_cast(Source v) {
auto r = static_cast<Target>(v); // 先转换
if (static_cast<Source>(r) != v) { // 再转回来比较
throw runtime_error("narrow_cast<>() 失败:值丢失");
}
return r;
}
int main() {
// 1. T{e} 构造语法:最安全,禁止窄化转换
double d = 3.14;
int i = int{3}; // OK:整数赋整数
// int j = int{d}; // 错误!double→int 是窄化,{} 拒绝
// int k = int{3.0}; // 错误!即使是整数值,也拒绝
int l = static_cast<int>(d); // OK:显式说明要截断
cout << "double{2}/4 = " << double{2}/4 << endl; // 0.5
// 2. static_cast:在相关类型之间转换
double pi = 3.14159;
int pi_int = static_cast<int>(pi); // 截断为 3
cout << "static_cast<int>(3.14159) = " << pi_int << endl;
float f = static_cast<float>(pi); // double → float
long l2 = static_cast<long>(pi_int); // int → long
// 3. const_cast:去掉 const(极少使用,谨慎)
const int cx = 10;
int* px = const_cast<int*>(&cx);
// *px = 20; // 危险!修改原本是 const 的对象,行为未定义
// 4. reinterpret_cast:重新解释位模式(最危险)
// 用于硬件编程、函数指针转换等底层场景
// IO_device* dev = reinterpret_cast<IO_device*>(0xFF00); // 设备地址
// 5. narrow_cast:可检查的窄化
try {
auto c1 = narrow_cast<char>(64); // OK:64 可以放入 char
cout << "char(64) = " << c1 << endl;
auto c2 = narrow_cast<char>(264); // 抛出异常:264 放不进 8 位 char
} catch (const runtime_error& e) {
cout << "捕获异常:" << e.what() << endl;
}
// 6. round + static_cast 正确处理浮点转整数
double x = 7.9;
int truncated = static_cast<int>(x); // 7(直接截断)
int rounded = static_cast<int>(round(x)); // 8(四舍五入)
cout << "截断:" << truncated << ",四舍五入:" << rounded << endl;
return 0;
}
第12章:函数
2.1 函数声明的组成部分
函数声明可以有很多修饰符,整体结构如下:
[[noreturn]] virtual inline constexpr
返回类型 函数名(参数列表) const noexcept override final
// 最简单的函数
int add(int a, int b);
// 带所有常见修饰符的成员函数(大部分时候不会全用)
struct Base {
// [[noreturn]]:函数永不返回
// virtual:可被派生类覆盖
// inline:建议内联
// constexpr:编译期可求值
// const:不修改成员
// noexcept:保证不抛异常
virtual int f(int x) const noexcept;
};
// 后置返回类型语法(在模板中有用)
auto to_string(int a) -> string; // 等价于 string to_string(int a)
// 用于模板:返回类型依赖于参数类型
template<class T, class U>
auto add(T x, U y) -> decltype(x + y);
2.2 返回值
#include <iostream>
#include <string>
using namespace std;
// 基本返回
int factorial(int n) {
return (n > 1) ? n * factorial(n - 1) : 1;
// 函数调用自身 = 递归
}
// 错误示范:返回局部变量的指针或引用(极其危险!)
int* bad_return_ptr() {
int local = 42;
return &local; // 危险!local 在函数返回后消亡,指针变悬空
}
int& bad_return_ref() {
int local = 42;
return local; // 同样危险!
}
// 正确做法:返回值(拷贝或移动)
string make_greeting(const string& name) {
string result = "Hello, " + name + "!";
return result; // 安全:返回字符串的副本(或通过移动语义优化)
}
// void 函数可以 return void 表达式(用于模板)
void g(int* p) { cout << *p << endl; }
void h(int* p) {
return g(p); // 等价于:g(p); return;
// 在模板中,当 T=void 时这个技巧很有用
}
// [[noreturn]]:标记永远不返回的函数
[[noreturn]] void fatal_error(const string& msg) {
cerr << "致命错误:" << msg << endl;
exit(1); // 或 throw、abort 等
}
// 函数退出的五种方式(函数的"出口")
/*
1. return 语句
2. 函数体执行完毕(仅对 void 函数和 main 合法)
3. 抛出未被本地捕获的异常
4. noexcept 函数中有异常抛出(导致 terminate())
5. 调用 exit() 等不返回的系统函数
*/
int main() {
cout << factorial(6) << endl; // 720
cout << make_greeting("World") << endl;
return 0;
}
2.3 inline 与 constexpr 函数
inline 函数
inline 是给编译器的"建议":把函数调用展开为函数体,避免函数调用开销。
// 编译器看到 fac(6) 可能直接算出 720,不产生函数调用
inline int fac(int n) {
return (n < 2) ? 1 : n * fac(n - 1);
}
调用开销(非 inline): 内联后:
push 参数到栈 直接展开代码
call 指令(跳转+保存返回地址) →
pop 结果 无跳转,更快
注意:inline 不保证一定内联,只是建议。
constexpr 函数
constexpr 函数的规则(C++11 时很严格):
- 只能有一条
return语句 - 不能有循环、局部变量
- 不能有副作用(不能修改全局变量)
constexpr 函数 = 纯函数(Pure Function) ≈ 数学函数 \text{constexpr 函数} = \text{纯函数(Pure Function)} \approx \text{数学函数} constexpr 函数=纯函数(Pure Function)≈数学函数
#include <iostream>
#include <array>
using namespace std;
// 编译期阶乘
constexpr int fac(int n) {
return (n > 1) ? n * fac(n - 1) : 1;
}
// 用于数组大小(必须是编译期常量)
char arr[fac(4)]; // char arr[24]; OK,fac(4) 在编译期算好了
// constexpr 函数也可以在运行时调用(此时普通求值)
int runtime_n = 5;
// int x = fac(runtime_n); // OK:运行时求值
// constexpr + 引用(只读引用没问题)
constexpr int ftbl[] {1, 2, 3, 5, 8, 13};
constexpr int fib_from_table(int n) {
return (n < 6) ? ftbl[n] : fib_from_table(n - 1) + fib_from_table(n - 2);
}
// 条件求值:未选择的分支不需要是常量表达式
constexpr int check(int i) {
// 如果 0 <= i < 99,返回 i(编译期求值)
// 否则 throw(运行时行为)
return (0 <= i && i < 99) ? i : throw out_of_range("check 失败");
}
int main() {
constexpr int f6 = fac(6); // 编译期求值:720
constexpr int f9 = fac(9); // 编译期求值:362880
cout << "6! = " << f6 << endl;
cout << "9! = " << f9 << endl;
// fib_from_table 也可以编译期求值
constexpr int fib5 = fib_from_table(5);
cout << "fib(5) = " << fib5 << endl; // 13
int n = 5;
int fn = fac(n); // 运行时求值(n 是变量)
// constexpr int fnn = fac(n); // 错误!n 不是常量
return 0;
}
constexpr 函数的限制总结:
| 允许 | 不允许 |
|---|---|
| 单个 return | void 函数 |
| 递归 | 循环(for/while) |
条件表达式 ?: |
局部变量 |
| 调用其他 constexpr 函数 | 副作用(写全局变量) |
| const 引用参数 | if/switch 语句 |
2.4 参数传递
选择传参方式的规则:
传参策略 = { 按值传递 小对象(int, double, 指针等) const 引用 大对象,只读 非 const 引用 需要修改,明确说明 指针 "无对象"是合法情况(用 nullptr 表示) 右值引用 移动语义和完美转发 \text{传参策略} = \begin{cases} \text{按值传递} & \text{小对象(int, double, 指针等)} \\ \text{const 引用} & \text{大对象,只读} \\ \text{非 const 引用} & \text{需要修改,明确说明} \\ \text{指针} & \text{"无对象"是合法情况(用 nullptr 表示)} \\ \text{右值引用} & \text{移动语义和完美转发} \end{cases} 传参策略=⎩
⎨
⎧按值传递const 引用非 const 引用指针右值引用小对象(int, double, 指针等)大对象,只读需要修改,明确说明"无对象"是合法情况(用 nullptr 表示)移动语义和完美转发
#include <iostream>
#include <vector>
#include <string>
#include <initializer_list>
using namespace std;
// 1. 按值传递(小对象,拷贝一份)
void by_value(int x) {
x = 99; // 不影响调用者
}
// 2. const 引用(大对象,只读,不拷贝)
void by_const_ref(const string& s) {
cout << s << endl;
// s = "改不了"; // 编译错误
}
// 3. 非 const 引用(明确表示会修改)
void by_ref(string& s) {
s += " modified"; // 调用者能看到修改
}
// 4. 指针(可以传 nullptr 表示"没有对象")
void by_pointer(int* p) {
if (p) *p = 42;
// p == nullptr 时不做任何事
}
// 5. 数组参数(退化为指针,失去大小信息!)
void array_arg(int* arr, int size) { // 必须额外传 size
for (int i = 0; i < size; i++) cout << arr[i] << " ";
cout << endl;
}
// 更好:传 vector 或 span,不需要单独传 size
void vector_arg(const vector<int>& v) {
for (auto x : v) cout << x << " ";
cout << endl;
}
// 6. initializer_list(可变数量的同类型参数)
void show(initializer_list<int> nums) {
for (auto n : nums) cout << n << " ";
cout << endl;
}
// 7. 默认参数
void connect(const string& host, int port = 80, bool ssl = false) {
cout << "连接到 " << host << ":" << port
<< (ssl ? "(SSL)" : "") << endl;
}
// 8. 右值引用(用于移动语义)
void process(vector<int>&& v) {
// v 是将亡值,可以"窃取"它的资源
vector<int> local = move(v); // 移动,不拷贝
cout << "处理了 " << local.size() << " 个元素" << endl;
}
int main() {
// 测试各种传参方式
int x = 5;
by_value(x);
cout << "by_value 后 x=" << x << endl; // 还是 5
string s = "hello";
by_ref(s);
cout << s << endl; // "hello modified"
int* p = nullptr;
by_pointer(p); // 安全,不崩溃
by_pointer(&x); // x 变为 42
int arr[] = {1, 2, 3, 4, 5};
array_arg(arr, 5);
vector<int> v = {10, 20, 30};
vector_arg(v);
show({1, 2, 3, 4, 5}); // initializer_list
connect("example.com"); // 使用默认端口 80
connect("example.com", 443, true); // 指定所有参数
process({1, 2, 3, 4, 5}); // 临时 vector 是右值
// const 引用可以接受字面量和临时对象
// 非 const 引用不能接受(避免修改临时对象)
// by_ref("hello"); // 错误!字符串字面量不能绑定到非 const 引用
return 0;
}
2.5 函数重载
重载:同名函数,不同参数类型。编译器根据参数类型自动选择正确的版本。
重载决议的优先级
精确匹配 > 提升(promotion) > 标准转换 > 用户定义转换 > 省略号(...) \text{精确匹配} > \text{提升(promotion)} > \text{标准转换} > \text{用户定义转换} > \text{省略号(...)} 精确匹配>提升(promotion)>标准转换>用户定义转换>省略号(...)
#include <iostream>
#include <string>
#include <complex>
using namespace std;
// 重载示例
void print(int x) { cout << "int: " << x << endl; }
void print(double x) { cout << "double: " << x << endl; }
void print(const char* x) { cout << "C-string: " << x << endl; }
void print(char x) { cout << "char: " << x << endl; }
// pow 函数重载(不同精度/类型)
int pow_int(int base, int exp);
double pow_double(double base, double exp);
// 用重载比用不同名字好:
int pow(int, int);
double pow(double, double);
complex<double> pow(double, complex<double>);
complex<double> pow(complex<double>, int);
int main() {
print('a'); // 精确匹配 print(char)
print(42); // 精确匹配 print(int)
print(3.14); // 精确匹配 print(double)
print("hello"); // 精确匹配 print(const char*)
short s = 10;
print(s); // short → int 提升(integral promotion),调用 print(int)
float f = 1.0f;
print(f); // float → double 提升,调用 print(double)
// 重载和返回类型无关(返回类型不参与重载决议)
// float sqrt(float);
// double sqrt(double);
// 调用时只看参数,不看你想要什么返回类型
// 歧义示例(编译错误)
// print(1L); // long 不能精确匹配,到 int 和 double 的转换代价相同 → 歧义
// 重载的作用域:不同作用域的同名函数不构成重载!
// 内层 f(double) 会隐藏外层的 f(int)
return 0;
}
重载 vs 不重载的对比:
// 不用重载:需要记很多名字
void print_int(int);
void print_char(char);
void print_string(const char*);
// 用重载:统一接口,更直观
void print(int);
void print(char);
void print(const char*);
2.6 前置条件与后置条件
#include <iostream>
#include <stdexcept>
#include <limits>
using namespace std;
/*
* 计算矩形面积
* 前置条件(precondition):len > 0 且 wid > 0
* 后置条件(postcondition):返回值 > 0
* 后置条件:返回值 = len * wid
*/
int area(int len, int wid) {
// 方式1:假设调用者满足前置条件(不检查,最快)
return len * wid;
}
// 方式2:检查并抛出异常(更安全)
int area_checked(int len, int wid) {
if (len <= 0 || wid <= 0) {
throw invalid_argument("长度和宽度必须为正数");
}
// 检查溢出
if (len > numeric_limits<int>::max() / wid) {
throw overflow_error("乘积溢出");
}
return len * wid;
}
// 方式3:使用 assert(调试时检查,发布时关闭)
#include <cassert>
int area_assert(int len, int wid) {
assert(len > 0 && "len 必须为正");
assert(wid > 0 && "wid 必须为正");
return len * wid;
}
int main() {
cout << area(5, 3) << endl; // 15
try {
cout << area_checked(5, 3) << endl; // 15
cout << area_checked(-1, 3) << endl; // 抛异常
} catch (const invalid_argument& e) {
cout << "捕获:" << e.what() << endl;
}
return 0;
}
2.7 函数指针
函数指针:存储函数地址的指针,可以像普通指针一样传递和存储。
函数指针的声明语法:
返回类型 (*指针名)(参数类型列表)
例如:
void (*pf)(string); // 指向"接受 string,返回 void"的函数
int (*cmp)(const void*, const void*); // 比较函数指针
#include <iostream>
#include <vector>
#include <algorithm>
#include <string>
using namespace std;
void error_msg(string s) {
cerr << "错误:" << s << endl;
}
int main() {
// 1. 基本用法
void (*pf)(string); // 声明函数指针
pf = &error_msg; // 取函数地址(& 可省略)
pf = error_msg; // 等价写法
pf("测试错误"); // 通过指针调用
(*pf)("另一种调用"); // 另一种等价写法
// 2. using 简化函数指针类型
using ErrorFunc = void(*)(string);
ErrorFunc ef = error_msg;
ef("简化的调用");
// 3. 函数指针用于回调(以 lambda 为例对比)
vector<int> v = {5, 2, 8, 1, 9, 3};
// 旧式:函数指针比较
bool (*cmp_ptr)(int, int) = [](int a, int b) { return a < b; };
sort(v.begin(), v.end(), cmp_ptr);
// 现代:直接用 lambda(更清晰)
sort(v.begin(), v.end(), [](int a, int b) { return a < b; });
for (auto x : v) cout << x << " ";
cout << endl;
// 4. 函数指针和重载:用返回类型区分
void f_void(int);
int f_int(char);
void (*pf1)(int) = f_void; // 选择 void f(int)
// void (*pf2)(int) = f_int; // 错误:类型不匹配
// 5. 没有捕获的 lambda 可以转换为函数指针
double (*fp)(double) = [](double x) { return x * x; };
cout << "2.0 的平方 = " << fp(2.0) << endl;
// 有捕获的 lambda 不能转为函数指针
double factor = 2.0;
// double (*fp2)(double) = [factor](double x) { return x * factor; }; // 错误!
return 0;
}
// 仅声明,供编译器检查
void f_void(int) {}
int f_int(char) { return 0; }
2.8 宏(Macros)
原则:尽量不用宏! 宏在预处理器阶段展开,不尊重 C++ 的类型系统和作用域规则。
// ============ 危险的宏(不要这样写)============
#define SQUARE(a) a*a // 危险!
// SQUARE(x+2) 展开为 x+2*x+2,不是 (x+2)*(x+2)
#define INCR_xx (xx)++ // 危险!
// 函数参数 xx 和全局变量 xx 重名时会增加错误的那个
// ============ 稍微安全一点的宏(加括号)============
#define MIN(a, b) (((a)<(b))?(a):(b))
// 但仍有副作用问题:
// MIN(x++, y++) 展开后 x++ 或 y++ 可能执行两次!
// ============ 用 C++ 特性替代宏(推荐)============
// 用 constexpr 替代常量宏
// #define PI 3.14159 // 不好
constexpr double PI = 3.14159; // 好
// 用 inline 模板替代函数式宏
// #define MAX(a,b) (a>b?a:b) // 不好
template<class T>
inline const T& max_val(const T& a, const T& b) {
return (a > b) ? a : b; // 好:类型安全
}
// ============ 宏的合理用途 ============
// 1. 条件编译(include guard)
#ifndef MY_HEADER_H
#define MY_HEADER_H
// ... 头文件内容 ...
#endif
// 2. 预定义宏(编译器提供的信息)
void debug_info() {
// __FILE__:当前文件名
// __LINE__:当前行号
// __FUNC__:当前函数名(实现定义)
// __DATE__:编译日期
// __TIME__:编译时间
std::cout << "函数 " << __func__
<< " 在 " << __FILE__
<< " 第 " << __LINE__ << " 行" << std::endl;
}
// 3. 可变参数宏(类似 printf)
#define err_print(...) fprintf(stderr, "错误: " __VA_ARGS__)
宏 vs 现代 C++ 的替代方案:
| 宏的用途 | 现代 C++ 替代 |
|---|---|
#define PI 3.14 常量 |
constexpr double PI = 3.14; |
#define MAX(a,b) 函数 |
template inline 函数 |
#define BEGIN { 语法糖 |
直接用 { |
| 条件特性开关 | if constexpr (C++17) |
| 类型无关代码 | 模板 |
| 代码生成 | 宏仍是少数合理选择 |
| include guard | #pragma once(非标准但广泛支持) |
建议总结
第11章:选择操作
| 编号 | 建议 | 原因 |
|---|---|---|
| 1 | 优先用前缀 ++ 而非后缀 |
后缀需要临时存储旧值,微小开销 |
| 2 | 用资源句柄避免泄漏和重复释放 | RAII 原则 |
| 3 | 不必要时不要用 new(用栈变量) | 栈变量自动管理生命周期 |
| 4 | 避免裸 new/delete | 用 unique_ptr/shared_ptr/vector |
| 5 | 用 RAII 管理资源 | 异常安全的基础 |
| 6 | 操作需要注释时用命名函数对象,不用 lambda | 可读性 |
| 7 | 保持 lambda 短小 | 长 lambda 难读难测 |
| 8 | 小心按引用捕获 | Lambda 可能比调用者活得长 |
| 9 | 让编译器推断 lambda 返回类型 | 简洁 |
| 10 | 构造用 T{e} 语法 |
禁止窄化,最安全 |
| 11 | 避免显式类型转换 | 强转往往是设计问题的信号 |
| 12 | 必须转换时用具名转换(named cast) | 意图明确,易于搜索 |
第12章:函数
| 编号 | 建议 | 原因 |
|---|---|---|
| 1 | 函数只做一件逻辑上的事 | 单一职责,易测试易复用 |
| 2 | 保持函数简短(理想 7 行,最多约 40 行) | 一屏能看完,减少 bug |
| 3 | 不要返回局部变量的指针或引用 | 悬空指针/引用 |
| 4 | 编译期可求值的函数标为 constexpr | 性能 + 正确性 |
| 5 | 永不返回的函数标为 [[noreturn]] |
帮助编译器优化和理解 |
| 6 | 小对象按值传递 | 简单高效 |
| 7 | 大对象按 const 引用传递 | 避免不必要的拷贝 |
| 8 | 结果通过返回值返回,不要用输出参数 | 更清晰 |
| 9 | 需要表示"无对象"时用指针 | nullptr 语义清晰 |
| 10 | 用重载统一不同类型的相似操作 | 比 print_int/print_char 更自然 |
| 11 | 写出前置条件和后置条件 | 文档化假设,减少 bug |
| 12 | 优先用函数对象和 lambda 而非函数指针 | 类型更安全 |
| 13 | 避免宏;必须用时用全大写名字 | 宏破坏类型系统和作用域 |
C++ 第13章·第14章 深度中文解析
原书:《The C++ Programming Language》(Stroustrup)
本文目标:从零开始,把每一个概念讲清楚,代码加详细注释,公式用 . . . ... ... 和 . . . ... ... 标注。
第13章:异常处理(Exception Handling)
13.1 错误处理的整体思路
核心问题
程序运行时难免出错。关键在于:谁发现错误,谁来处理?
发现错误的地方(比如底层库函数)
↓
往往不知道该怎么办(不知道上层业务逻辑)
需要处理错误的地方(比如主程序)
↓
往往不在现场,没法直接发现错误
这就是为什么 C++ 需要一套"把错误从发现点传递到处理点"的机制——异常(Exception)。
13.1.1 异常的基本机制
异常的工作方式:
对应代码:
#include <iostream>
#include <stdexcept>
// 定义一个专用的异常类型(强烈推荐用专门的类,而不是 int、string 等内置类型)
struct Some_error {};
// 被调用函数:发现问题就 throw,自己处理不了
int do_task(bool can_do)
{
if (can_do)
return 42; // 成功:正常返回结果
else
throw Some_error{}; // 失败:抛出异常,不再执行后面的代码
}
// 调用函数:用 try/catch 表示"我准备处理这类异常"
void taskmaster()
{
try {
// try 块里写"正常逻辑"
auto result = do_task(false);
std::cout << "结果:" << result << "\n";
}
catch (Some_error) {
// catch 块里写"出错时的处理逻辑"
std::cout << "do_task 失败了,在这里处理\n";
}
}
int main()
{
taskmaster();
return 0;
}
关键点:
throw把错误"扔"出去catch接住它- 如果没有人接,程序终止
13.1.2 传统错误处理方式及其缺陷
在异常机制出现之前,有四种传统方式,各有严重问题:
| 方式 | 做法 | 缺陷 |
|---|---|---|
| 终止程序 | exit(1) |
太粗暴,库不能随便 exit |
| 返回错误值 | return -1 |
有时没有合法的"错误值";调用方容易忘记检查 |
| 设置全局错误状态 | errno = xxx |
多线程下不安全;容易被忽视 |
| 调用错误处理函数 | on_error() |
问题转移了,本质没解决 |
#include <cmath> // sqrt
#include <cerrno> // errno
#include <cstdio> // printf
// 演示传统方式的问题
void traditional_problems()
{
// 问题1:返回错误值——int 每个值都可能合法,没有"错误专用值"
// int get_next_int(); // 返回什么表示错误?
// 问题2:errno——容易忘记检查,多线程不安全
errno = 0;
double d = std::sqrt(-1.0); // 参数非法
if (errno != 0) {
// 大多数程序员根本不写这个检查!
printf("sqrt 出错了\n");
}
// 问题3:printf 返回负数表示出错,但没人检查
printf("hello"); // 如果输出失败,返回值是负数,但被忽略了
}
13.1.3 "将就着用"的危害
有些程序员遇到错误选择"忽略,继续跑"——这叫 Muddling Through(将就着用)。
后果:
- 错误数据在系统里越传越深
- 最终崩溃的地方离出错的地方很远,极难调试
- 用户得到错误结果,却不知道哪里出了问题
C++ 的异常机制让"未处理的异常"直接终止程序,这比带着错误继续跑更安全。
13.1.4 异常不只是"罕见事件"
很多人误以为"异常"就是"极少发生的事"——错!
“异常"的意思是"某部分程序无法完成它被要求做的事”。
不管发生频率高不低,只要是"局部无法处理、需要向上汇报的失败",就应该用异常。
注意: 异常只处理同步错误(比如数组越界、文件打开失败)。
键盘中断、断电等异步事件需要用信号(signal)等机制,不在此范畴。
13.1.5 不能用异常时的替代方案
有些场景真的不能用异常(比如嵌入式实时系统、老旧代码库)。此时的模拟方案:
#include <utility> // std::pair
#include <cstdlib> // malloc, free
// ---- 方案一:给类加 invalid() 函数,模拟 RAII ----
template <typename T>
class my_vector {
public:
int error_code = 0; // 0 表示成功,非零表示失败
explicit my_vector(int n)
{
if (n <= 0) {
error_code = 1; // 标记失败,但不抛出异常
return;
}
data = static_cast<T*>(malloc(n * sizeof(T)));
if (!data) {
error_code = 2; // 内存分配失败
}
}
~my_vector() { free(data); }
bool invalid() const { return error_code != 0; }
private:
T* data = nullptr;
};
void f(int n)
{
my_vector<int> x(n);
if (x.invalid()) {
// 手动检查并处理错误
return;
}
// 正常使用 x ...
}
// ---- 方案二:返回 pair<值, 错误码>,模拟异常 ----
std::pair<int*, int> make_array(int n)
{
if (n <= 0)
return {nullptr, 1}; // {结果, 错误码}
int* p = static_cast<int*>(malloc(n * sizeof(int)));
if (!p)
return {nullptr, 2};
return {p, 0}; // 成功
}
void g(int n)
{
auto [ptr, err] = make_array(n);
if (err) {
// 处理错误
return;
}
// 使用 ptr ...
free(ptr);
}
int main()
{
f(10);
g(10);
return 0;
}
这些方案比异常笨拙,但在不能用异常的场合是合理的折中。
13.1.6 分层错误处理
错误处理应该分层:低层处理自己能处理的,处理不了的往上抛。
原则:
- 每个函数只处理它能合理处理的错误
- 其余的往上抛
- 不要让每个函数都变成"防火墙"——这样重复检查太多,性能差,代码乱
跨语言边界的转换(C 调 C++,C++ 调 C):
#include <cerrno>
#include <stdexcept>
// 场景:C++ 代码调用 C 函数
void callC()
{
errno = 0;
// 假设 c_function() 是某个 C 库函数
// c_function();
if (errno) {
// 把 errno 错误码转换为 C++ 异常,让上层用 C++ 方式处理
throw std::runtime_error("C函数执行失败");
}
}
// 场景:C 代码调用 C++ 函数(用 extern "C" 暴露接口)
// noexcept 表示这个函数保证不向外抛出异常
extern "C" void call_from_C() noexcept
{
try {
// c_plus_plus_function(); // 调用可能抛异常的 C++ 函数
}
catch (...) {
// 把 C++ 异常转换回 errno,让 C 代码能理解
errno = 1; // 某个表示失败的错误码
}
}
13.1.7 异常与效率
很多人担心异常慢。现实是:
异常的开销 = try块建立 + 栈展开(仅在throw时) \text{异常的开销} = \text{try块建立} + \text{栈展开(仅在throw时)} 异常的开销=try块建立+栈展开(仅在throw时)
- 没有异常被抛出时:现代编译器几乎零开销
- 有异常被抛出时:比普通函数调用慢,但这是错误路径,不在乎速度
对比传统错误处理代码:
#include <cstdlib> // malloc, free
#include <cstring> // 字符串操作
// === 传统方式(不用异常)===
// 代码膨胀严重,每次调用都要检查返回值
bool g_traditional(int);
bool h_traditional(const char*);
char* read_long_string();
bool f_traditional()
{
char* s = read_long_string();
// ...
if (g_traditional(1)) {
if (h_traditional(s)) {
free(s); // 成功路径也要 free
return true;
} else {
free(s); // 失败路径也要 free(容易忘!)
return false;
}
} else {
free(s); // 又一个 free(非常容易忘!)
return false;
}
}
// === 异常方式 ===
// 代码清晰,资源管理交给 RAII(见13.3节)
#include <string>
void g_exception(int); // 失败时 throw
void h_exception(const std::string&); // 失败时 throw
void f_exception()
{
std::string buf; // string 的析构函数自动释放内存
// g 或 h 抛出异常时,buf 会被自动销毁——不会泄漏!
g_exception(1);
h_exception(buf);
}
int main() { return 0; }
结论: 不要过早担心异常的性能,先把代码写正确。
13.2 异常安全保证(Exception Guarantees)
这是本章最重要的概念之一。
三级保证
| 级别 | 名称 | 含义 |
|---|---|---|
| 基本保证 | Basic Guarantee | 即使抛出异常,对象仍处于合法状态,没有资源泄漏 |
| 强保证 | Strong Guarantee | 操作要么完全成功,要么完全没有效果(回滚) |
| 不抛保证 | Nothrow Guarantee | 保证绝对不抛出异常(用 noexcept 声明) |
类不变式(Class Invariant)
不变式是类始终应该满足的条件。比如:
#include <vector>
#include <cassert>
namespace Points {
// 不变式:vx.size() == vy.size()(点的 x 坐标和 y 坐标数量必须相等)
std::vector<int> vx;
std::vector<int> vy;
}
// 正确操作:同时维护两个向量
void add_point(int x, int y)
{
Points::vx.push_back(x); // 如果这里 push_back 成功...
Points::vy.push_back(y); // ...但这里抛异常,不变式被破坏!
// 需要用强保证来防止这种情况
}
什么是"合法状态"
对象在以下时间段内存在:
构造函数完成 ≤ 合法状态 ≤ 析构函数开始 \text{构造函数完成} \leq \text{合法状态} \leq \text{析构函数开始} 构造函数完成≤合法状态≤析构函数开始
13.3 资源管理(Resource Management)
问题:异常导致资源泄漏
#include <cstdio> // FILE, fopen, fclose
#include <stdexcept>
// 危险代码:如果中间抛异常,fclose 永远不会被调用!
void use_file_WRONG(const char* fn)
{
FILE* f = fopen(fn, "r");
// ... 中间某段代码抛出异常 ...
fclose(f); // 如果上面抛异常,这行永远不执行 → 文件描述符泄漏!
}
// 笨方法:用 try/catch 手动清理(繁琐,容易漏)
void use_file_CLUMSY(const char* fn)
{
FILE* f = fopen(fn, "r");
try {
// ... 使用 f ...
}
catch (...) {
fclose(f); // 异常路径:关闭文件
throw; // 重新抛出,让上层继续处理
}
fclose(f); // 正常路径:关闭文件(两处 fclose,容易不一致!)
}
解决方案:RAII(资源获取即初始化)
RAII 的核心思想:
把资源的生命周期绑定到对象的生命周期。
构造函数获取资源,析构函数释放资源。
无论正常退出还是异常退出,析构函数都会被调用。
#include <cstdio>
#include <stdexcept>
#include <string>
// RAII 封装类:用对象管理 FILE*
class File_ptr {
FILE* p;
public:
// 构造函数:打开文件(获取资源)
File_ptr(const char* name, const char* mode)
: p{ fopen(name, mode) }
{
if (p == nullptr)
throw std::runtime_error("无法打开文件"); // 获取失败,抛异常
}
// 支持 string 参数
File_ptr(const std::string& name, const char* mode)
: File_ptr{ name.c_str(), mode } // 委托给上面的构造函数
{}
// 析构函数:关闭文件(释放资源)
// 不管是正常退出还是异常退出,析构函数都会被调用!
~File_ptr()
{
if (p) fclose(p);
}
// 禁止复制(资源不能被多个对象同时持有)
File_ptr(const File_ptr&) = delete;
File_ptr& operator=(const File_ptr&) = delete;
// 隐式转换:让 File_ptr 可以当 FILE* 用
operator FILE*() { return p; }
};
// 优雅的使用方式:不需要任何手动清理
void use_file_RAII(const char* fn)
{
File_ptr f(fn, "r"); // 构造时打开文件
// ... 使用 f(就像使用 FILE* 一样)...
// 函数结束(无论正常还是异常),f 的析构函数自动关闭文件
}
// 多资源的情况:析构顺序与构造顺序相反
#include <mutex>
class Locked_file_handle {
File_ptr p; // 先构造(先获取文件)
std::unique_lock<std::mutex> lck; // 后构造(后获取锁)
public:
Locked_file_handle(const char* file, std::mutex& m)
: p{ file, "rw" }, // 先获取文件
lck{ m } // 再获取锁
{}
// 析构时:先释放锁,再关闭文件(与构造顺序相反)——自动完成!
};
int main()
{
// 演示 RAII:正常使用
try {
use_file_RAII("test.txt");
}
catch (const std::runtime_error& e) {
// 文件打开失败,File_ptr 的析构函数依然安全(p==nullptr时不会fclose)
}
return 0;
}
析构顺序示意:
构造顺序:p → lck
析构顺序:lck → p (反序)
比如:
p 获取文件 ──────────────────────────────→ p 的析构关闭文件
lck 获取锁 ───────────────────────→ lck 的析构释放锁
中间代码(可能抛异常)
13.3.1 Finally 惯用法
有时你需要在函数结束时执行任意代码(不方便写成一个类)。可以用 finally 惯用法:
#include <iostream>
#include <cstdlib> // malloc, free
#include <functional>
// 模板类:在析构时执行任意操作
template<typename F>
struct Final_action {
F clean; // 存储要在退出时执行的操作
explicit Final_action(F f) : clean{ f } {}
// 析构函数:执行清理操作
~Final_action() { clean(); }
// 禁止复制,防止重复执行
Final_action(const Final_action&) = delete;
Final_action& operator=(const Final_action&) = delete;
};
// 辅助函数:自动推导类型,方便使用
template<typename F>
Final_action<F> finally(F f)
{
return Final_action<F>(f);
}
// 演示用法
void test()
{
// 用 C 风格分配内存(正常应该用 unique_ptr,这里演示 finally)
int* p = new int{7};
int* buf = static_cast<int*>(malloc(100 * sizeof(int)));
// 注册退出时要执行的清理操作
// Lambda 捕获 p 和 buf,无论函数如何退出都会执行
auto act1 = finally([&]{
delete p;
free(buf);
std::cout << "act1: 清理完毕\n";
});
int var = 0;
std::cout << "var = " << var << "\n"; // 输出 0
{
// 内层作用域
var = 1;
auto act2 = finally([&]{
std::cout << "act2: finally!\n";
var = 7;
});
std::cout << "var = " << var << "\n"; // 输出 1
} // act2 在这里析构,输出 "finally!",var 变为 7
std::cout << "var = " << var << "\n"; // 输出 7
} // act1 在这里析构,输出清理信息,释放 p 和 buf
int main()
{
test();
return 0;
}
运行输出:
var = 0
var = 1
act2: finally!
var = 7
act1: 清理完毕
注意:
finally比 RAII 类更临时、更 ad hoc。
如果一段资源逻辑会重复出现,最好还是写成专门的 RAII 类。
13.4 强制不变式(Enforcing Invariants)
断言(Assertion)
当函数的前提条件(precondition)不满足时,有三种策略:
| 策略 | 做法 | 适用场景 |
|---|---|---|
| 忽略(Just don’t do that) | 让调用方负责 | 高性能场合,调试期 |
| 终止程序 | std::terminate() |
前提条件违反是设计错误 |
| 抛出异常 | throw ... |
大多数库代码 |
标准断言工具
#include <cassert> // assert 宏
#include <stdexcept>
// 1. assert(condition):调试时检查,发布时可关闭(定义 NDEBUG 宏)
void f_assert(int n)
{
assert(n > 0); // 如果 n<=0 且未定义 NDEBUG,程序终止并打印出错信息
// ...
}
// 2. static_assert:编译时检查
static_assert(sizeof(int) >= 4, "int 必须至少 4 字节");
// 3. 自定义:运行时抛异常
void f_throw(int n, int max_val)
{
if (n <= 0 || n > max_val)
throw std::out_of_range("n 超出范围");
// ...
}
int main()
{
f_throw(5, 100);
return 0;
}
assert 与异常的选择原则:
是否可以恢复?
是 → throw 异常(让上层处理)
否 → assert / terminate(快速失败,便于调试)
是否是调试期的合理性检查?
是 → assert(发布时可关闭)
否 → 运行时检查(不能关闭)
13.5 抛出与捕获异常
13.5.1 抛出异常(throw)
#include <stdexcept>
#include <string>
// 推荐:定义专用异常类
struct Range_error {
int value;
explicit Range_error(int v) : value{v} {}
};
// 也可以继承标准库异常
struct My_error : public std::runtime_error {
explicit My_error(const std::string& msg)
: std::runtime_error(msg) {}
// what() 返回错误描述字符串
const char* what() const noexcept override
{
return std::runtime_error::what();
}
};
void f(int n, int max_val)
{
if (n < 0 || n > max_val)
throw Range_error{n}; // 抛出时创建异常对象(会被复制)
// ...
}
// 栈展开(Stack Unwinding)演示
// 异常被抛出后,沿调用栈向上传播,沿途析构所有局部对象
#include <iostream>
struct Tracker {
std::string name;
explicit Tracker(std::string n) : name{n} {
std::cout << "构造: " << name << "\n";
}
~Tracker() {
std::cout << "析构: " << name << "\n"; // 抛出异常时也会被调用
}
};
void h_demo()
{
Tracker t1{"h中的t1"};
throw My_error{"h 函数出错了"}; // 抛出异常
Tracker t2{"h中的t2"}; // 这行永远不会执行
}
void g_demo()
{
Tracker t{"g中的t"};
h_demo(); // h 抛出异常,g 的 t 也会被析构
}
int main()
{
try {
g_demo();
}
catch (const My_error& e) {
std::cout << "捕获异常: " << e.what() << "\n";
}
// 析构顺序(逆构造顺序):先析构 g 中的 t,再析构 h 中的 t1
return 0;
}
13.5.1.1 noexcept 说明符
#include <vector>
#include <string>
// 声明函数不会抛出异常
// 如果函数内部确实抛出了异常:程序直接 terminate(),不继续传播
double compute(double x) noexcept
{
return x * x; // 简单计算,确实不会抛
}
// 危险:声明了 noexcept,但内部可能抛
double risky(double x) noexcept
{
std::vector<double> tmp(10); // vector 构造可能 throw bad_alloc!
// 如果抛异常,不是继续传播,而是直接 terminate()
return x;
}
// 条件 noexcept:根据类型特征决定是否 noexcept
template<typename T>
void my_fct(T& x) noexcept(std::is_trivially_copyable_v<T>)
{
// 如果 T 是可平凡复制的(POD 类型),就保证 noexcept
// 否则可能抛出
}
int main()
{
double d = 3.14;
compute(d);
my_fct(d); // double 是 POD,noexcept 生效
std::string s = "hello";
my_fct(s); // string 不是 POD,可能抛出
return 0;
}
为什么要声明 noexcept?
- 编译器优化:不需要生成异常处理代码,更快
- 语义清晰:告诉调用方"放心,不会抛"
- 某些标准库操作(如
std::vector移动元素时)只在移动构造函数是 noexcept 时才会移动(否则复制),影响性能
13.5.2 捕获异常(catch)
捕获规则
异常 E 被 catch(H) 捕获的条件:
H = E 或 H 是 E 的公有基类 或 H 是引用/指针且满足前两条 H = E \quad \text{或} \quad H \text{ 是 } E \text{ 的公有基类} \quad \text{或} \quad H \text{ 是引用/指针且满足前两条} H=E或H 是 E 的公有基类或H 是引用/指针且满足前两条
#include <iostream>
#include <stdexcept>
// 演示各种捕获方式
void demo_catch()
{
try {
throw std::out_of_range("索引超出范围");
}
// 按派生类在前、基类在后的顺序排列!
catch (const std::out_of_range& e) {
// 捕获具体类型(优先匹配)
std::cout << "out_of_range: " << e.what() << "\n";
}
catch (const std::exception& e) {
// 捕获基类(如果上面没匹配到)
std::cout << "exception: " << e.what() << "\n";
}
catch (...) {
// 捕获任意类型(最后的保险网)
std::cout << "未知异常\n";
}
}
// 重新抛出(Rethrow)
void partial_handler()
{
try {
throw std::runtime_error("底层错误");
}
catch (std::exception& err) {
std::cout << "部分处理: " << err.what() << "\n";
// 自己处理不完,重新抛出
// 注意:throw; 重抛原始异常(保持类型),不是 throw err;(会切片!)
throw;
}
}
int main()
{
demo_catch();
try {
partial_handler();
}
catch (const std::exception& e) {
std::cout << "最终处理: " << e.what() << "\n";
}
return 0;
}
重要:throw; vs throw err; 的区别
throw; → 重抛原始异常对象,类型不变(比如 out_of_range)
throw err; → 用 err 构造新异常,类型变为 exception(发生"切片",丢失派生类信息)
作用域规则
#include <stdexcept>
void scope_demo()
{
int x1 = 0; // x1 在 try 块外,try 和 catch 都能访问
try {
int x2 = x1; // x2 只在 try 块内有效
// ...
}
catch (std::exception&) {
++x1; // OK:x1 在外面
// ++x2; // 错误:x2 不在 catch 的作用域内
int x3 = 7; // x3 只在这个 catch 块内有效
}
catch (...) {
// ++x3; // 错误:x3 是上一个 catch 块的变量
}
++x1; // OK
// ++x2; // 错误:x2 出了 try 块就不存在了
}
int main()
{
scope_demo();
return 0;
}
构造函数 try 块
#include <vector>
#include <string>
#include <stdexcept>
#include <iostream>
class X {
std::vector<int> vi;
std::vector<std::string> vs;
public:
// 构造函数 try 块:可以捕获成员初始化时抛出的异常
X(int sz1, int sz2)
try // try 关键字放在初始化列表之前
: vi(sz1), // 如果 sz1 很大,可能 throw bad_alloc
vs(sz2) // 如果 sz2 很大,可能 throw bad_alloc
{
// 正常构造逻辑
}
catch (std::exception& err) {
// 注意:在这里不能"修复"对象后正常返回
// 只能重新抛出异常(或者隐式重抛)
std::cout << "构造失败: " << err.what() << "\n";
// 这里默认会重抛原始异常
}
};
int main()
{
try {
X obj(10, 20); // 正常
X obj2(1000000000, 0); // 可能 bad_alloc
}
catch (std::bad_alloc& e) {
std::cout << "内存不足\n";
}
return 0;
}
terminate() 的触发条件
以下情况会直接调用 std::terminate():
1. 没有合适的 catch 块匹配抛出的异常
2. noexcept 函数内部抛出了异常
3. 析构函数在栈展开时又抛出了异常
4. 在没有当前异常时执行了 throw;(重抛)
5. 静态/线程局部对象的构造或析构抛出异常
最佳实践:析构函数绝对不能抛出异常!
13.6 vector 的异常安全实现
这是本章最具体的工程案例,展示如何把上面所有概念运用到实际代码里。
架构设计
vector<T, A>
└── vector_base<T, A> ← 专门管理内存(RAII)
├── elem ─── 元素起始地址
├── space ─── 已构造元素的末尾
└── last ─── 分配内存的末尾
内存布局(capacity = 8,size = 5):
elem space last
| | |
v v v
[e0][e1][e2][e3][e4][ ][ ][ ]
<-- size=5 -->
<---------- capacity=8 ------->
#include <memory> // std::allocator, std::allocator_traits
#include <algorithm> // std::copy
#include <utility> // std::move, std::swap
#include <stdexcept>
#include <iostream>
// ============================================================
// vector_base:专门负责内存管理(RAII)
// 不管理对象(T类型)的构造和析构,只管理原始内存
// ============================================================
template<typename T, typename A = std::allocator<T>>
struct vector_base {
A alloc; // 分配器
T* elem; // 已分配内存的起始地址
T* space; // 已构造元素的末尾(下一个可放元素的位置)
T* last; // 已分配内存的末尾
// 构造函数:分配 n 个 T 的内存(但不构造对象)
vector_base(const A& a, std::size_t n)
: alloc{a},
elem{std::allocator_traits<A>::allocate(alloc, n)},
space{elem + n},
last{elem + n}
{}
// 析构函数:释放内存(调用者必须先析构对象!)
~vector_base()
{
std::allocator_traits<A>::deallocate(alloc, elem, last - elem);
}
// 禁止复制(内存不能有两个主人)
vector_base(const vector_base&) = delete;
vector_base& operator=(const vector_base&) = delete;
// 移动构造:转移所有权
vector_base(vector_base&& a) noexcept
: alloc{a.alloc}, elem{a.elem}, space{a.space}, last{a.last}
{
a.elem = a.space = a.last = nullptr; // a 不再拥有内存
}
// 移动赋值:用 swap 转移所有权
vector_base& operator=(vector_base&& a) noexcept
{
std::swap(*this, a);
return *this;
}
};
// ============================================================
// vector<T>:在 vector_base 基础上管理对象
// ============================================================
template<typename T, typename A = std::allocator<T>>
class vector {
vector_base<T, A> vb; // 内存管理交给 vb
// 销毁所有已构造的元素(不释放内存)
void destroy_elements()
{
for (T* p = vb.elem; p != vb.space; ++p)
std::allocator_traits<A>::destroy(vb.alloc, p);
vb.space = vb.elem;
}
public:
using size_type = std::size_t;
// ---- 构造函数(强保证版本) ----
explicit vector(size_type n, const T& val = T{}, const A& a = A{})
: vb{a, n} // 先分配内存(如果失败,vb 的析构不会释放,因为分配失败前 vb 没有内存)
{
// uninitialized_fill 提供强保证:
// 要么所有元素构造成功,要么回滚已构造的元素
std::uninitialized_fill(vb.elem, vb.elem + n, val);
}
// ---- 复制构造函数 ----
vector(const vector& a)
: vb{a.vb.alloc, a.size()}
{
std::uninitialized_copy(a.begin(), a.end(), vb.elem);
}
// ---- 移动构造函数 ----
vector(vector&& a) noexcept
: vb{std::move(a.vb)} // 转移内存所有权,O(1) 操作
{}
// ---- 析构函数 ----
~vector() { destroy_elements(); }
// vb 的析构函数随后自动释放内存
// ---- 基本访问 ----
size_type size() const { return vb.space - vb.elem; }
size_type capacity() const { return vb.last - vb.elem; }
bool empty() const { return size() == 0; }
T* begin() { return vb.elem; }
T* end() { return vb.space; }
const T* begin() const { return vb.elem; }
const T* end() const { return vb.space; }
T& operator[](size_type i) { return vb.elem[i]; }
const T& operator[](size_type i) const { return vb.elem[i]; }
// ---- 复制赋值(强保证版本) ----
vector& operator=(const vector& a)
{
vector temp{a}; // 构造副本(可能抛异常,此时 *this 不变)
std::swap(*this, temp); // 交换(noexcept,不会抛异常)
return *this;
} // temp 的析构函数释放旧内存
// ---- 移动赋值 ----
vector& operator=(vector&& a) noexcept
{
destroy_elements();
std::swap(vb, a.vb);
return *this;
}
// ---- reserve:增加容量 ----
void reserve(size_type newalloc)
{
if (newalloc <= capacity()) return; // 容量已够,不做事
vector_base<T, A> b{vb.alloc, newalloc}; // 申请新内存
// 把旧元素移动到新内存(移动比复制快)
T* dst = b.elem;
for (T* src = vb.elem; src != vb.space; ++src, ++dst)
::new(static_cast<void*>(dst)) T{std::move(*src)};
b.space = b.elem + size();
// 销毁旧内存中的元素(已被移动,状态为"有效但不确定")
destroy_elements();
std::swap(vb, b); // 安装新内存
} // b 的析构函数释放旧内存
// ---- resize:改变元素数量 ----
void resize(size_type newsize, const T& val = T{})
{
reserve(newsize);
if (size() < newsize) {
// 需要增加元素:在已分配但未构造的空间里构造
std::uninitialized_fill(vb.space, vb.elem + newsize, val);
} else {
// 需要减少元素:析构多余的
for (T* p = vb.elem + newsize; p != vb.space; ++p)
std::allocator_traits<A>::destroy(vb.alloc, p);
}
vb.space = vb.elem + newsize;
vb.last = vb.elem + newsize;
}
// ---- push_back:末尾追加元素 ----
void push_back(const T& val)
{
size_type sz = size();
if (capacity() == sz) // 容量不足
reserve(sz ? 2 * sz : 8); // 翻倍或初始化为 8
// 在 space 指向的位置构造新元素(placement new)
::new(static_cast<void*>(vb.space)) T{val};
++vb.space; // 只有构造成功后才移动 space 指针
}
};
// ============================================================
// 测试
// ============================================================
int main()
{
// 基本使用
vector<int> v(3, 42);
std::cout << "size=" << v.size() << " capacity=" << v.capacity() << "\n";
for (auto x : v) std::cout << x << " ";
std::cout << "\n";
// push_back
v.push_back(100);
v.push_back(200);
std::cout << "after push_back: size=" << v.size() << "\n";
for (auto x : v) std::cout << x << " ";
std::cout << "\n";
// 复制赋值(强保证)
vector<int> v2(5, 0);
v2 = v;
std::cout << "v2 after copy: ";
for (auto x : v2) std::cout << x << " ";
std::cout << "\n";
return 0;
}
异常安全关键思路总结:
"不要在有替代品之前丢掉旧信息"
reserve() 的正确顺序:
1. 申请新内存
2. 把元素移动到新内存 ← 如果这里抛异常,旧内存还在
3. 销毁旧内存的元素
4. 交换新旧内存(swap,noexcept)
5. 释放旧内存
而不是:
1. 销毁旧元素 ← 一旦这里成功,旧数据就没了
2. 释放旧内存
3. 申请新内存 ← 如果这里失败,数据永久丢失!
13.7 本章建议总结
| 编号 | 建议 |
|---|---|
| 1 | 在设计初期就制定错误处理策略 |
| 2 | 用异常表示"无法完成被分配的任务" |
| 3 | 用专门设计的类作为异常类型,不要用 int/string |
| 4 | 始终提供基本保证;尽量提供强保证 |
| 5 | 让构造函数建立不变式,失败时 throw |
| 6 | 抛出异常前释放已持有的资源 |
| 7 | 用 RAII 管理资源,最小化 try 块的使用 |
| 8 | 析构函数绝对不能抛出异常 |
| 9 | 如果函数不会抛出,声明 noexcept |
| 10 | 捕获异常层次结构时用引用 |
第14章:命名空间(Namespaces)
14.1 命名冲突问题
想象有两个库:
// Graph_lib 提供的:
class Line { /*...*/ }; // 直线
class Text { /*...*/ }; // 文字标签
// Text_lib 提供的:
class Line { /*...*/ }; // 文字行
class Text { /*...*/ }; // 文字内容
两个库同时 #include,编译器不知道你要哪个 Line——名字冲突(Name Clash)!
命名空间就是为了解决这个问题。
14.2 命名空间基础
基本语法
#include <iostream>
#include <string>
// 定义命名空间(可以分多次定义,命名空间是开放的)
namespace Graph_lib {
class Shape { public: virtual void draw() {} };
class Line : public Shape { /* 直线 */ };
class Text : public Shape {
public:
std::string content;
};
// 函数也可以放在命名空间里
void open(const char* filename) {}
}
namespace Text_lib {
class Glyph { /* 字形 */ };
class Word { /* 单词 */ };
class Line { /* 文字行(与 Graph_lib::Line 完全不同!) */ };
class Text { /* 文字内容 */ };
}
int main()
{
// 完全限定名(Fully Qualified Name):不会有任何歧义
Graph_lib::Line gl; // 图形库的 Line
Text_lib::Line tl; // 文字库的 Line
Graph_lib::Text gt;
Text_lib::Text tt;
return 0;
}
14.2.1 显式限定(Explicit Qualification)
在命名空间外定义成员,必须用 命名空间::成员名 的方式:
#include <iostream>
namespace Parser {
// 声明(放在头文件里)
double expr(bool get);
double term(bool get);
double prim(bool get);
}
// 定义(放在源文件里)——注意必须用 Parser:: 前缀
double Parser::prim(bool get)
{
std::cout << "prim called\n";
return 0.0;
}
double Parser::term(bool get)
{
return Parser::prim(get); // 调用同一命名空间的成员,可以不加前缀
}
double Parser::expr(bool get)
{
return term(get); // 在 Parser 的实现里,term 指 Parser::term
}
// 错误示例(注释掉):
// void Parser::logical(bool); // 错误:Parser 里根本没有 logical
// double Parser::trem(bool); // 错误:拼写错误(trem 不存在)
int main()
{
Parser::expr(true);
return 0;
}
14.2.2 using 声明(using-Declaration)
把某个具体名字引入当前作用域:
#include <string>
#include <vector>
#include <sstream>
#include <iostream>
// 没有 using 声明时:每次都要写 std::
std::vector<std::string> split_verbose(const std::string& s)
{
std::vector<std::string> res;
std::istringstream iss(s);
for (std::string buf; iss >> buf;)
res.push_back(buf);
return res;
}
// 有 using 声明时:只引入需要的名字,其余仍然需要 std::
using std::string; // 只把 string 引入当前作用域
using std::vector;
vector<string> split(const string& s)
{
vector<string> res;
std::istringstream iss(s); // istringstream 没有 using,仍需 std::
for (string buf; iss >> buf;)
res.push_back(buf);
return res;
}
int main()
{
auto words = split("hello world foo bar");
for (const auto& w : words)
std::cout << w << "\n";
return 0;
}
14.2.3 using 指令(using-Directive)
把整个命名空间的所有名字引入当前作用域:
#include <iostream>
#include <string>
#include <vector>
#include <sstream>
using namespace std; // 引入 std 的所有名字
vector<string> split(const string& s)
{
vector<string> res;
istringstream iss(s); // 不再需要 std::
for (string buf; iss >> buf;)
res.push_back(buf);
return res;
}
int main()
{
cout << "输入: hello world\n"; // 不再需要 std::
auto words = split("hello world");
for (const& w : words)
cout << w << "\n";
return 0;
}
警告:在头文件里不要用 using namespace!
// 危险的头文件(bad.h)
#include <vector>
using namespace std; // 这会污染所有 #include 这个头文件的代码!
// 任何 #include "bad.h" 的文件都会突然多出 std 的所有名字
// 可能导致意外的名字冲突
14.2.4 实参依赖查找(ADL,Argument-Dependent Lookup)
当调用函数找不到时,会自动在实参类型所在的命名空间里搜索:
#include <iostream>
#include <string>
namespace Chrono {
class Date {
public:
int year, month, day;
};
// operator== 定义在 Chrono 命名空间里
bool operator==(const Date& d, const std::string& s)
{
return false; // 简化实现
}
// format 函数也在 Chrono 命名空间里
std::string format(const Date& d)
{
return std::to_string(d.year) + "-" +
std::to_string(d.month) + "-" +
std::to_string(d.day);
}
}
void f(Chrono::Date d, int i)
{
// format(d) 没有在当前作用域找到 format 的声明
// 但 d 是 Chrono::Date 类型
// ADL:自动在 Chrono 命名空间里搜索
std::string s = format(d); // 找到 Chrono::format !
std::cout << s << "\n";
// format(i) 失败:int 没有关联命名空间,在全局也没有 format
// std::string t = format(i); // 编译错误!
}
int main()
{
Chrono::Date today{2026, 5, 27};
f(today, 42);
return 0;
}
ADL 特别重要的场合:
#include <iostream>
#include <vector>
// 为自定义类型定义 operator<<,需要 ADL 才能让 std::cout << 找到它
namespace MyLib {
struct Point { int x, y; };
std::ostream& operator<<(std::ostream& os, const Point& p)
{
return os << "(" << p.x << ", " << p.y << ")";
}
}
int main()
{
MyLib::Point p{3, 4};
std::cout << p << "\n"; // ADL 找到 MyLib::operator<<
return 0;
}
14.2.5 命名空间是开放的
同一个命名空间可以分多次声明(分布在不同文件或同一文件的不同位置):
namespace A {
int f(); // 第一次声明:A 只有 f
}
// 在别处(或别的文件)继续向 A 添加成员
namespace A {
int g(); // 现在 A 有 f 和 g
int h();
}
// 实现可以放在更后面
int A::f() { return 1; }
int A::g() { return 2; }
int A::h() { return 3; }
这对于把旧代码逐步迁移到命名空间非常有用。
14.3 模块化与接口
计算器程序的模块结构
实线 = 使用关系,虚线 = 实现关系
用命名空间表达模块
#include <map>
#include <string>
#include <iostream>
// ---- 错误处理模块 ----
namespace Error {
int no_of_errors = 0;
double error(const std::string& s)
{
std::cerr << "错误: " << s << "\n";
++no_of_errors;
return 1.0; // 返回一个合理的默认值,让程序能继续
}
}
// ---- 词法分析模块 ----
namespace Lexer {
enum class Kind : char {
number, name, plus='+', minus='-',
mul='*', div='/', assign='=',
lp='(', rp=')', end
};
struct Token {
Kind kind;
double number_value = 0;
std::string string_value;
};
// Token_stream 这里简化为演示
struct Token_stream {
Token current_token;
Token get() { return current_token; }
Token current() const { return current_token; }
} ts;
}
// ---- 符号表模块 ----
namespace Table {
std::map<std::string, double> table; // 变量名 → 值
}
// ---- 语法分析模块(接口) ----
namespace Parser {
double expr(bool get); // 只向外暴露 expr
}
// ---- 语法分析模块(实现,可以访问其他所有模块) ----
double Parser::expr(bool get)
{
// 实现时可以用 Lexer::, Table::, Error:: 等
// 这里简化返回
return 0.0;
}
// ---- 驱动层 ----
namespace Driver {
void calculate()
{
double val = Parser::expr(true);
std::cout << "结果 = " << val << "\n";
}
}
int main()
{
Driver::calculate();
return 0;
}
接口与实现的分离
理想状态:
用户看到的 Parser(接口):
只有 double expr(bool);
实现者看到的 Parser(实现):
double prim(bool);
double term(bool);
double expr(bool);
using namespace Lexer;
using Error::error;
using Table::table;
// parser.h(用户看到的接口——只有这一个函数)
namespace Parser {
double expr(bool get);
}
// parser_impl.h(实现者用的内部接口)
namespace Parser_impl {
using namespace Parser; // 包含对外接口
double prim(bool);
double term(bool);
double expr(bool); // 这里是 Parser_impl 的 expr
// 引入其他模块,方便实现代码使用
// using namespace Lexer;
// using Error::error;
// using Table::table;
}
14.4 命名空间的组合技巧
14.4.1 using 声明 vs using 指令的安全性
#include <iostream>
namespace X {
int i = 10, j = 20, k = 30;
}
int k = 99; // 全局的 k
void f1()
{
int i = 0;
using namespace X; // 把 X 的名字引入,但不会遮蔽局部变量
i++; // 局部 i(遮蔽了 X::i)
j++; // X::j(没有局部 j)
// k++; // 错误!X::k 和全局 k 都能看到,歧义!
::k++; // 明确:全局 k
X::k++; // 明确:X::k
}
void f2()
{
int i = 0;
using X::i; // 错误:i 在 f2 中已经声明过了!
using X::j; // OK:引入 X::j
using X::k; // OK:遮蔽全局 k
// i++; // 会报错(重复声明)
j++; // X::j
k++; // X::k(遮蔽了全局 k)
}
int main()
{
::k = 0;
f1();
std::cout << "全局 k = " << ::k << "\n"; // 被 f1 修改过
std::cout << "X::k = " << X::k << "\n"; // 被 f1 修改过
return 0;
}
规律总结:
using 声明(using X::name):
- 把名字引入当前作用域
- 遮蔽同名的外层变量(局部优先)
- 重复声明是错误
using 指令(using namespace X):
- 不把名字引入当前作用域(只是"可见")
- 与全局名字同名时产生歧义(不会自动选一个)
- 歧义只在使用时报错,不是引入时报错
14.4.2 命名空间别名
// 长名字不好用
namespace American_Telephone_and_Telegraph {
struct String { std::string data; };
}
// 定义别名(短而不失清晰)
namespace ATT = American_Telephone_and_Telegraph;
// 用别名访问
ATT::String s;
// 别名特别适合版本管理
namespace Foundation_library_v2r11 {
struct Widget {};
}
namespace Lib = Foundation_library_v2r11; // 升级时只改这一行!
Lib::Widget w; // 代码其他地方不用改
14.4.3 命名空间组合
#include <string>
namespace His_string {
class String {
public:
String(const char* s = "") : data(s) {}
std::string data;
};
String operator+(const String& a, const String& b)
{
return String((a.data + b.data).c_str());
}
}
namespace Her_vector {
template<typename T>
class Vector {
T items[100];
int sz = 0;
public:
void push(const T& v) { items[sz++] = v; }
T& operator[](int i) { return items[i]; }
};
}
// 把两个命名空间组合成一个新命名空间
namespace My_lib {
using namespace His_string; // 包含 His_string 的所有内容
using namespace Her_vector; // 包含 Her_vector 的所有内容
void my_fct(String& s) { /* ... */ }
}
// 使用组合后的命名空间
void f()
{
My_lib::String s = "Byron"; // 找到 His_string::String
My_lib::Vector<My_lib::String> v;
v.push(s);
}
int main()
{
f();
return 0;
}
14.4.4 组合与选择(解决冲突)
当两个命名空间有同名实体时,用 using 声明指定用哪一个:
#include <string>
namespace His_lib {
class String { public: std::string val; };
template<typename T> class Vector {};
}
namespace Her_lib {
template<typename T> class Vector {}; // 与 His_lib::Vector 冲突!
class String { public: std::string val; }; // 与 His_lib::String 冲突!
}
namespace My_lib {
using namespace His_lib; // 引入 His_lib 的所有内容
using namespace Her_lib; // 引入 Her_lib 的所有内容(可能有冲突)
// 用 using 声明显式解决冲突:
using His_lib::String; // String 用 His_lib 的(覆盖 Her_lib 的)
using Her_lib::Vector; // Vector 用 Her_lib 的(覆盖 His_lib 的)
template<typename T>
class List {}; // My_lib 自己的新内容
}
int main()
{
My_lib::String s; // 用的是 His_lib::String
My_lib::Vector<int> v; // 用的是 Her_lib::Vector
My_lib::List<double> l; // My_lib 自己的
return 0;
}
14.4.5 命名空间与函数重载
重载(overloading)跨命名空间工作,这让库迁移到命名空间后,用户代码几乎不用改:
#include <iostream>
// 旧版本(没有命名空间)——两个函数在全局空间重载
// void f(int) { std::cout << "f(int)\n"; }
// void f(char) { std::cout << "f(char)\n"; }
// 新版本(用命名空间,但行为一样)
namespace A {
void f(int) { std::cout << "A::f(int)\n"; }
}
namespace B {
void f(char) { std::cout << "B::f(char)\n"; }
}
using namespace A;
using namespace B;
void g()
{
f('a'); // 重载决议:char 更匹配 B::f(char)
f(1); // 重载决议:int 更匹配 A::f(int)
}
// 用命名空间扩展标准库
#include <algorithm>
#include <vector>
namespace Estd {
using namespace std;
// 添加"容器版本"的 sort
template<typename C>
void sort(C& c) { std::sort(c.begin(), c.end()); }
template<typename C, typename P>
void sort(C& c, P pred) { std::sort(c.begin(), c.end(), pred); }
}
int main()
{
g();
using namespace Estd;
std::vector<int> v{3, 1, 4, 1, 5, 9};
sort(v); // 用 Estd::sort(容器版本)
sort(v.begin(), v.end()); // 用 std::sort(迭代器版本,通过 ADL)
for (auto x : v) std::cout << x << " ";
std::cout << "\n";
return 0;
}
14.4.6 版本管理(inline namespace)
#include <iostream>
namespace Popular {
// inline 表示 V3_2 是默认版本
inline namespace V3_2 {
double f(double x) { return x * 2; }
int f(int x) { return x * 2; }
}
namespace V3_0 {
double f(double x) { return x + 1; }
}
namespace V2_4_2 {
double f(double x) { return x - 1; }
}
}
void demo_versioning()
{
using namespace Popular;
f(1); // 调用 Popular::V3_2::f(int)(默认版本)
V3_0::f(1); // 显式调用旧版本
V2_4_2::f(1); // 显式调用更旧的版本
std::cout << "默认 f(1.0) = " << f(1.0) << "\n"; // V3_2
std::cout << "V3_0 f(1.0) = " << Popular::V3_0::f(1.0) << "\n";
std::cout << "V2_4_2 f(1) = " << Popular::V2_4_2::f(1.0) << "\n";
}
int main()
{
demo_versioning();
return 0;
}
14.4.7 嵌套命名空间
#include <iostream>
void h_global();
namespace X {
void g();
namespace Y {
void f() { std::cout << "X::Y::f\n"; }
void ff() {
f(); // X::Y::f(当前命名空间)
g(); // X::g(外层命名空间)
h_global(); // 全局 h
}
}
}
void X::g()
{
// f(); // 错误:X 里没有 f,Y::f 需要明确指定
X::Y::f(); // OK
std::cout << "X::g\n";
}
void h_global()
{
// f(); // 错误:全局没有 f
// Y::f(); // 错误:全局没有 Y
// X::f(); // 错误:X 里没有 f
X::Y::f(); // OK:完整限定名
}
int main()
{
X::Y::ff();
X::g();
h_global();
return 0;
}
14.4.8 匿名命名空间
当你想让某些名字只在当前文件(翻译单元)可见时,用匿名命名空间(相当于旧式的 static):
// file1.cpp
namespace {
// 这些名字只在 file1.cpp 里可见
// 其他文件的匿名命名空间是不同的命名空间,不会冲突
int secret_counter = 0;
void internal_helper()
{
++secret_counter;
}
}
// 对外暴露的函数
void public_api()
{
internal_helper(); // OK:在同一文件里
}
// file2.cpp 有自己的匿名命名空间,与 file1.cpp 的互不干扰
// namespace {
// int secret_counter = 0; // 不冲突!
// }
14.5 本章建议总结
| 编号 | 建议 |
|---|---|
| 1 | 用命名空间表达逻辑结构 |
| 2 | 把所有非 main 的名字放进某个命名空间 |
| 3 | 命名空间要设计成不会让用户意外访问到不相关内容 |
| 4 | 避免用非常短的命名空间名(容易冲突) |
| 5 | 用命名空间别名缩写过长的命名空间名 |
| 6 | 接口和实现用不同命名空间分离 |
| 7 | 用 Namespace::member 语法定义命名空间成员 |
| 8 | 用 inline namespace 支持版本管理 |
| 9 | using 指令只用于过渡期、基础库(如 std)或局部作用域 |
| 10 | 不要在头文件里放 using-directive |
附录:两章核心概念速查
异常处理流程
RAII 生命周期
构造函数 对象存活期 析构函数
| |
v v
[获取资源] ──────────────────────── [释放资源]
^ ^
| |
正常退出 异常退出
都会调用析构!
命名空间访问方式对比
| 方式 | 语法 | 作用域污染 | 推荐场合 |
|---|---|---|---|
| 完全限定 | std::cout |
无 | 不常用的名字 |
| using 声明 | using std::cout |
最小(只引入一个) | 频繁用某个名字 |
| using 指令 | using namespace std |
较大(引入所有) | 局部作用域、基础库 |
| 命名空间别名 | namespace S = std |
无 | 长名字简写 |
C++ 第15章·第16章 深度中文解析
原书:《The C++ Programming Language》(Stroustrup)
覆盖:第15章 源文件与程序、第16章 类
目标:从零开始,把每个概念讲清楚,代码加详细注释。
第15章:源文件与程序(Source Files and Programs)
15.1 分离编译(Separate Compilation)
为什么要分离编译?
一个真实的程序不可能写在一个文件里。原因有三:
1. 标准库和操作系统的代码不以源码形式提供
2. 单个大文件难以理解和维护
3. 任何微小改动都要重编译整个文件——太慢
分离编译的好处:
只修改了 lexer.cpp
↓
只重新编译 lexer.cpp(几秒)
↓
链接器把所有 .o 文件合并
↓
得到可执行程序
而不是:重新编译整个项目(可能要几分钟或几小时)
编译流程
源文件 (.cpp)
↓ 预处理器(展开宏、处理 #include)
翻译单元 (Translation Unit)
↓ 编译器
目标文件 (.o / .obj)
↓ 链接器 (Linker)
可执行文件
翻译单元(Translation Unit):一个源文件经过预处理后的结果。
C++ 的语言规则描述的是翻译单元,不是原始源文件。
15.2 链接(Linkage)
基本概念
多个翻译单元里的名字必须一致。链接器负责把它们绑在一起,并检查某些不一致。
// file1.cpp
int x = 1; // 定义:分配内存并赋初值
int f() { return x; } // 定义:函数体在这里
// file2.cpp
extern int x; // 声明:告诉编译器 x 在别处定义
int f(); // 声明:告诉编译器 f 在别处定义
void g() { x = f(); } // 使用 file1.cpp 里的 x 和 f
声明 vs 定义的区别:
声明(Declaration):告诉编译器"有这个东西,类型是这样的"
定义(Definition):真正分配内存或提供函数体
一个实体:可以声明多次,但只能定义一次!
常见的链接错误
// file1.cpp
int x = 1; // 定义
int b = 1; // 定义
extern int c; // 声明(c 在别处定义)
// file2.cpp
int x; // 错误1:x 被定义了两次!(全局变量默认初始化为0,也算定义)
extern double b;// 错误2:b 的类型不一致(一个是int,一个是double)
extern int c; // 声明(但 c 从未被定义——如果用到 c,链接器会报错)
| 错误类型 | 说明 | 谁来检测 |
|---|---|---|
| 同一名字定义两次 | 链接错误 | 链接器(大多能检测) |
| 同一名字类型不一致 | 未定义行为 | 链接器(很多检测不出来!) |
| 声明了但从未定义 | 链接错误 | 链接器(使用时才报错) |
15.2.1 外部链接 vs 内部链接
外部链接(External Linkage):名字可以在其他翻译单元中使用。
内部链接(Internal Linkage):名字只在本翻译单元内可见。
// 演示内部 vs 外部链接
// === 内部链接(其他文件看不到这些名字)===
static int x1 = 1; // static 关键字 → 内部链接
const char x2 = 'a'; // const 默认内部链接(在命名空间/全局作用域)
namespace { // 匿名命名空间 → 内部链接
void secret_func() {}
int secret_var = 42;
}
// === 外部链接(其他文件可以访问)===
int x3 = 10; // 普通全局变量 → 外部链接
extern const char x4 = 'b'; // extern 显式声明外部链接的 const
void public_func() {} // 普通函数 → 外部链接
// 记忆规律:
// const / static / 匿名namespace → 内部链接(文件私有)
// 普通变量和函数 → 外部链接(跨文件可见)
默认内部链接的类型:
| 实体类型 | 默认链接 | 改为外部链接的方法 |
|---|---|---|
const 变量 |
内部 | 加 extern |
constexpr 变量 |
内部 | 加 extern |
static 变量/函数 |
内部 | 去掉 static |
| 匿名命名空间内的实体 | 内部 | 无法改变 |
inline 函数 |
外部(但每处定义必须相同) | — |
inline 函数的特殊规则
// 错误示例:同一个 inline 函数在两个文件里有不同定义
// file1.cpp
inline int f(int i) { return i; } // 版本1
// file2.cpp
inline int f(int i) { return i + 1; } // 版本2——非法!定义不一致!
// 正确做法:把 inline 函数放在头文件里,所有文件都 #include 同一个定义
// h.h:
inline int next(int i) { return i + 1; } // 所有包含 h.h 的文件看到同一个定义
15.2.2 头文件(Header Files)
头文件的核心作用:在不同翻译单元之间共享声明,保证一致性。
┌─────────────────────────┐
│ s.h │
│ struct S { int a; }; │
│ void f(S*); │
└────────┬────────────────┘
│ #include
┌────────────┴────────────┐
↓ ↓
file1.cpp file2.cpp
// use f() here void f(S* p) { ... }
两个文件都 #include "s.h",编译器能保证它们对 S 和 f 的理解一致。
头文件中可以放什么:
| 可以放 | 示例 |
|---|---|
| 命名空间定义 | namespace N { ... } |
| 类型定义 | struct Point { int x, y; }; |
| 模板声明和定义 | template<class T> class V { ... }; |
| 函数声明 | extern int strlen(const char*); |
inline 函数定义 |
inline char get(char* p) { return *p++; } |
constexpr 函数定义 |
constexpr int fac(int n) { ... } |
变量声明(加 extern) |
extern int a; |
const 定义 |
const float pi = 3.14f; |
constexpr 定义 |
constexpr float pi2 = pi * pi; |
| 枚举定义 | enum class Light { red, green }; |
| 类型别名 | using value_type = long; |
| 静态断言 | static_assert(4 <= sizeof(int), "..."); |
#include 指令 |
#include <algorithm> |
头文件中不能放什么:
| 不能放 | 原因 |
|---|---|
| 普通函数定义 | 每个包含该头文件的文件都会有一份定义,违反 ODR |
变量定义(无 extern) |
同上,每个文件各自分配内存 |
| 数组定义 | 同上 |
| 匿名命名空间 | 每个文件得到不同的命名空间,不是共享 |
using namespace 指令 |
会污染所有包含该头文件的代码 |
// 完整示例:正确使用头文件
// ===== mylib.h =====
#ifndef MYLIB_H // include guard(防止重复包含)
#define MYLIB_H
#include <string> // 头文件里可以 #include 其他头文件
// 类型定义:OK
struct Point {
int x, y;
};
// 类声明:OK
class Calculator {
public:
Calculator();
int add(int a, int b);
static int instance_count; // 静态数据成员声明
private:
int state;
};
// 变量声明(extern):OK
extern int global_counter;
// const 定义:OK(内部链接,每个翻译单元各有一份,但值相同)
const double PI = 3.14159265358979;
// inline 函数定义:OK(必须在头文件里,让每个翻译单元都能看到定义)
inline int square(int x) { return x * x; }
// 函数声明:OK
void print_point(const Point& p);
#endif // MYLIB_H
// ===== mylib.cpp =====
// (不在此处展示,但里面会 #include "mylib.h" 并提供函数定义)
15.2.3 单一定义规则(One-Definition Rule, ODR)
ODR: 在整个程序中,每个类、枚举、模板等必须只有一个定义。
ODR 的精确版本:
如果同一个类在不同翻译单元中有多个定义,满足以下三个条件时,视为同一个定义:
合法 ⟺ { 出现在不同翻译单元 token 逐字相同 每个 token 在两处含义相同 \text{合法} \iff \begin{cases} \text{出现在不同翻译单元} \\ \text{token 逐字相同} \\ \text{每个 token 在两处含义相同} \end{cases} 合法⟺⎩ ⎨ ⎧出现在不同翻译单元token 逐字相同每个 token 在两处含义相同
// 合法:两个文件都通过 #include 得到完全相同的定义
// s.h: struct S { int a; char b; };
// file1.cpp: #include "s.h"
// file2.cpp: #include "s.h" ← OK,符合 ODR
// 违反 ODR 的例子:
// 例1:同一文件里定义两次
struct S1 { int a; char b; };
struct S1 { int a; char b; }; // 错误:同一翻译单元里定义两次
// 例2:两个文件里的定义成员名不同
// file1.cpp: struct S2 { int a; char b; };
// file2.cpp: struct S2 { int a; char bb; }; // 错误:成员名不同
// 例3:最隐蔽的 ODR 违反:token 相同但含义不同
// file1.cpp:
// typedef int X;
// struct S3 { X a; char b; }; ← X 是 int
// file2.cpp:
// typedef char X;
// struct S3 { X a; char b; }; ← X 是 char,但结构看起来一样!
// 这是 ODR 违反,但很多编译器检测不出来!
最佳防御:让头文件自包含(Self-Contained)
// 危险的 s.h(不自包含)
// struct S { Point a; char b; }; ← 假设 Point 已经定义了
// 安全的 s.h(自包含)
#ifndef S_H
#define S_H
#include "point.h" // 自己 #include 所需的依赖
struct S { Point a; char b; };
#endif
15.2.4 标准库头文件
C++ 标准库头文件用尖括号引用,没有 .h 后缀:
#include <iostream> // C++ 风格
#include <string>
#include <vector>
// 对应 C 标准库(加 c 前缀,去掉 .h)
#include <cstdio> // 对应 C 的 <stdio.h>
#include <cstring> // 对应 C 的 <string.h>
#include <cmath> // 对应 C 的 <math.h>
C 和 C++ 头文件共享的原理(简化版的 stdio.h):
// 如果是 C++ 编译器
#ifdef __cplusplus
namespace std {
extern "C" {
#endif
// 实际的 C 函数声明(C 和 C++ 共用)
int printf(const char*, ...);
// ...
#ifdef __cplusplus
} // end extern "C"
} // end namespace std
using std::printf; // 把 printf 引入全局作用域,向后兼容
#endif
15.2.5 与非 C++ 代码的链接
不同语言(C、Fortran、汇编)编译出的函数,其名字修饰(Name Mangling)和调用约定可能不同。
C++ 用 extern "C" 来指定使用 C 的链接约定:
#include <cstring>
#include <iostream>
// 单个函数声明为 C 链接
extern "C" char* strcpy(char*, const char*);
// 一组函数批量声明为 C 链接
extern "C" {
char* strcpy(char*, const char*);
int strcmp(const char*, const char*);
int strlen(const char*);
}
// 用于创建 C/C++ 通用头文件的标准技巧
#ifdef __cplusplus
extern "C" {
#endif
// 以下声明在 C 和 C++ 中都有效
int my_c_function(int x);
#ifdef __cplusplus
}
#endif
// 注意:extern "C" 只影响链接约定,不影响 C++ 的类型检查!
extern "C" int f();
int g()
{
// return f(1); // 错误:f 声明为无参,C++ 仍然检查参数类型!
return f(); // OK
}
int main()
{
char dst[20];
strcpy(dst, "hello");
std::cout << dst << "\n";
return 0;
}
C 和 C++ 名字修饰的区别:
C 编译器: void foo(int) → 链接名: foo
C++ 编译器: void foo(int) → 链接名: _Z3fooi (包含了参数类型信息)
void foo(double) → 链接名: _Z3food
extern "C" 的作用:让 C++ 编译器不做名字修饰,用 C 风格的简单名字
15.2.6 链接与函数指针
把函数指针在 C 和 C++ 之间传递时,必须保证链接约定一致:
#include <cstdlib> // qsort
#include <iostream>
// C++ 链接的比较函数类型
typedef int (*FT)(const void*, const void*);
// C 链接的比较函数类型(用于传给 C 的 qsort)
extern "C" {
typedef int (*CFT)(const void*, const void*);
// qsort 期望接收 C 链接的函数指针
void qsort(void* p, size_t n, size_t sz, CFT cmp);
}
// C++ 链接的比较函数(不能直接传给 qsort!)
int compare_cpp(const void* a, const void* b)
{
return *static_cast<const int*>(a) - *static_cast<const int*>(b);
}
// C 链接的比较函数(可以传给 qsort)
extern "C" int compare_c(const void* a, const void* b)
{
return *static_cast<const int*>(a) - *static_cast<const int*>(b);
}
int main()
{
int arr[] = {5, 3, 8, 1, 9, 2};
int n = sizeof(arr) / sizeof(arr[0]);
// qsort(arr, n, sizeof(int), &compare_cpp); // 错误!类型不匹配
qsort(arr, n, sizeof(int), &compare_c); // OK:C 链接匹配
for (int x : arr) std::cout << x << " ";
std::cout << "\n";
return 0;
}
15.3 使用头文件
15.3.1 单头文件组织(Single-Header Organization)
小型程序的推荐组织方式:一个共用头文件 + 多个实现文件。
以计算器程序为例:
项目结构:
dc.h ← 所有模块的声明都放这里
lexer.cpp ← 词法分析器的实现
parser.cpp ← 语法分析器的实现
table.cpp ← 符号表的实现
error.cpp ← 错误处理的实现
main.cpp ← 主程序
关键点: 每个 .cpp 文件都 #include "dc.h",从而共享所有声明。
// ===== dc.h(单头文件组织)=====
#ifndef DC_H
#define DC_H
#include <map>
#include <string>
#include <iostream>
namespace Parser {
double expr(bool get); // 只声明,不定义
double term(bool get);
double prim(bool get);
}
namespace Lexer {
enum class Kind : char {
name, number, end,
plus='+', minus='-', mul='*', div='/',
assign='=', lp='(', rp=')'
};
struct Token {
Kind kind;
std::string string_value;
double number_value = 0;
};
// extern:声明变量,不定义(定义在 lexer.cpp 里)
extern Token current_token;
}
namespace Table {
extern std::map<std::string, double> table; // 声明,不定义
}
namespace Error {
extern int no_of_errors; // 声明,不定义
double error(const std::string& s);
}
#endif // DC_H
// ===== lexer.cpp =====
// #include "dc.h"
// Lexer::Token Lexer::current_token; ← 定义(分配内存)
// ===== table.cpp =====
// #include "dc.h"
// std::map<std::string, double> Table::table; ← 定义
// ===== error.cpp =====
// #include "dc.h"
// int Error::no_of_errors = 0; ← 定义
// double Error::error(const std::string& s) { ... } ← 定义
使用 dc.h 中的声明确保一致性的原理:
编译 lexer.cpp 时,编译器看到:
1. 来自 dc.h 的声明:Token get();
2. lexer.cpp 里的定义:Token Token_stream::get() { ... }
如果两者类型不一致 → 编译器立即报错
如果定义缺失 → 链接器报错
15.3.2 多头文件组织(Multiple-Header Organization)
大型程序的最佳实践:每个模块有自己的头文件。
模块划分(以 Parser 为例):
parser.h ← 对外接口(用户看到的)
parser_impl.h ← 对内接口(实现者看到的)
parser.cpp ← 实现代码
// ===== parser.h(用户接口:只暴露必要的函数)=====
#ifndef PARSER_H
#define PARSER_H
namespace Parser {
double expr(bool get); // 用户只需要知道这一个函数
}
#endif
// ===== parser_impl.h(实现者接口:提供实现所需的环境)=====
#ifndef PARSER_IMPL_H
#define PARSER_IMPL_H
#include "parser.h" // 包含对外接口
// #include "error.h"
// #include "lexer.h"
// using Error::error;
// using namespace Lexer;
namespace Parser {
// 这些函数只给实现者用,不对外暴露
double prim(bool get);
double term(bool get);
double expr(bool get);
}
#endif
// ===== parser.cpp(实现)=====
// #include "parser_impl.h"
// #include "table.h"
// using Table::table;
// double Parser::prim(bool get) { ... }
// double Parser::term(bool get) { ... }
// double Parser::expr(bool get) { ... }
多头文件 vs 单头文件的对比:
| 特性 | 单头文件 | 多头文件 |
|---|---|---|
| 适用规模 | 小型项目 | 大型项目 |
| 修改一个模块的影响 | 重编译所有文件 | 只重编译依赖该模块的文件 |
| 编译速度 | 慢(项目变大后) | 快(依赖分析精确) |
| 维护难度 | 简单 | 稍复杂但可管理 |
| 接口与实现分离 | 弱 | 强 |
15.3.3 包含保护(Include Guards)
同一个头文件可能被间接包含多次,导致类的重复定义(违反 ODR)。
解决方案:Include Guard(包含保护)
// error.h
// 第一次被包含时:CALC_ERROR_H 未定义 → 进入 #ifndef 块 → 定义 CALC_ERROR_H
// 第二次被包含时:CALC_ERROR_H 已定义 → 跳过整个文件
#ifndef CALC_ERROR_H
#define CALC_ERROR_H // 标记"我已经被处理过了"
#include <string>
namespace Error {
extern int no_of_errors;
double error(const std::string& s);
}
#endif // CALC_ERROR_H ← 注释说明对应的宏,便于阅读
// ===========================
// 现代替代方案(非标准但广泛支持):
// #pragma once
// 效果与 include guard 相同,但更简洁
// ===========================
为什么 include guard 用长而丑的名字?
头文件可以在任意上下文中被包含,没有命名空间保护宏名。
所以要选择几乎不可能被其他地方使用的宏名。
常见命名规则:
项目名_模块名_H 例:MYPROJECT_PARSER_H
项目名_路径_模块名_H 例:MYPROJECT_CALC_ERROR_H
15.4 程序(Programs)
程序的组成
一个程序 = 多个分别编译的翻译单元 + 链接器。
每个程序必须有且仅有一个 main() 函数。
#include <iostream>
#include <sstream>
#include <string>
// main 的两种合法形式:
// 形式1:无参数
// int main() { ... }
// 形式2:命令行参数
// argc:参数个数(至少为1,第0个是程序名)
// argv:参数字符串数组
int main(int argc, char* argv[])
{
std::cout << "程序名: " << argv[0] << "\n";
std::cout << "参数个数: " << argc - 1 << "\n";
for (int i = 1; i < argc; ++i)
std::cout << "参数" << i << ": " << argv[i] << "\n";
return 0; // 0 表示成功,非零表示失败
}
15.4.1 非局部变量的初始化
#include <cmath> // sqrt
#include <iostream>
// 非局部变量(全局变量、命名空间变量、类的静态成员)
// 在 main() 开始之前被初始化
double x = 2.0; // 常量表达式 → 链接时初始化(最安全)
double y; // 没有初始化器 → 默认初始化为 0.0
double sqx = std::sqrt(x + y); // 运行时初始化(依赖 x 和 y)
// 初始化顺序:在同一翻译单元内,按定义顺序
// 所以:x → y → sqx(sqx = sqrt(2.0) = 1.414...)
// 危险:不同翻译单元之间的全局变量初始化顺序不确定!
// file1.cpp: int a = 1;
// file2.cpp: int b = a + 1; ← 如果 file1 先初始化,b=2;否则 b=1(错!)
// 推荐:用函数返回引用替代全局变量,实现"按需初始化"
int& use_count()
{
// 局部静态变量:第一次调用时初始化,之后保持
// 线程安全(C++11 保证)
static int uc = 0;
return uc;
}
void f()
{
++use_count(); // 读写全局计数器,但通过函数控制
std::cout << "use_count = " << use_count() << "\n";
}
int main()
{
f();
f();
f();
std::cout << "sqx = " << sqx << "\n";
return 0;
}
"函数返回引用"技术的优点:
普通全局变量:
程序启动时初始化(顺序不确定)
可能在被使用前就出现依赖问题
函数返回 static 局部变量的引用:
第一次调用该函数时才初始化
避免了跨翻译单元的初始化顺序问题
称为"Meyers Singleton"或"懒初始化"
15.4.2 初始化与并发
// 危险示例:多线程中全局变量的初始化
int x_global = 3;
// int y_global = std::sqrt(++x_global); // 多线程时可能有数据竞争!
// 安全的替代方案(按优先级排列):
// 方案1:用常量表达式初始化(编译时/链接时完成,无运行时数据竞争)
constexpr int CONST_VAL = 42; // 编译时确定
// 方案2:用没有副作用的表达式初始化
// double pi = 3.14159; // 不修改其他变量,安全
// 方案3:在已知的单线程启动阶段初始化
// (在 main 里,在创建任何线程之前)
// 方案4:用互斥锁保护(最后的手段,有性能开销)
#include <mutex>
std::mutex init_mutex;
int shared_val = 0;
void safe_init()
{
std::lock_guard<std::mutex> lock(init_mutex);
if (shared_val == 0) shared_val = compute_value();
}
int compute_value() { return 42; }
int main()
{
return 0;
}
15.4.3 程序终止
程序有多种终止方式,行为各不相同:
#include <cstdlib> // exit, abort, atexit, quick_exit
#include <iostream>
struct Resource {
Resource() { std::cout << "Resource 构造\n"; }
~Resource() { std::cout << "Resource 析构\n"; }
};
// 用 atexit 注册清理函数(C 风格,不如 RAII)
void cleanup_function()
{
std::cout << "atexit 清理函数被调用\n";
// 注意:此时析构函数已经(或正在)被调用
// 调用顺序:atexit 函数与析构函数的相对顺序取决于注册时机
}
int main()
{
// 注册 atexit 清理函数
if (atexit(&cleanup_function) != 0) {
std::cout << "atexit 注册失败(超过限制)\n";
}
static Resource r; // 静态对象:程序结束时析构
std::cout << "main 开始\n";
// exit(0); // 正常退出:调用静态对象析构,调用 atexit 函数
// abort(); // 异常终止:不调用析构函数,不调用 atexit 函数
// quick_exit(0); // 类似 exit 但不调用析构函数
return 0; // 等价于 exit(0)
}
// 运行输出(使用 return 0):
// Resource 构造
// main 开始
// Resource 析构
// atexit 清理函数被调用
| 终止方式 | 调用静态析构函数 | 调用 atexit 函数 | 用途 |
|---|---|---|---|
return from main |
是 | 是 | 正常退出 |
exit(0) |
是 | 是 | 正常退出(在任何地方调用) |
quick_exit(0) |
否 | 否(调用 at_quick_exit) | 快速退出 |
abort() |
否 | 否 | 异常终止、调试 |
| 未捕获异常 | 实现定义 | 实现定义 | 错误情况 |
15.5 本章建议总结
| 编号 | 建议 |
|---|---|
| 1 | 用头文件表示接口,强调逻辑结构 |
| 2 | 在实现源文件中 #include 对应的头文件(让编译器检查一致性) |
| 3 | 不要在不同翻译单元中定义同名但含义不同的全局实体 |
| 4 | 头文件中避免非 inline 函数的定义 |
| 5 | #include 只放在全局作用域和命名空间内 |
| 6 | 只 #include 完整的声明 |
| 7 | 使用 include guards(#ifndef / #define / #endif) |
| 8 | 让头文件自包含(自己 #include 所需的依赖) |
| 9 | 区分用户接口和实现者接口 |
| 10 | 尽量避免需要运行时初始化的全局变量 |
第16章:类(Classes)
16.1 引言:为什么需要类?
类的本质:用代码表达现实世界的概念。
内置类型(语言提供):int, double, char, ...
用户定义类型(你来定义):Date, Point, Matrix, BankAccount, ...
好的类设计的核心原则:
把实现细节(数据怎么存储)和使用接口(有哪些操作)分开。
不用类时:
Date 用三个 int 表示(d, m, y)
每个程序员自己处理"2月有多少天"、"闰年怎么加"……
代码散落各处,难以维护,容易出错
用类时:
Date 类封装了所有细节
用户只需调用 add_day(1),不需要知道内部怎么实现
改变实现(比如换成"从1970年起的天数")不影响用户代码
16.2 类基础
16.2.1 成员函数
#include <iostream>
#include <string>
// 方式1:用 struct 加独立函数(数据和操作分离,脆弱)
struct Date_bad {
int d, m, y; // 数据暴露在外,任何人都能随意修改
};
// 操作函数是独立的,和 Date_bad 没有强制联系
void init_date(Date_bad& d, int day, int month, int year)
{
d.d = day; d.m = month; d.y = year;
}
// 方式2:用 struct 加成员函数(数据和操作绑定在一起)
struct Date {
int d, m, y;
// 成员函数:必须通过 Date 对象调用
void init(int dd, int mm, int yy);
void add_year(int n);
void add_month(int n);
void add_day(int n);
void print() const;
};
// 在类外定义成员函数:用 Date:: 指定它属于 Date
void Date::init(int dd, int mm, int yy)
{
// 在成员函数里,d/m/y 指的是"调用这个函数的那个对象"的成员
d = dd;
m = mm;
y = yy;
}
void Date::add_year(int n) { y += n; }
void Date::add_month(int n)
{
m += n;
while (m > 12) { m -= 12; ++y; } // 简化处理
while (m < 1) { m += 12; --y; }
}
void Date::add_day(int n) { d += n; /* 简化 */ }
void Date::print() const
{
std::cout << y << "-" << m << "-" << d << "\n";
}
int main()
{
Date today;
today.init(27, 5, 2026); // 用成员函数初始化
today.print(); // 2026-5-27
Date tomorrow = today; // 默认复制:逐成员复制
tomorrow.add_day(1);
tomorrow.print(); // 2026-5-28
return 0;
}
16.2.2 默认复制
#include <iostream>
struct Date {
int d, m, y;
void print() const { std::cout << y << "-" << m << "-" << d << "\n"; }
};
int main()
{
Date d1{25, 12, 2026}; // 初始化
// 默认复制:逐成员复制(memberwise copy)
Date d2 = d1; // 复制初始化:d2.d=25, d2.m=12, d2.y=2026
Date d3{d1}; // 等价写法
d2.d = 1; // 修改 d2 不影响 d1
d1.print(); // 2026-12-25(未变)
d2.print(); // 2026-12-1
// 默认赋值:也是逐成员复制
Date d4{1, 1, 2000};
d4 = d1; // d4 变为 d1 的副本
d4.print(); // 2026-12-25
// 注意:如果类管理资源(如动态内存),默认复制可能不够用
// 需要自定义复制构造函数和赋值运算符(见第17章)
return 0;
}
16.2.3 访问控制(Access Control)
#include <iostream>
#include <stdexcept>
// 使用 class(成员默认私有)来强制访问控制
class Date {
// private 区域:只有成员函数可以访问
int d, m, y;
public:
// public 区域:任何人都可以访问
// 构造函数(初始化)
void init(int dd, int mm, int yy)
{
// 可以在这里验证数据合法性
if (mm < 1 || mm > 12) throw std::invalid_argument("月份不合法");
if (dd < 1 || dd > 31) throw std::invalid_argument("日期不合法");
d = dd; m = mm; y = yy;
}
// 访问器(读取私有数据)
int day() const { return d; }
int month() const { return m; }
int year() const { return y; }
void add_year(int n) { y += n; }
void print() const
{
std::cout << y << "-" << m << "-" << d << "\n";
}
};
// 非成员函数:不能直接访问私有成员
void timewarp(Date& d)
{
// d.y -= 200; // 错误!y 是私有的
// 只能通过公有接口操作
}
int main()
{
Date dx;
// dx.m = 3; // 错误!m 是私有的
dx.init(25, 3, 2026); // OK:通过公有函数
dx.print();
// 访问控制的好处:
// 1. 任何非法 Date(如 2月30日)必然是成员函数的 bug
// 2. 修改内部表示只需修改成员函数,不影响用户代码
// 3. 用户只需了解公有接口
return 0;
}
16.2.4 class 与 struct 的区别
// class 和 struct 的唯一区别:默认访问权限
// struct:成员默认 public
struct S {
int x; // 默认 public
void f(); // 默认 public
};
// class:成员默认 private
class C {
int x; // 默认 private
void f(); // 默认 private
public:
void g();
};
// 这两个定义完全等价:
class Date1 {
int d, m, y; // private
public:
Date1(int dd, int mm, int yy);
};
struct Date2 {
private:
int d, m, y;
public:
Date2(int dd, int mm, int yy);
};
// 使用建议:
// struct → 简单数据聚合,没有真正的不变式
// class → 有不变式需要维护的"真正的类型"
int main() { return 0; }
16.2.5 构造函数(Constructors)
构造函数解决了"必须手动初始化、容易忘记"的问题。
#include <iostream>
#include <stdexcept>
#include <string>
// 假设有个今天的日期(实际中会从系统获取)
struct Today {
int d = 27, m = 5, y = 2026;
} today;
class Date {
int d, m, y;
public:
// 多个构造函数(重载):提供多种初始化方式
Date(int dd, int mm, int yy) : d{dd}, m{mm}, y{yy}
{
validate(); // 构造时验证
}
// 默认参数版本:用 0 表示"使用今天的值"
explicit Date(int dd = 0, int mm = 0, int yy = 0)
: d{ dd ? dd : today.d },
m{ mm ? mm : today.m },
y{ yy ? yy : today.y }
{
validate();
}
// 从字符串构造(演示多种构造方式)
explicit Date(const std::string& s)
{
// 简化:假设格式是 "YYYY-MM-DD"
y = std::stoi(s.substr(0, 4));
m = std::stoi(s.substr(5, 2));
d = std::stoi(s.substr(8, 2));
validate();
}
void print() const
{
std::cout << y << "-" << m << "-" << d << "\n";
}
private:
void validate() const
{
if (m < 1 || m > 12 || d < 1 || d > 31)
throw std::invalid_argument("非法日期");
}
};
int main()
{
// 不同的构造方式(使用 {} 初始化语法)
Date d1{27, 5, 2026}; // 完整指定
Date d2{1}; // 只指定日,月年用今天的值
Date d3{}; // 全部用今天的值
Date d4{"2026-05-27"}; // 从字符串构造
d1.print(); // 2026-5-27
d2.print(); // 2026-5-1
d3.print(); // 2026-5-27
d4.print(); // 2026-5-27
// Date d5; // 错误!如果有带参构造函数且没有默认构造,必须提供参数
return 0;
}
16.2.6 explicit 构造函数
#include <iostream>
#include <string>
// 演示为什么需要 explicit
class Date_implicit {
int d, m, y;
public:
// 没有 explicit:允许隐式转换
Date_implicit(int dd, int mm = 1, int yy = 2000)
: d{dd}, m{mm}, y{yy} {}
void print() const { std::cout << y << "-" << m << "-" << d << "\n"; }
};
class Date_explicit {
int d, m, y;
public:
// 有 explicit:禁止隐式转换
explicit Date_explicit(int dd, int mm = 1, int yy = 2000)
: d{dd}, m{mm}, y{yy} {}
void print() const { std::cout << y << "-" << m << "-" << d << "\n"; }
};
void process_implicit(Date_implicit d) { d.print(); }
void process_explicit(Date_explicit d) { d.print(); }
int main()
{
// === 隐式转换(没有 explicit)===
Date_implicit di1 = 15; // OK:int → Date_implicit(隐式转换,危险!)
Date_implicit di2{15}; // OK
process_implicit(15); // OK:15 被隐式转换为 Date_implicit{15}(可能意外!)
// === 显式转换(有 explicit)===
// Date_explicit de1 = 15; // 错误!= 初始化不做隐式转换
Date_explicit de2{15}; // OK:直接初始化(显式)
Date_explicit de3 = Date_explicit{15}; // OK:显式构造后复制
// process_explicit(15); // 错误!参数传递不做隐式转换
process_explicit(Date_explicit{15}); // OK:显式构造
di1.print();
de2.print();
return 0;
}
记忆规则:
单参数构造函数(或有默认参数可以单参数调用的)默认加
explicit。
只有在明确需要隐式转换(如complex<double>从double构造)时才省略。
16.2.7 类内初始化器(In-Class Initializers)
#include <iostream>
struct DefaultDate {
int d = 27, m = 5, y = 2026;
} today_val;
class Date {
// 成员变量在声明时指定默认值
// 构造函数没有显式初始化这些成员时,使用这里的默认值
int d {today_val.d}; // 默认:今天的日
int m {today_val.m}; // 默认:今天的月
int y {today_val.y}; // 默认:今天的年
public:
// 只指定 d,m 和 y 使用类内默认值
explicit Date(int dd) : d{dd} {}
// 指定 d 和 m,y 使用类内默认值
Date(int dd, int mm) : d{dd}, m{mm} {}
// 全部指定,覆盖类内默认值
Date(int dd, int mm, int yy) : d{dd}, m{mm}, y{yy} {}
// 不指定任何成员,全部使用类内默认值
Date() = default; // 等价于 Date() : d{today_val.d}, m{today_val.m}, y{today_val.y} {}
void print() const
{
std::cout << y << "-" << m << "-" << d << "\n";
}
};
int main()
{
Date d1{15}; // d=15, m=today, y=today
Date d2{15, 3}; // d=15, m=3, y=today
Date d3{15, 3, 2025}; // d=15, m=3, y=2025
Date d4{}; // d=today, m=today, y=today
d1.print();
d2.print();
d3.print();
d4.print();
return 0;
}
16.2.8 类内函数定义(inline 成员函数)
#include <iostream>
class Date {
int d, m, y;
public:
Date(int dd, int mm, int yy) : d{dd}, m{mm}, y{yy} {}
// 类内定义的成员函数自动是 inline
// inline 意味着:调用时可以直接展开,避免函数调用开销
int day() const { return d; } // inline
int month() const { return m; } // inline
int year() const { return y; } // inline
// 复杂的函数通常在类外定义(保持类的声明简洁)
void add_month(int n);
void print() const;
};
// 类外定义(需要显式加 inline 或不加——这里不加,是普通函数)
void Date::add_month(int n)
{
m += n;
// 处理月份溢出...
while (m > 12) { m -= 12; ++y; }
while (m < 1) { m += 12; --y; }
}
void Date::print() const
{
std::cout << y << "-" << m << "-" << d << "\n";
}
// 类外定义的 inline 版本(可以放在头文件里)
inline int Date::day() const { return d; } // 重复定义会报错,此处仅演示语法
int main()
{
Date d{15, 1, 2026};
std::cout << "年:" << d.year() << " 月:" << d.month() << " 日:" << d.day() << "\n";
d.add_month(2);
d.print();
return 0;
}
16.2.9 可变性(Mutability)
const 成员函数
#include <iostream>
class Date {
int d, m, y;
public:
Date(int dd, int mm, int yy) : d{dd}, m{mm}, y{yy} {}
// const 成员函数:承诺不修改对象
// 可以被 const 对象和非 const 对象调用
int day() const { return d; }
int month() const { return m; }
int year() const { return y; }
// 非 const 成员函数:可以修改对象
// 只能被非 const 对象调用
void add_year(int n) { y += n; }
// 错误示例(注释掉):
// int year() const { return ++y; } // 错误!const 函数里不能修改成员
};
void f(Date& d, const Date& cd)
{
int i = d.year(); // OK:非 const 对象可以调用 const 函数
d.add_year(1); // OK:非 const 对象可以调用非 const 函数
int j = cd.year(); // OK:const 对象可以调用 const 函数
// cd.add_year(1); // 错误!const 对象不能调用非 const 函数
}
int main()
{
Date d{1, 1, 2026};
const Date cd{25, 12, 2026};
f(d, cd);
std::cout << d.year() << "\n"; // 2027
return 0;
}
mutable:逻辑 const 下的物理修改
#include <iostream>
#include <string>
class Date {
int d, m, y;
// mutable:即使在 const 函数中也可以修改
mutable bool cache_valid = false;
mutable std::string cache;
void compute_cache() const
{
// 虽然这个函数是 const,但可以修改 mutable 成员
cache = std::to_string(y) + "-" +
std::to_string(m) + "-" +
std::to_string(d);
cache_valid = true;
}
public:
Date(int dd, int mm, int yy) : d{dd}, m{mm}, y{yy} {}
// 逻辑上是 const(不改变日期的值),但内部缓存了字符串
std::string string_rep() const
{
if (!cache_valid)
compute_cache(); // 第一次调用时计算,之后直接返回缓存
return cache;
}
};
int main()
{
const Date d{27, 5, 2026};
std::cout << d.string_rep() << "\n"; // 计算并缓存
std::cout << d.string_rep() << "\n"; // 直接返回缓存
return 0;
}
16.2.10 自引用(Self-Reference,this 指针)
#include <iostream>
class Date {
int d, m, y;
public:
Date(int dd, int mm, int yy) : d{dd}, m{mm}, y{yy} {}
// 返回 *this 的引用,支持链式调用
Date& add_year(int n)
{
y += n;
return *this; // this 是指向当前对象的指针,*this 是对象本身
}
Date& add_month(int n)
{
m += n;
while (m > 12) { m -= 12; ++y; }
while (m < 1) { m += 12; --y; }
return *this;
}
Date& add_day(int n)
{
d += n;
return *this;
}
void print() const
{
// 所有成员访问都隐含 this->
// this->d == d, this->m == m, this->y == y
std::cout << y << "-" << m << "-" << d << "\n";
}
};
int main()
{
Date d{27, 5, 2026};
// 链式调用:每个函数返回 *this(引用),可以继续调用
d.add_day(1).add_month(1).add_year(1);
// 等价于:d.add_day(1); d.add_month(1); d.add_year(1);
d.print(); // 2027-6-28
return 0;
}
this 指针的类型:
在非 const 成员函数中: this 的类型 = X ∗ \text{在非 const 成员函数中:} \quad \text{this 的类型} = X^* 在非 const 成员函数中:this 的类型=X∗
在 const 成员函数中: this 的类型 = const X ∗ \text{在 const 成员函数中:} \quad \text{this 的类型} = \text{const } X^* 在 const 成员函数中:this 的类型=const X∗
16.2.11 成员访问(. 和 ->)
#include <iostream>
struct Point {
int x, y;
void print() const { std::cout << "(" << x << "," << y << ")\n"; }
};
void demonstrate_access(Point p, Point* pp, const Point& rp)
{
// 对象:用 . (点) 访问
p.x = 1;
p.print();
// 指针:用 -> (箭头) 访问,等价于 (*pp).x
pp->x = 2;
(*pp).x = 2; // 与上一行等价
pp->print();
// 引用:用 . (点) 访问(引用像对象,不像指针)
int val = rp.x;
// 常见错误(注释掉):
// p->x = 3; // 错误:p 不是指针,不能用 ->
// pp.x = 3; // 错误:pp 是指针,不能用 .
}
int main()
{
Point p1{3, 4};
Point p2{5, 6};
demonstrate_access(p1, &p2, p1);
return 0;
}
16.2.12 静态成员(static Members)
#include <iostream>
#include <stdexcept>
class Date {
int d, m, y;
// static 数据成员:属于类,不属于某个对象
// 整个程序中只有一份(而不是每个对象各一份)
static Date default_date_val;
public:
explicit Date(int dd = 0, int mm = 0, int yy = 0)
: d{ dd ? dd : default_date_val.d },
m{ mm ? mm : default_date_val.m },
y{ yy ? yy : default_date_val.y }
{}
int day() const { return d; }
int month() const { return m; }
int year() const { return y; }
// static 成员函数:不依赖某个对象,没有 this 指针
static void set_default(int dd, int mm, int yy)
{
default_date_val = Date{dd, mm, yy};
}
void print() const
{
std::cout << y << "-" << m << "-" << d << "\n";
}
};
// static 数据成员的定义必须在类外(给它真正分配内存)
// 不重复 static 关键字
Date Date::default_date_val{1, 1, 1970}; // 初始默认值:1970年1月1日
int main()
{
Date d1{}; // 使用默认值:1970-1-1
d1.print();
// 修改默认值(通过类名调用 static 函数,不需要对象)
Date::set_default(27, 5, 2026);
Date d2{}; // 使用新的默认值:2026-5-27
d2.print();
// 也可以通过对象调用 static 函数(但不推荐,容易误解)
// d1.set_default(1, 1, 2000); // 合法但容易误导
return 0;
}
static 成员的本质:
class Date {
static Date default_date_val; // 声明:类里说"有这个东西"
};
Date Date::default_date_val{...}; // 定义:类外真正分配内存
// 普通成员变量:每个 Date 对象各有一份 d, m, y
// static 成员变量:所有 Date 对象共享一份 default_date_val
16.2.13 成员类型(Member Types)
#include <iostream>
#include <string>
template<typename T>
class Tree {
public:
// 成员类型别名(Associate Type):让外部代码知道 Tree 用什么类型
using value_type = T;
// 成员枚举:限定在 Tree 的作用域内
enum class Policy { rb_tree, splay_tree };
// 嵌套类:Node 只在 Tree 的上下文里有意义
class Node {
public:
value_type value;
Node* left = nullptr;
Node* right = nullptr;
// 嵌套类可以访问外围类的私有成员
void show() const { std::cout << value << " "; }
};
// Tree 的成员函数
void insert(const T& val)
{
Node* n = new Node{};
n->value = val;
// 简化:直接设为根
if (!root) root = n;
}
private:
Node* root = nullptr;
};
int main()
{
Tree<int> t;
t.insert(42);
// 通过类名访问成员类型
Tree<int>::value_type x = 100; // = int
Tree<int>::Policy p = Tree<int>::Policy::rb_tree;
// Tree<int>::Node* n = new Tree<int>::Node{};
std::cout << x << "\n";
return 0;
}
16.3 具体类(Concrete Classes)
具体类(Concrete Class)= 其表示(数据成员)是定义的一部分的类。
与抽象类(Abstract Class)的对比:
具体类:
- 表示在类定义里可见
- 可以放在栈上(不需要 new)
- 可以直接复制和移动
- 编译器知道大小,可以最优化
- 例:int, Date, complex<double>, string
抽象类:
- 只暴露接口,隐藏实现
- 通常通过指针/引用使用
- 支持多态(虚函数)
- 例:Shape(有 Circle、Rectangle 等具体子类)
16.3.1 完整的 Date 类实现
#include <iostream>
#include <stdexcept>
#include <string>
#include <sstream>
namespace Chrono {
// 用枚举类表示月份:避免 {6,7} vs {7,6} 的混淆
enum class Month {
jan=1, feb, mar, apr, may, jun,
jul, aug, sep, oct, nov, dec
};
// 前向声明
bool is_leapyear(int y);
bool is_date(int d, Month m, int y);
const class Date& default_date(); // 声明 default_date 函数
class Date {
public:
// 专用异常类:报告非法日期
class Bad_date : public std::exception {
std::string msg;
public:
explicit Bad_date(const std::string& m = "非法日期") : msg{m} {}
const char* what() const noexcept override { return msg.c_str(); }
};
// 构造函数:{} 表示"使用默认值"
explicit Date(int dd = {}, Month mm = {}, int yy = {});
// 读取函数(const:不修改对象)
int day() const { return d; }
Month month() const { return m; }
int year() const { return y; }
// 修改函数(返回引用支持链式调用)
Date& add_year(int n);
Date& add_month(int n);
Date& add_day(int n);
std::string to_string() const;
void print() const { std::cout << to_string() << "\n"; }
private:
bool is_valid() const { return is_date(d, m, y); }
int d; // 日
Month m; // 月
int y; // 年
};
// ===== 辅助函数(不是成员,放在 Chrono 命名空间里)=====
bool is_leapyear(int y)
{
return (y % 4 == 0 && y % 100 != 0) || (y % 400 == 0);
}
bool is_date(int d, Month m, int y)
{
if (y == 0) return false; // 没有公元0年
int ndays;
switch (m) {
case Month::feb:
ndays = 28 + (is_leapyear(y) ? 1 : 0);
break;
case Month::apr: case Month::jun:
case Month::sep: case Month::nov:
ndays = 30;
break;
case Month::jan: case Month::mar: case Month::may:
case Month::jul: case Month::aug: case Month::oct:
case Month::dec:
ndays = 31;
break;
default:
return false; // 非法月份
}
return 1 <= d && d <= ndays;
}
// 返回默认日期(静态局部变量:第一次调用时初始化)
const Date& default_date()
{
static Date d{1, Month::jan, 1970}; // 1970年1月1日(Unix 纪元)
return d;
}
// ===== 构造函数实现 =====
Date::Date(int dd, Month mm, int yy)
: d{dd}, m{mm}, y{yy}
{
// 用 0({})表示"使用默认值"
if (y == 0) y = default_date().year();
if (m == Month{}) m = default_date().month();
if (d == 0) d = default_date().day();
if (!is_valid())
throw Bad_date("日期非法: " + to_string());
}
// ===== 修改函数实现 =====
Date& Date::add_year(int n)
{
// 特殊情况:2月29日加年份
if (d == 29 && m == Month::feb && !is_leapyear(y + n)) {
d = 1; m = Month::mar; // 跳到3月1日
}
y += n;
return *this;
}
Date& Date::add_month(int n)
{
if (n == 0) return *this;
int mm = static_cast<int>(m) + n;
int delta_y = 0;
while (mm > 12) { mm -= 12; ++delta_y; }
while (mm < 1) { mm += 12; --delta_y; }
y += delta_y;
m = static_cast<Month>(mm);
// 如果当前 d 超过了新月份的最大天数,截断到月末
// (简化处理)
return *this;
}
Date& Date::add_day(int n)
{
// 简化实现(完整版需要处理月份和年份进位)
d += n;
return *this;
}
std::string Date::to_string() const
{
std::ostringstream os;
os << y << "-"
<< static_cast<int>(m) << "-"
<< d;
return os.str();
}
// ===== 比较运算符 =====
bool operator==(const Date& a, const Date& b)
{
return a.day() == b.day() &&
a.month() == b.month() &&
a.year() == b.year();
}
bool operator!=(const Date& a, const Date& b)
{
return !(a == b);
}
bool operator<(const Date& a, const Date& b)
{
if (a.year() != b.year()) return a.year() < b.year();
if (a.month() != b.month()) return a.month() < b.month();
return a.day() < b.day();
}
// 前置递增:加一天
Date& operator++(Date& d) { return d.add_day(1); }
// 加 n 天
Date operator+(Date d, int n) { return d.add_day(n); }
// 输出运算符
std::ostream& operator<<(std::ostream& os, const Date& d)
{
return os << d.to_string();
}
} // namespace Chrono
// ===== 测试 =====
int main()
{
using namespace Chrono;
try {
Date d1{27, Month::may, 2026};
std::cout << "d1 = " << d1 << "\n";
// 链式操作
d1.add_day(1).add_month(1).add_year(1);
std::cout << "加 1天1月1年后: " << d1 << "\n";
// 默认值
Date d2{}; // 使用 1970-1-1
std::cout << "d2 (默认) = " << d2 << "\n";
// 比较
Date d3{27, Month::may, 2026};
Date d4{1, Month::jan, 2026};
std::cout << "d3 < d4? " << (d3 < d4 ? "是" : "否") << "\n";
// 非法日期会抛出异常
// Date bad{30, Month::feb, 2026}; // 会抛出 Bad_date
}
catch (const Chrono::Date::Bad_date& e) {
std::cout << "捕获异常: " << e.what() << "\n";
}
return 0;
}
16.3.2 辅助函数
辅助函数是与类相关但不需要直接访问私有数据的函数。
原则:
如果一个函数不需要访问类的私有成员,就不要让它成为成员函数
→ 减少需要了解类内部实现的函数数量
→ 类的表示改变时,需要修改的代码更少
// 辅助函数放在同一命名空间(Chrono)里,通过 ADL 关联到 Date
namespace Chrono {
// 计算两个日期之间的天数差(不需要访问私有成员)
int diff(const Date& a, const Date& b)
{
// 使用公有接口
int days_a = a.year() * 365 + static_cast<int>(a.month()) * 30 + a.day();
int days_b = b.year() * 365 + static_cast<int>(b.month()) * 30 + b.day();
return days_a - days_b;
}
}
16.3.3 运算符重载
// 在 Chrono 命名空间里定义这些运算符(见上面完整示例)
// 让 Date 的使用像内置类型一样自然
// 使用示例:
// Date d{27, Month::may, 2026};
// ++d; // 前置 ++,调用 operator++(d)
// Date d2 = d + 5; // 加 5 天
// if (d == d2) ... // 比较
// cout << d << "\n";// 输出
16.3.4 具体类的意义
具体类的设计哲学:
做一件事,做好,做高效
不需要虚函数和运行时多态
可以放在栈上,可以直接复制
使用建议:
优先用具体类(最简单)
需要多态时用抽象类和类层次
不要为了"可扩展性"而过度设计
具体类的性能优势:
Date d{27, 5, 2026}; // 栈上分配,没有 new/delete 开销
Date d2 = d; // 直接复制,没有虚函数表查找
d.day(); // 内联调用,编译器直接展开,零开销
16.4 本章建议总结
| 编号 | 建议 |
|---|---|
| 1 | 用类表达概念(Concept) |
| 2 | 把类的接口与实现分离 |
| 3 | 只有真正是"纯数据聚合"时才用 struct(无不变式) |
| 4 | 定义构造函数处理对象初始化 |
| 5 | 默认情况下,单参数构造函数加 explicit |
| 6 | 不修改对象状态的成员函数加 const |
| 7 | 简单常用的类型优先用具体类 |
| 8 | 只有需要直接访问表示的函数才做成员函数 |
| 9 | 用命名空间把类和其辅助函数关联起来 |
| 10 | 需要访问表示但不依赖特定对象的函数做成 static 成员 |
附录:两章核心概念速查
程序的编译链接流程
源文件(.cpp)
|
| 预处理器(展开宏、处理#include)
↓
翻译单元
|
| 编译器
↓
目标文件(.o)
|
| 链接器
↓
可执行文件
头文件组织原则
头文件(.h):放声明
类定义、函数声明、extern变量声明
const/constexpr定义、inline函数定义
typedef/using别名
源文件(.cpp):放定义
函数体、变量定义(分配内存)
static成员变量定义
类成员的访问级别
class X {
private: // 只有成员函数(和友元)可访问
int data;
protected: // 成员函数和派生类可访问(见第20章)
int pdata;
public: // 任何人都可访问
void f(); // 接口
};
const 成员函数规则
const对象 → 只能调用 const 成员函数 \text{const对象} \to \text{只能调用} \text{ const } \text{成员函数} const对象→只能调用 const 成员函数
非const对象 → 可以调用任意成员函数 \text{非const对象} \to \text{可以调用任意成员函数} 非const对象→可以调用任意成员函数
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)