核心主题:运算符细节、动态内存管理、Lambda 表达式、类型转换、函数的方方面面。

目录

  1. 第11章:选择操作
    • 1.1 逻辑运算符与位运算符
    • 1.2 条件表达式与自增自减
    • 1.3 自由存储(堆内存)
    • 1.4 列表初始化
    • 1.5 Lambda 表达式
    • 1.6 显式类型转换
  2. 第12章:函数
    • 2.1 函数声明的组成部分
    • 2.2 返回值
    • 2.3 inline 与 constexpr 函数
    • 2.4 参数传递
    • 2.5 函数重载
    • 2.6 前置条件与后置条件
    • 2.7 函数指针
    • 2.8 宏
  3. 建议总结

第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;             ← 手动释放

三大内存问题

堆内存的三大问题

内存泄漏
new 了但忘记 delete

提前释放
delete 后还用指针

重复释放
同一块内存 delete 两次

程序越跑越慢
最终耗尽内存

悬空指针
读到垃圾数据或崩溃

内存管理器损坏
不可预测的崩溃

#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:[]{})

[capture]

[] 不捕获任何局部变量

[&] 全部按引用捕获

[=] 全部按值捕获(拷贝)

[x, &y] x按值 y按引用

[&, x] x按值 其他按引用

[=, &x] x按引用 其他按值

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 显式类型转换

转换方法的安全性从高到低

显式类型转换

T{e}
构造语法(最安全)
禁止窄化

具名转换

(T)e C风格转换
(危险,避免使用)

T(e) 函数风格
(和C风格一样危险)

static_cast
相关类型间转换

reinterpret_cast
无关类型(位模式重解释)

const_cast
去除/添加 const

dynamic_cast
运行时检查的类层级转换

#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 异常的基本机制

异常的工作方式:

调用

成功

失败

异常向上传播

处理完毕

调用者 taskmaster

被调用者 do_task

返回结果

throw Some_error

catch Some_error 处理

程序继续

对应代码:

#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 分层错误处理

错误处理应该分层:低层处理自己能处理的,处理不了的往上抛。

catch 顶层错误

catch 业务错误

throw 底层错误

throw 业务错误

应用层 main

业务模块

工具库

原则:

  • 每个函数只处理它能合理处理的错误
  • 其余的往上抛
  • 不要让每个函数都变成"防火墙"——这样重复检查太多,性能差,代码乱
    跨语言边界的转换(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?

  1. 编译器优化:不需要生成异常处理代码,更快
  2. 语义清晰:告诉调用方"放心,不会抛"
  3. 某些标准库操作(如 std::vector 移动元素时)只在移动构造函数是 noexcept 时才会移动(否则复制),影响性能

13.5.2 捕获异常(catch)

捕获规则

异常 Ecatch(H) 捕获的条件:
H = E 或 H  是  E  的公有基类 或 H  是引用/指针且满足前两条 H = E \quad \text{或} \quad H \text{ 是 } E \text{ 的公有基类} \quad \text{或} \quad H \text{ 是引用/指针且满足前两条} H=EH  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 命名空间基础

基本语法

全局作用域 ::

Graph_lib::Line
Graph_lib::Text

Text_lib::Line
Text_lib::Text

其他全局名字

#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 模块化与接口

计算器程序的模块结构

实现

实现

实现

Driver 驱动层
main, calculate

Parser 接口

Error 接口

Lexer 接口

Table 接口

Parser 实现

Lexer 实现

Table 实现

实线 = 使用关系,虚线 = 实现关系

用命名空间表达模块

#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

附录:两章核心概念速查

异常处理流程

调用

不能

throw; 重抛

正常结束

调用函数

被调用函数
能完成任务?

正常返回

throw exception_obj

调用栈上有
合适的 catch?

执行 catch 块
处理错误

std::terminate
程序终止

程序继续

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",编译器能保证它们对 Sf 的理解一致。
头文件中可以放什么:

可以放 示例
命名空间定义 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     ← 主程序

dc.h
所有声明

lexer.cpp

parser.cpp

table.cpp

error.cpp

main.cpp

标准库头文件
map / string / iostream

关键点: 每个 .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
(用户接口)
double expr(bool);

parser_impl.h
(实现者接口)
double prim, term, expr

parser.cpp
(实现)

lexer.h

error.h

table.h

main.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 程序终止

程序有多种终止方式,行为各不相同:

程序终止

正常终止

异常终止

return from main()
调用静态对象析构函数

exit()
调用静态对象析构函数

quick_exit()
不调用析构函数

abort()
不调用析构函数,立即终止

未捕获的异常
调用 terminate()

违反 noexcept
调用 terminate()

#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对象可以调用任意成员函数

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐