教材:《C++ Programming Language》第4版
目标:从零开始理解,代码含详细注释

第17章:构造、清理、拷贝与移动

17.1 概述:对象的"一生"

一个对象从创建到销毁,经历以下几个阶段:

构造(Constructor) → 使用 → 拷贝/移动 → 析构(Destructor)

用一个具体例子来说明这几个阶段都发生了什么:

#include <iostream>
#include <string>
using std::string;
// 演示函数:接收字符串参数(按值传递 = 拷贝),返回它(移动)
string ident(string arg) {
    return arg;  // 返回时会触发"移动构造",把 arg 的内容搬出去
}
int main() {
    // 1. 普通构造:用字符串字面量初始化 s1
    string s1 {"Adams"};
    // 2. 拷贝构造(把 s1 拷贝进 ident 的参数 arg)
    //    + 移动赋值(把 ident 返回的临时对象移动赋值给 s1)
    s1 = ident(s1);
    // 结果:s1 仍然是 "Adams"
    // 3. 普通构造 s2
    string s2 {"Pratchett"};
    // 4. 拷贝赋值:把 s2 的内容拷贝到 s1
    s1 = s2;
    // 结果:s1 和 s2 都是 "Pratchett"
    // 5. 退出 main 时,s1 和 s2 的析构函数被自动调用
    return 0;
}

拷贝 vs 移动的关键区别:

  • 拷贝:操作后两个对象都拥有相同的值(源对象不变)
  • 移动:操作后目标对象拥有原来的值,源对象变为"空/未定义"状态
    拷贝: a → b ⇒ a = 原值 ,    b = 原值 \text{拷贝}:a \rightarrow b \quad \Rightarrow \quad a = \text{原值},\; b = \text{原值} 拷贝aba=原值,b=原值
    移动: a → b ⇒ b = 原值 ,    a = 不确定(通常为空) \text{移动}:a \rightarrow b \quad \Rightarrow \quad b = \text{原值},\; a = \text{不确定(通常为空)} 移动abb=原值,a=不确定(通常为空)

17.2 构造函数与析构函数

17.2.1 构造函数与类不变式

类不变式(Class Invariant):一组在类的所有成员函数执行前后都必须成立的条件。
构造函数的职责就是建立这个不变式:

#include <stdexcept>
class Vector {
public:
    // 构造函数:建立不变式
    // 不变式1:elem 指向一个含 sz 个 double 的数组
    // 不变式2:sz >= 0
    Vector(int s) {
        if (s < 0)
            throw std::bad_alloc(); // 无法建立不变式,抛出异常
        sz   = s;
        elem = new double[s]; // 申请堆内存
    }
private:
    double* elem; // 指向数组
    int     sz;   // 元素数量,非负
};

为什么要定义不变式?

  • 让设计有明确目标
  • 简化成员函数的实现(每个函数假设不变式已成立)
  • 便于文档化
17.2.2 析构函数与资源管理(RAII)

RAII(Resource Acquisition Is Initialization,资源获取即初始化):

构造时获取资源,析构时释放资源。

#include <iostream>
class Vector {
public:
    // 构造函数:获取资源(堆内存)
    Vector(int s) : elem{new double[s]}, sz{s} {}
    // 析构函数:释放资源
    ~Vector() {
        delete[] elem; // 归还堆内存
    }
private:
    double* elem;
    int     sz;
};
void demo() {
    Vector v1(5); // 构造:在堆上分配 5 个 double
    // ... 使用 v1 ...
} // 离开作用域时,v1 的析构函数自动被调用,内存被释放
17.2.3 构造与析构的顺序
构造顺序(从底向上):
  1. 基类构造函数
  2. 成员构造函数(按声明顺序)
  3. 自身构造函数体
析构顺序(从顶向下,反序):
  1. 自身析构函数体
  2. 成员析构函数(逆序)
  3. 基类析构函数

用 ASCII 图示意:

构造:
  基类 B ──► 成员 m1 ──► 成员 m2 ──► 派生类 D 的函数体
  ──────────────────────────────────────────────────►
析构:
  派生类 D 的函数体 ──► 成员 m2 ──► 成员 m1 ──► 基类 B
  ──────────────────────────────────────────────────►
17.2.4 显式调用构造/析构函数

通常不需要也不应该显式调用析构函数。但有一个例外:容器需要在特定地址构造/析构元素。

#include <new> // 需要这个头文件来使用 placement new
class MyContainer {
    char* memory; // 原始内存池
public:
    // 在指定地址 p 构造一个 X 对象(Placement new)
    void push_back_at(int* p, int val) {
        new(p) int(val); // 在地址 p 处原地构造
    }
    // 显式调用析构函数(只在极少数情况下使用!)
    void destroy_at(int* p) {
        p->~int(); // 调用 int 的"析构"(对内置类型无实际效果)
    }
};
17.2.5 虚析构函数

规则:如果一个类有虚函数,它的析构函数也应该是虚函数。

#include <iostream>
class Shape {
public:
    virtual void draw() = 0;  // 纯虚函数
    virtual ~Shape() {        // 虚析构函数!
        std::cout << "Shape 被析构\n";
    }
};
class Circle : public Shape {
public:
    void draw() override {
        std::cout << "画圆\n";
    }
    ~Circle() override {
        std::cout << "Circle 被析构\n";
    }
};
int main() {
    Shape* p = new Circle(); // 基类指针指向派生类对象
    p->draw();
    delete p; // 如果 ~Shape() 不是虚函数,只会调用 ~Shape(),导致内存泄漏!
              // 因为是虚函数,会正确调用 ~Circle() 再调用 ~Shape()
    return 0;
}

17.3 类对象的初始化

17.3.1 没有构造函数时的初始化

没有用户定义构造函数的类(聚合类/结构体)可以用成员逐一初始化:

struct Work {
    std::string author;
    std::string name;
    int         year;
};
Work beethoven {
    "Beethoven",
    "Symphony No. 9",
    1824
}; // 成员逐一初始化
Work copy {beethoven}; // 拷贝初始化
Work empty {};         // 默认初始化:{"", "", 0}

注意:静态变量用 {} 默认初始化,局部变量的内置类型成员不初始化(留垃圾值)。

17.3.2 使用构造函数初始化
struct X {
    X(int); // 声明了构造函数,必须提供 int
};
// X x0;        // 错误:没有默认构造函数
// X x1 {};     // 错误:空初始化列表也不行
X x2 {2};       // OK
// X x3 {"two"};// 错误:类型不匹配
// X x4 {1, 2}; // 错误:参数个数不对
X x5 {x2};      // OK:拷贝构造

为什么推荐 {} 而不是 ()=

  • {}通用的(universal):在任何地方都能用
  • {} 禁止窄化转换(如 int 隐式转为 char 丢失精度会报错)
  • () 在某些地方会被解析为函数声明(经典陷阱)
int a();   // 这其实是函数声明,不是变量!
int b {};  // 这才是变量,值为 0
17.3.3 默认构造函数

默认构造函数:可以不提供任何参数就调用的构造函数。

class Vector {
public:
    Vector() {}  // 默认构造函数:创建空向量
};
Vector v1;   // OK:用默认构造函数
Vector v2 {}; // OK:同上

什么时候该有默认构造函数?
只有当类型存在"自然的默认值"时:

  • string → 空字符串 ""
  • vector → 空容器 {}
  • 数值类型 → 零
    如果类型没有自然默认值(比如"日期"),就不要强行添加默认构造函数。
17.3.4 initializer_list 构造函数

接受 std::initializer_list<T> 参数的构造函数,允许用花括号列表初始化:

#include <iostream>
#include <initializer_list>
#include <vector>
#include <algorithm> // for std::uninitialized_copy
// 自定义一个支持列表初始化的容器(简化版)
template<class E>
class MyVec {
public:
    // initializer_list 构造函数
    MyVec(std::initializer_list<E> lst)
        : sz{(int)lst.size()}, elem{new E[lst.size()]}
    {
        // 遍历 initializer_list,将元素复制到数组
        int i = 0;
        for (auto& x : lst)
            elem[i++] = x;
    }
    ~MyVec() { delete[] elem; }
    int size() const { return sz; }
    E   operator[](int i) const { return elem[i]; }
private:
    int sz;
    E*  elem;
};
int main() {
    MyVec<int> v {1, 2, 3, 4, 5}; // 触发 initializer_list 构造
    for (int i = 0; i < v.size(); ++i)
        std::cout << v[i] << " "; // 输出:1 2 3 4 5
    std::cout << "\n";
    return 0;
}

消歧义规则(当多个构造函数都匹配时):

规则1:优先使用默认构造函数(而非 initializer_list 构造函数),当列表为空时
规则2:优先使用 initializer_list 构造函数,而非普通构造函数
#include <vector>
std::vector<int> v1 {77};   // 1 个元素,值为 77(initializer_list 优先)
std::vector<int> v2(77);    // 77 个元素,每个值为 0(普通构造函数)

17.4 成员与基类的初始化

17.4.1 成员初始化列表

在构造函数体执行之前,通过初始化列表直接构造成员:

#include <string>
#include <vector>
struct Date { int y, m, d; };
class Club {
    std::string              name;
    std::vector<std::string> members;
    std::vector<std::string> officers;
    Date                     founded;
public:
    // 成员初始化列表:用冒号开头,逗号分隔
    Club(const std::string& n, Date fd)
        : name{n},         // 直接构造 name
          members{},       // 构造空 vector
          officers{},      // 构造空 vector
          founded{fd}      // 直接构造 founded
    {
        // 此处 name, members, officers, founded 已经构造完毕
    }
};

成员初始化 vs 赋值的区别:

class Person {
    std::string name;
    std::string address;
public:
    Person(const std::string& n, const std::string& a)
        : name{n}        // 方式1:直接用 n 构造 name(高效)
    {
        address = a;     // 方式2:先默认构造 address="",再赋值(低效,多一次操作)
    }
};

成员的初始化顺序按声明顺序,不是初始化列表的顺序!

17.4.2 委托构造函数

一个构造函数调用同类的另一个构造函数,避免重复代码:

#include <string>
#include <stdexcept>
class X {
    int a;
    static const int max = 100;
public:
    // 主构造函数:负责验证逻辑
    X(int x) {
        if (0 < x && x <= max)
            a = x;
        else
            throw std::out_of_range("X out of range");
    }
    // 委托给主构造函数,提供默认值 42
    X() : X{42} {}
    // 委托给主构造函数,先把字符串转成整数
    X(const std::string& s) : X{std::stoi(s)} {}
};

注意:委托构造函数不能同时显式初始化成员。

17.4.4 类内初始化器

直接在类声明中给成员一个默认值:

// 有多个构造函数时,可以把"通用默认值"放在类内
class A {
public:
    A() {}                    // a=7, b=5, algorithm="MD5"(由类内初始化器提供)
    A(int a_val) : a{a_val} {} // a=a_val, b=5, algorithm="MD5"
    A(double d) : b{(int)d} {} // a=7, b=d, algorithm="MD5"
private:
    int         a         {7};      // 类内初始化器
    int         b         {5};
    std::string algorithm {"MD5"};  // 所有构造函数都用这个默认值
    std::string state     {"Constructor run"};
};

17.5 拷贝与移动

17.5.1 拷贝

拷贝由两个操作定义:

  • 拷贝构造函数X(const X&)
  • 拷贝赋值运算符X& operator=(const X&)
    以一个二维矩阵为例:
#include <array>
#include <algorithm>   // for std::copy, std::uninitialized_copy
#include <stdexcept>
template<class T>
class Matrix {
    std::array<int, 2> dim;  // 两个维度
    T*                 elem; // 指向元素数组
public:
    // 普通构造函数
    Matrix(int d1, int d2)
        : dim{d1, d2}, elem{new T[d1 * d2]} {}
    int size() const { return dim[0] * dim[1]; }
    // 析构函数:释放内存
    ~Matrix() { delete[] elem; }
    // ---- 拷贝构造函数 ----
    Matrix(const Matrix& m)
        : dim{m.dim},
          elem{new T[m.size()]} // 新分配一块内存
    {
        // 把 m 的元素逐个复制到新内存
        std::uninitialized_copy(m.elem, m.elem + m.size(), elem);
    }
    // ---- 拷贝赋值运算符 ----
    Matrix& operator=(const Matrix& m) {
        if (dim[0] != m.dim[0] || dim[1] != m.dim[1])
            throw std::runtime_error("Matrix 尺寸不匹配");
        // 使用"拷贝后交换"技巧,保证异常安全
        Matrix tmp{m};               // 先拷贝到临时对象(可能抛出异常)
        std::swap(dim,  tmp.dim);    // 再交换(不会抛出异常)
        std::swap(elem, tmp.elem);
        return *this;
        // tmp 被销毁,原来 *this 的内存被 tmp 的析构函数释放
    }
};

拷贝的两个要求:
等价性(Equivalence): x = y ⇒ x = = y  且  f ( x ) = = f ( y ) \text{等价性(Equivalence)}:x = y \Rightarrow x == y \text{ 且 } f(x) == f(y) 等价性(Equivalencex=yx==y  f(x)==f(y)
独立性(Independence): x = y  后,修改  x  不影响  y \text{独立性(Independence)}:x = y \text{ 后,修改 } x \text{ 不影响 } y 独立性(Independencex=y 后,修改 x 不影响 y
浅拷贝的危险(浅拷贝 vs 深拷贝):

浅拷贝(默认指针行为):
  x: [ ptr ]──►[ 共享数据 ]◄──[ ptr ] :y
  修改 x 的数据 = 修改 y 的数据(纠缠!)
深拷贝:
  x: [ ptr ]──►[ 数据副本A ]
  y: [ ptr ]──►[ 数据副本B ]
  完全独立

切片(Slicing)问题:

struct Base   { int b; };
struct Derived : Base { int d; };
void bad(Base* p) {
    Base b2 = *p; // 危险:如果 p 指向 Derived 对象
                  // b2 只拷贝了 Base 部分,d 被"切掉"了!
}

防止切片:

  1. 删除基类的拷贝操作(= delete
  2. 将基类设为私有基类
17.5.2 移动

移动语义:避免不必要的拷贝,直接"搬走"资源。

#include <array>
#include <algorithm> // for std::swap
template<class T>
class Matrix {
    std::array<int, 2> dim;
    T*                 elem;
public:
    Matrix(int d1, int d2)
        : dim{d1, d2}, elem{new T[d1 * d2]} {}
    ~Matrix() { delete[] elem; }
    // ---- 移动构造函数 ----
    // 参数是右值引用 (&&):表示"这个对象马上就没用了,可以偷它的资源"
    Matrix(Matrix&& a)
        : dim{a.dim},    // 复制维度信息(便宜)
          elem{a.elem}   // 直接接管 a 的指针(不分配新内存!)
    {
        a.dim  = {0, 0}; // 让 a 变成"空"状态
        a.elem = nullptr; // 防止 a 析构时 double-free
    }
    // ---- 移动赋值运算符 ----
    Matrix& operator=(Matrix&& a) {
        // 用 swap 实现:把 a 的内容换给 *this,
        // 然后 a 持有旧的 *this 的内容,
        // 当 a 销毁时,旧内容被自动释放
        std::swap(dim,  a.dim);
        std::swap(elem, a.elem);
        return *this;
    }
};
// std::move() 的作用:把一个左值"假装"成右值,允许移动它
template<class T>
void my_swap(T& a, T& b) {
    T tmp = std::move(a); // 移动构造 tmp,a 变为空
    a     = std::move(b); // 移动赋值,b 变为空
    b     = std::move(tmp); // 移动赋值,tmp 变为空
    // 三次移动,没有任何深拷贝!
}

移动 vs 拷贝对比:

拷贝 swap(慢):
  tmp = a   →  申请内存,复制所有数据
  a = b     →  申请内存,复制所有数据
  b = tmp   →  申请内存,复制所有数据
  共 3 次内存分配 + 大量数据复制
移动 swap(快):
  tmp = move(a) → 仅交换指针,O(1)
  a   = move(b) → 仅交换指针,O(1)
  b   = move(tmp)→ 仅交换指针,O(1)
  共 0 次内存分配!

17.6 默认操作的生成

编译器默认为类生成以下6种操作:

操作 声明形式 生成条件
默认构造函数 X() 没有用户声明任何构造函数
拷贝构造函数 X(const X&) 没有用户声明拷贝/移动/析构
拷贝赋值 X& operator=(const X&) 同上
移动构造函数 X(X&&) 没有用户声明拷贝/移动/析构
移动赋值 X& operator=(X&&) 同上
析构函数 ~X() 没有用户声明析构函数

抑制规则(重要!)

  • 声明了任何构造函数 → 默认构造函数不再生成
  • 声明了拷贝/移动/析构 → 拷贝和移动操作都不再生成
17.6.1 显式指定默认操作
class MyClass {
public:
    MyClass()                           = default; // 显式要求编译器生成默认版本
    ~MyClass()                          = default;
    MyClass(const MyClass&)             = default; // 默认拷贝构造
    MyClass(MyClass&&)                  = default; // 默认移动构造
    MyClass& operator=(const MyClass&)  = default; // 默认拷贝赋值
    MyClass& operator=(MyClass&&)       = default; // 默认移动赋值
};
17.6.4 删除函数

= delete 明确禁止某个操作:

class Base {
public:
    // 禁止拷贝(防止切片问题)
    Base(const Base&)            = delete;
    Base& operator=(const Base&) = delete;
    // 禁止移动
    Base(Base&&)                 = delete;
    Base& operator=(Base&&)      = delete;
};
// Base x1;
// Base x2{x1}; // 编译错误:拷贝构造被删除

也可以删除特定的重载,禁止不想要的隐式转换:

struct Z {
    Z(double);       // 允许从 double 构造
    Z(int) = delete; // 禁止从 int 构造(防止 1 被当成 double 偷偷转换)
};
// Z z1{1};    // 错误:Z(int) 被删除
Z z2{1.0};    // OK:使用 Z(double)

第18章:运算符重载

18.1 为什么要重载运算符?

自然语言表达:
x + y × z 比 “将 y 乘以 z 再加上 x” 更直观 x + y \times z \quad \text{比} \quad \text{``将 y 乘以 z 再加上 x''} \quad \text{更直观} x+y×z y 乘以 z 再加上 x”更直观
C++ 允许为用户定义类型(类)定义运算符的含义,这叫运算符重载

18.2 运算符函数

可以重载的运算符:

算术:   +  -  *  /  %
位操作: ^  &  |  ~
逻辑:   !  &&  ||
比较:   ==  !=  <  >  <=  >=
赋值:   =  +=  -=  *=  /=  %=  ^=  &=  |=
移位:   <<  >>  >>=  <<=
其他:   ++  --  ->  []  ()  new  delete  new[]  delete[]  ->*  ,

不能重载的运算符:

::   .   .*   ?:   sizeof   alignof   typeid
18.2.1 二元与一元运算符

二元运算符(两个操作数)的实现方式:

// 方式1:成员函数(左操作数是 this)
class X {
public:
    X operator+(const X& rhs); // a + b → a.operator+(b)
};
// 方式2:非成员函数(两个参数都显式写出)
X operator+(const X& lhs, const X& rhs); // a + b → operator+(a, b)

规则:以下运算符必须是成员函数:

  • =(赋值)
  • [](下标)
  • ()(函数调用)
  • ->(成员访问)
18.2.3 运算符与用户定义类型

规则:运算符函数至少要有一个参数是用户定义类型(类/枚举),否则无法定义。
这防止了"修改内置类型行为"的危险操作。

18.3 复数类型的完整实现

以实现一个复数类为例,展示运算符重载的各种用法:

18.3.1 成员运算符 vs 非成员运算符

设计原则

  • 需要修改自身(如 +=)→ 用成员函数
  • 只是产生新值(如 +)→ 用非成员函数,调用成员函数实现
#include <iostream>
#include <cmath>    // sqrt, cos, sin, atan2
#include <sstream>  // ostringstream
class complex {
    double re, im; // 实部和虚部
public:
    // ---- 构造函数 ----
    // constexpr 允许编译期计算
    constexpr complex(double r = 0, double i = 0)
        : re{r}, im{i} {}
    // ---- 访问器 ----
    constexpr double real() const { return re; }
    constexpr double imag() const { return im; }
    void real(double r) { re = r; }
    void imag(double i) { im = i; }
    // ---- 成员运算符(修改自身) ----
    complex& operator+=(complex a) {
        re += a.re;
        im += a.im;
        return *this; // 返回自身引用,支持链式操作
    }
    complex& operator+=(double a) {
        re += a;      // 只修改实部
        return *this;
    }
    complex& operator*=(complex a) {
        // 复数乘法:(a+bi)(c+di) = (ac-bd) + (ad+bc)i
        double new_re = re * a.re - im * a.im;
        double new_im = re * a.im + im * a.re;
        re = new_re;
        im = new_im;
        return *this;
    }
    complex& operator-=(complex a) {
        re -= a.re;
        im -= a.im;
        return *this;
    }
};
// ---- 非成员运算符(产生新值,通过调用成员运算符实现) ----
// 二元 + 的三个版本(混合算术)
inline complex operator+(complex a, complex b) {
    return a += b; // 传值参数,直接修改副本并返回
}
inline complex operator+(complex a, double b) {
    return a += b;
}
inline complex operator+(double a, complex b) {
    return complex{a + b.real(), b.imag()};
}
// 二元 -
inline complex operator-(complex a, complex b) {
    return a -= b;
}
// 一元负号
inline complex operator-(complex a) {
    return {-a.real(), -a.imag()};
}
// 一元正号
inline complex operator+(complex a) {
    return a;
}
// 相等比较
inline bool operator==(complex a, complex b) {
    return a.real() == b.real() && a.imag() == b.imag();
}
inline bool operator!=(complex a, complex b) {
    return !(a == b);
}
// 输出运算符
std::ostream& operator<<(std::ostream& os, complex c) {
    os << "(" << c.real();
    if (c.imag() >= 0) os << "+";
    os << c.imag() << "i)";
    return os;
}
// 求模(绝对值)
inline double abs(complex c) {
    return std::sqrt(c.real() * c.real() + c.imag() * c.imag());
}
int main() {
    complex a{1, 2};       // 1 + 2i
    complex b{3, 4};       // 3 + 4i
    std::cout << "a = " << a << "\n"; // 输出:(1+2i)
    std::cout << "b = " << b << "\n"; // 输出:(3+4i)
    std::cout << "a+b = " << a + b << "\n"; // (4+6i)
    std::cout << "a*b = " << a * b << "\n"; // ???
    // a*b = (1+2i)(3+4i) = (3-8) + (4+6)i = -5+10i
    std::cout << "|b| = " << abs(b) << "\n"; // 5
    complex c{2.0};  // 用 double 构造:2 + 0i
    std::cout << "2+a = " << 2 + a << "\n"; // 3+2i
    std::cout << "a+2 = " << a + 2 << "\n"; // 3+2i
    return 0;
}
18.3.3 隐式转换

构造函数接受单个参数时,定义了一种隐式转换:

class complex {
public:
    complex(double r = 0, double i = 0) : re{r}, im{i} {}
    // ...
};
complex c = 3.14;  // 自动调用 complex(3.14),即 3.14+0i

运算符查找顺序:
当执行 3 == cccomplex)时:

  1. 编译器找到 operator==(complex, complex)
  2. complex(3) 隐式把 3 转换为 complex
  3. 调用 operator==(complex{3}, c)

18.4 类型转换运算符

除了通过构造函数实现"其他类型 → 本类型"的转换,还可以通过转换运算符实现"本类型 → 其他类型"的转换:

#include <iostream>
#include <stdexcept>
// 一个只能存 0-63 的 6 位非负整数
class Tiny {
    char v; // 实际存储,范围 0~63(6 bit)
    void assign(int i) {
        if (i & ~0x3F) // 如果超出 6 位范围(~077 = ~63 = ...1100 0000)
            throw std::out_of_range("Tiny overflow");
        v = static_cast<char>(i);
    }
public:
    // 构造函数:从 int 转到 Tiny(有范围检查)
    Tiny(int i) { assign(i); }
    // 赋值运算符:从 int 赋值(有范围检查)
    Tiny& operator=(int i) { assign(i); return *this; }
    // 转换运算符:从 Tiny 转到 int(隐式转换,无需范围检查)
    // 注意:返回类型写在 operator 的名字里,不需要单独写返回类型
    operator int() const { return v; }
};
int main() {
    Tiny c1 = 2;
    Tiny c2 = 62;
    Tiny c3 = c2 - c1; // c2 和 c1 隐式转换为 int,结果 60,再赋给 c3
    int i = c1 + c2;   // c1, c2 转 int,64,i = 64
    try {
        c1 = c1 + c2;  // 64 超出范围,抛出异常
    } catch (const std::out_of_range& e) {
        std::cout << "错误:" << e.what() << "\n";
    }
    std::cout << "c3 = " << (int)c3 << "\n"; // 60
    std::cout << "i = " << i << "\n";        // 64
    return 0;
}
18.4.2 explicit 转换运算符

explicit 防止在意外的上下文中发生隐式转换:

#include <memory>
#include <iostream>
// unique_ptr 内部的类似设计:
class SafeHandle {
    int* p;
public:
    SafeHandle(int* ptr) : p{ptr} {}
    ~SafeHandle() { delete p; }
    // explicit:只允许在 if/while/bool 这类"显式需要 bool"的地方转换
    explicit operator bool() const {
        return p != nullptr;
    }
};
int main() {
    SafeHandle h{new int{42}};
    if (h)                    // OK:显式的 bool 语境
        std::cout << "有效\n";
    // bool b = h;           // 错误:explicit 禁止隐式转换
    bool b = (bool)h;        // OK:显式转换
    bool c = static_cast<bool>(h); // OK:显式转换
    return 0;
}

综合:完整的"资源管理类"模板

把第17、18章的知识综合在一起,实现一个完整的资源句柄类:

#include <iostream>
#include <stdexcept>
#include <algorithm> // for std::swap
// 一个管理堆上单个对象的智能指针(类似 unique_ptr)
template<class T>
class Handle {
    T* p; // 所管理的资源
public:
    // ---- 构造函数 ----
    explicit Handle(T* pp = nullptr) : p{pp} {}
    // ---- 析构函数(释放资源) ----
    ~Handle() { delete p; }
    // ---- 禁止拷贝(避免 double-free) ----
    Handle(const Handle&)            = delete;
    Handle& operator=(const Handle&) = delete;
    // ---- 允许移动(转移所有权) ----
    Handle(Handle&& a) noexcept      // noexcept:移动通常不抛出异常
        : p{a.p}
    {
        a.p = nullptr; // 源对象不再拥有资源
    }
    Handle& operator=(Handle&& a) noexcept {
        if (this != &a) {      // 防止自移动
            delete p;          // 释放旧资源
            p   = a.p;         // 接管新资源
            a.p = nullptr;     // 源对象置空
        }
        return *this;
    }
    // ---- 解引用运算符 ----
    T& operator*() const {
        if (!p) throw std::runtime_error("Handle: null pointer dereference");
        return *p;
    }
    T* operator->() const {
        if (!p) throw std::runtime_error("Handle: null pointer dereference");
        return p;
    }
    // ---- bool 转换:判断是否持有资源 ----
    explicit operator bool() const { return p != nullptr; }
    // ---- 获取原始指针(慎用) ----
    T* get() const { return p; }
};
// 测试
struct Point {
    int x, y;
    Point(int x, int y) : x{x}, y{y} {}
};
int main() {
    Handle<Point> h1{new Point{3, 4}};
    std::cout << "x=" << h1->x << " y=" << h1->y << "\n"; // x=3 y=4
    // Handle<Point> h2{h1}; // 错误:拷贝被禁止
    Handle<Point> h2{std::move(h1)}; // OK:移动
    if (!h1) std::cout << "h1 已为空\n"; // h1 已为空
    if (h2)  std::cout << "h2 有效\n";   // h2 有效
    std::cout << "h2: x=" << h2->x << "\n"; // x=3
    return 0; // h2 析构,delete Point 对象
}

知识总结

第17章核心规则:构造/析构/拷贝/移动 要作为一组设计

如果类管理资源(有析构函数非默认),通常需要:
  - 构造函数(获取资源)
  - 析构函数(释放资源)
  - 拷贝构造函数(深拷贝)
  - 拷贝赋值运算符(深拷贝)
  - 移动构造函数(转移所有权)
  - 移动赋值运算符(转移所有权)
这叫"五法则"(Rule of Five)或"零法则"(Rule of Zero)

情况 建议
类不管理资源 用默认生成的操作(Rule of Zero)
类管理资源 定义全部6种操作(Rule of Five/Six)
基类(多态) 虚析构函数,禁止拷贝
移动比拷贝便宜 标注 noexcept,容器才会用移动

第18章核心规则:运算符重载


情况 建议
修改自身(+=, -= 成员函数
产生新值(+, - 非成员函数(调用成员函数)
对称运算符(==, + 非成员函数(两个操作数地位相等)
需要左值(=, [] 成员函数
隐式转换 谨慎!优先用 explicit

运算符重载的黄金法则:
  1. 模拟约定俗成的用法(+ 就该像加法)
  2. 不要让用户惊讶(principle of least surprise)
  3. 不要定义语义不明确的转换

C++ 第19章:特殊运算符 + 第20章:派生类

教材:《C++ Programming Language》第4版
目标:从零理解,代码含详细注释

第19章:特殊运算符

19.1 概述

C++ 中有几个运算符在重载时语法稍有不同,但它们是设计容器、智能指针、迭代器的关键:

[] —— 下标运算符(subscript)
() —— 函数调用运算符(call)
-> —— 成员访问运算符(dereference)
++ -- —— 自增/自减(increment/decrement)
new delete —— 内存分配/释放
"" —— 用户自定义字面量(user-defined literals)

19.2.1 下标运算符 operator[]

operator[] 让对象支持"像数组一样用下标访问"的语法。
经典用途:实现关联数组(字符串→整数映射)

#include <iostream>
#include <vector>
#include <string>
// 简单的关联数组:用 vector 存储 {名字, 值} 对
struct Assoc {
    std::vector<std::pair<std::string, int>> vec;
    // 非 const 版本:可以修改,找不到就插入
    int& operator[](const std::string& s) {
        // 遍历查找已有的键
        for (auto& x : vec)
            if (s == x.first) return x.second; // 找到了,返回引用
        // 没找到:插入新键,初始值为 0
        vec.push_back({s, 0});
        return vec.back().second; // 返回新插入元素的引用
    }
    // const 版本:只读,找不到就抛异常
    const int& operator[](const std::string& s) const {
        for (const auto& x : vec)
            if (s == x.first) return x.second;
        throw std::out_of_range("key not found");
    }
};
int main() {
    Assoc counter;
    // 统计单词出现次数
    std::string words[] = {"apple", "banana", "apple", "cherry", "banana", "apple"};
    for (const auto& w : words)
        ++counter[w]; // 第一次访问时自动插入,初始值 0
    // 输出结果
    for (const auto& p : counter.vec)
        std::cout << p.first << ": " << p.second << "\n";
    // 输出:apple: 3, banana: 2, cherry: 1
    return 0;
}

注意operator[] 必须是成员函数,不能是非成员函数。

19.2.2 函数调用运算符 operator()

重载 () 让对象可以像函数一样被调用,这样的对象叫函数对象(function object)仿函数(functor)
函数对象的优势:可以带状态(比普通函数指针强大得多)

#include <iostream>
#include <vector>
#include <list>
#include <algorithm>  // for std::for_each
#include <complex>
using Complex = std::complex<double>;
// 一个"加法器"函数对象
// 它记住一个固定的值,每次调用时把这个值加到参数上
class Add {
    Complex val; // 存储要加的值(函数对象的"状态")
public:
    // 构造时传入要加的值
    Add(Complex c) : val{c} {}
    Add(double r, double i) : val{r, i} {}
    // operator():使对象可以像函数一样被调用
    // 参数 c 是被修改的目标
    void operator()(Complex& c) const {
        c += val; // 将存储的值加到 c 上
    }
};
int main() {
    std::vector<Complex> vec{{1,0}, {2,0}, {3,0}};
    std::list<Complex>   lst{{4,0}, {5,0}};
    // Add{2,3} 构造一个函数对象,记住 "2+3i"
    // for_each 对 vec 中每个元素调用这个函数对象
    std::for_each(vec.begin(), vec.end(), Add{2, 3});
    // 结果:vec = {3+3i, 4+3i, 5+3i}
    Complex z{10, 0};
    std::for_each(lst.begin(), lst.end(), Add{z});
    // 结果:lst = {14+0i, 15+0i}
    std::cout << "vec[0] = " << vec[0] << "\n"; // (3,3)
    std::cout << "lst[0] = " << *lst.begin() << "\n"; // (14,0)
    return 0;
}

和 lambda 的等价关系:

// 这两种写法等价:
// 方式1:函数对象
for_each(vec.begin(), vec.end(), Add{2,3});
// 方式2:lambda 表达式(本质上编译器生成了类似 Add 的匿名类)
for_each(vec.begin(), vec.end(), [](Complex& a){ a += Complex{2,3}; });

Lambda 就是"编译器自动生成的函数对象"的语法糖。

19.2.3 解引用运算符 operator->

operator-> 让对象可以像指针一样用 -> 访问成员,这是实现智能指针的核心机制。
工作原理:

p->m
等价于
(p.operator->())->m
#include <iostream>
#include <string>
// 一个简单的"磁盘指针"示例:演示延迟加载
// 真实程序中可以替换成数据库、文件、网络等
struct Record {
    std::string name;
    int         id;
};
template<typename T>
class LazyPtr {
    T*          data;    // 指向实际数据(nullptr 表示尚未加载)
    std::string source;  // 数据来源标识
    // 模拟"从磁盘读取"
    T* load() {
        std::cout << "[加载数据:" << source << "]\n";
        return new T{"从" + source + "加载的记录", 42};
    }
public:
    // 构造时不立刻加载,只记录来源
    LazyPtr(const std::string& s)
        : data{nullptr}, source{s} {}
    // 析构时写回(这里只是打印)
    ~LazyPtr() {
        if (data) {
            std::cout << "[写回数据:" << source << "]\n";
            delete data;
        }
    }
    // operator->:第一次访问时才加载数据(延迟加载)
    T* operator->() {
        if (!data)
            data = load(); // 按需加载
        return data;       // 返回原始指针,编译器再用 -> 访问成员
    }
    // operator*:解引用
    T& operator*() {
        if (!data) data = load();
        return *data;
    }
};
int main() {
    LazyPtr<Record> p{"disk://record_001"};
    // 第一次访问 -> 触发加载
    std::cout << p->name << "\n";  // 输出:[加载] 从disk://record_001加载的记录
    // 第二次访问 -> 直接用缓存,不再加载
    std::cout << p->id << "\n";    // 输出:42
    return 0; // 析构时输出:[写回数据]
}

operator-> 必须是成员函数,且返回值必须是指针或另一个支持 -> 的对象。

19.2.4 自增/自减运算符 ++ --

++ 有前缀(++p)和后缀(p++)两种形式,区分方法:后缀版本多一个 int 假参数

前缀 ++:Ptr& operator++()        — 没有 int 参数
后缀 ++:Ptr  operator++(int)     — 有一个不用的 int 参数(纯粹用来区分)
#include <iostream>
#include <stdexcept>
// 带范围检查的"安全指针":访问越界时抛出异常而不是未定义行为
template<typename T>
class CheckedPtr {
    T*  ptr;    // 当前指向位置
    T*  begin;  // 数组起始
    T*  end;    // 数组结束(past-the-end)
public:
    // 构造:绑定到数组
    template<int N>
    CheckedPtr(T* p, T(&arr)[N])
        : ptr{p}, begin{arr}, end{arr + N} {}
    // ---- 前缀 ++:先移动,返回自身引用 ----
    // 返回引用(高效,不需要拷贝)
    CheckedPtr& operator++() {
        if (ptr >= end)
            throw std::out_of_range("CheckedPtr: increment past end");
        ++ptr;
        return *this;
    }
    // ---- 后缀 ++:先保存旧值,移动,返回旧值 ----
    // 返回值(必须拷贝,因为要返回"移动前"的状态)
    CheckedPtr operator++(int) {  // int 参数是哑参数,只用来区分前缀/后缀
        CheckedPtr old = *this;   // 保存当前状态
        ++(*this);                // 调用前缀版本(复用逻辑)
        return old;               // 返回旧状态
    }
    // ---- 前缀 -- ----
    CheckedPtr& operator--() {
        if (ptr <= begin)
            throw std::out_of_range("CheckedPtr: decrement past begin");
        --ptr;
        return *this;
    }
    // ---- 后缀 -- ----
    CheckedPtr operator--(int) {
        CheckedPtr old = *this;
        --(*this);
        return old;
    }
    // 解引用
    T& operator*() {
        if (ptr < begin || ptr >= end)
            throw std::out_of_range("CheckedPtr: dereference out of range");
        return *ptr;
    }
};
int main() {
    int arr[5] = {10, 20, 30, 40, 50};
    CheckedPtr<int> p{arr, arr};
    // 前缀 ++:更高效(不需要拷贝)
    std::cout << *p << "\n"; // 10
    ++p;
    std::cout << *p << "\n"; // 20
    // 后缀 ++:需要拷贝旧值,稍慢
    auto old = p++;           // old 指向 20,p 移动到 30
    std::cout << *old << "\n"; // 20
    std::cout << *p << "\n";   // 30
    // 越界检测
    try {
        CheckedPtr<int> q{arr + 5, arr}; // 指向末尾之后
        *q; // 应该抛出异常
    } catch (const std::out_of_range& e) {
        std::cout << "捕获异常: " << e.what() << "\n";
    }
    return 0;
}

设计建议:优先使用前缀 ++++p),后缀 ++ 需要额外拷贝,较慢。

19.2.5 内存分配/释放 operator new / operator delete

可以为特定类重载 newdelete,实现专用内存池,大幅提升特定场景的性能。

#include <iostream>
#include <cstddef>  // for size_t
#include <cstdlib>  // for malloc/free
// 演示:为 SmallObj 提供专用内存池(简化版)
class SmallObj {
    int data;
public:
    SmallObj(int v) : data{v} {}
    // 重载 operator new:为 SmallObj 专门分配内存
    // size 参数由编译器自动传入(即 sizeof(SmallObj))
    void* operator new(std::size_t size) {
        std::cout << "[分配 " << size << " 字节]\n";
        return std::malloc(size); // 实际项目中这里接自定义内存池
    }
    // 重载 operator delete:释放内存
    // size 参数是对象大小(对于有虚析构函数的类,编译器能正确传入派生类大小)
    void operator delete(void* p, std::size_t size) {
        std::cout << "[释放 " << size << " 字节]\n";
        std::free(p);
    }
    void print() const { std::cout << "data=" << data << "\n"; }
};
int main() {
    SmallObj* p = new SmallObj{42}; // 调用 SmallObj::operator new
    p->print();
    delete p; // 调用 SmallObj::operator delete
    return 0;
}

19.2.6 用户自定义字面量

C++ 内置了 123(int)、3.14(double)、"hello"(char*)等字面量。通过字面量运算符可以定义自己的字面量后缀:

#include <iostream>
#include <string>
#include <complex>
#include <stdexcept>
// ---- 示例1:虚数字面量 ----
// 定义后缀 _i,让 3.14_i 表示虚数 0+3.14i
constexpr std::complex<double> operator"" _i(long double d) {
    return std::complex<double>{0, static_cast<double>(d)};
}
// ---- 示例2:字符串字面量 ----
// 定义后缀 _s(注意:标准库已有 "s",这里用 _mystr 避免冲突)
std::string operator"" _mystr(const char* p, std::size_t n) {
    // p 是字符数组,n 是字符个数
    return std::string{p, n}; // 构造 std::string
}
// ---- 示例3:长度单位字面量 ----
// 定义后缀 _km 和 _m,统一转换为"米"
constexpr double operator"" _km(long double d) {
    return static_cast<double>(d) * 1000.0; // 千米转米
}
constexpr double operator"" _m(long double d) {
    return static_cast<double>(d); // 米不变
}
// ---- 示例4:模板字面量运算符(处理大整数/进制转换) ----
// 定义后缀 _b2,让 1010_b2 表示二进制 1010 = 10
template<char... chars>
constexpr int operator"" _b2() {
    int result = 0;
    // 折叠展开每个字符
    ((result = result * 2 + (chars - '0')), ...);
    return result;
}
int main() {
    // 示例1:虚数
    auto z1 = 1.5_i; // 0 + 1.5i
    auto z2 = 2.0 + 3.0_i; // 2 + 3i
    std::cout << "z1 = " << z1 << "\n";
    std::cout << "z2 = " << z2 << "\n";
    // 示例2:字符串
    auto s = "Hello, World"_mystr;
    std::cout << "字符串长度: " << s.size() << "\n"; // 12
    // 示例3:单位
    double dist1 = 3.5_km; // 3500 米
    double dist2 = 500.0_m; // 500 米
    std::cout << "总距离: " << (dist1 + dist2) << " 米\n"; // 4000
    // 示例4:二进制字面量
    constexpr int val = 1010_b2; // 二进制 1010 = 十进制 10
    std::cout << "1010_b2 = " << val << "\n"; // 10
    return 0;
}

命名规范:标准库保留所有不以下划线 _ 开头的后缀,自定义后缀必须以 _ 开头!

123km   // 危险:标准库保留
123_km  // 安全:用户自定义

19.3 一个 String 类的完整实现

String 类展示了所有特殊运算符的综合应用,并实现了短字符串优化(Short String Optimization, SSO)
短字符串优化的核心思路:

字符串很短(≤15字符):
  直接存在对象内部的 char 数组 ch[] 中
  → 不需要堆内存分配!
字符串很长(>15字符):
  在堆上分配内存,ptr 指向堆内存
  → 需要 new/delete

内存布局示意(ASCII):

短字符串(sz <= 15):
  +------+-----+------------------+
  | sz=3 | ptr | ch[]="abc\0..." |
  +------+-----+------------------+
           |_______↑(ptr指向ch)
长字符串(sz > 15):
  +------+-------+-------+
  | sz=20| ptr   | space |     space = 额外预留空间数
  +------+-------+-------+
             |
             ↓ 堆内存
          [ 20个字符 + 预留 + '\0' ]
#include <iostream>
#include <stdexcept>
#include <cstring>   // for strlen, strcpy, memcpy
#include <algorithm> // for std::swap
// ======================================================
// 完整的 String 类实现(含短字符串优化)
// ======================================================
class String {
public:
    // ---- 构造函数 ----
    String();                          // 默认:空字符串
    explicit String(const char* p);    // 从 C 字符串构造
    String(const String& x);          // 拷贝构造
    String& operator=(const String& x); // 拷贝赋值
    String(String&& x);               // 移动构造
    String& operator=(String&& x);    // 移动赋值
    ~String() {                        // 析构:仅长字符串需要释放
        if (short_max < sz) delete[] ptr;
    }
    // ---- 下标访问 ----
    char& operator[](int n) { return ptr[n]; }          // 不检查范围
    char  operator[](int n) const { return ptr[n]; }
    char& at(int n) { check(n); return ptr[n]; }        // 检查范围
    char  at(int n) const { check(n); return ptr[n]; }
    // ---- 追加字符 ----
    String& operator+=(char c);
    // ---- 其他接口 ----
    const char* c_str() { return ptr; }
    const char* c_str() const { return ptr; }
    int size() const { return sz; }
    int capacity() const {
        // 短字符串最大容量 = short_max
        // 长字符串容量 = 当前大小 + 预留空间
        return (sz <= short_max) ? short_max : sz + space;
    }
private:
    static const int short_max = 15; // 短字符串阈值
    int   sz;  // 当前字符数(不含终止符 '\0')
    char* ptr; // 指向字符数组(短时指向 ch,长时指向堆)
    // 匿名联合体:sz <= short_max 时用 ch,否则用 space
    // 两个字段共享同一块内存,节省空间
    union {
        int  space;           // 长字符串的预留空间(额外可用字节数)
        char ch[short_max+1]; // 短字符串的内联存储(+1 为 '\0')
    };
    // 范围检查
    void check(int n) const {
        if (n < 0 || sz <= n)
            throw std::out_of_range("String::at()");
    }
    // 辅助:把 x 的内容复制到 *this(不负责清理旧内容)
    void copy_from(const String& x) {
        if (x.sz <= short_max) {
            // 短字符串:直接内存拷贝整个对象
            memcpy(this, &x, sizeof(x));
            ptr = ch; // 让 ptr 指向本对象的 ch(不是 x 的 ch!)
        } else {
            // 长字符串:分配新内存,复制字符
            ptr = new char[x.sz + 1];
            strcpy(ptr, x.ptr);
            sz    = x.sz;
            space = 0;
        }
    }
    // 辅助:从 x 移动到 *this(x 变成空字符串)
    void move_from(String& x) {
        if (x.sz <= short_max) {
            // 短字符串:同样直接拷贝(短字符串移动不比拷贝快多少)
            memcpy(this, &x, sizeof(x));
            ptr = ch;
        } else {
            // 长字符串:直接接管 x 的堆指针(不分配新内存!)
            ptr   = x.ptr;
            sz    = x.sz;
            space = x.space;
            // 将 x 重置为空字符串(避免 x 析构时 double-free)
            x.ptr   = x.ch;
            x.sz    = 0;
            x.ch[0] = 0;
        }
    }
};
// ---- 默认构造函数 ----
String::String()
    : sz{0}, ptr{ch} // ptr 指向内联数组 ch
{
    ch[0] = 0; // 空字符串:终止符
}
// ---- 从 C 字符串构造 ----
String::String(const char* p)
    : sz{(int)strlen(p)},
      // 根据长度决定用内联存储还是堆
      ptr{(sz <= short_max) ? ch : new char[sz + 1]},
      space{0}
{
    strcpy(ptr, p); // 复制字符
}
// ---- 拷贝构造 ----
String::String(const String& x) {
    copy_from(x);
}
// ---- 移动构造 ----
String::String(String&& x) {
    move_from(x);
}
// ---- 拷贝赋值 ----
String& String::operator=(const String& x) {
    if (this == &x) return *this; // 自赋值保护
    // 记录旧的堆内存地址(如果有的话),拷贝后再释放
    char* old_ptr = (short_max < sz) ? ptr : nullptr;
    copy_from(x);
    delete[] old_ptr; // 释放旧内存(nullptr 的 delete 是无操作)
    return *this;
}
// ---- 移动赋值 ----
String& String::operator=(String&& x) {
    if (this == &x) return *this; // 自移动保护
    if (short_max < sz) delete[] ptr; // 释放旧内存
    move_from(x);
    return *this;
}
// ---- 追加字符 ----
String& String::operator+=(char c) {
    if (sz == short_max) {
        // 从短变长:分配新内存(容量翻倍)
        int   n   = sz + sz + 2; // +2 给终止符留空间
        char* tmp = new char[n];
        strcpy(tmp, ptr);
        ptr   = tmp;
        space = n - sz - 2; // 新的预留空间
    } else if (short_max < sz) {
        if (space == 0) {
            // 长字符串没有预留空间了:再翻倍
            int   n   = sz + sz + 2;
            char* tmp = new char[n];
            strcpy(tmp, ptr);
            delete[] ptr;
            ptr   = tmp;
            space = n - sz - 2;
        } else {
            --space; // 消耗一个预留位置
        }
    }
    // 此时 ptr[sz] 必然可写
    ptr[sz]   = c;   // 写入新字符
    ptr[++sz] = 0;   // 更新终止符
    return *this;
}
// ---- 非成员辅助函数 ----
std::ostream& operator<<(std::ostream& os, const String& s) {
    return os << s.c_str();
}
bool operator==(const String& a, const String& b) {
    if (a.size() != b.size()) return false;
    for (int i = 0; i != a.size(); ++i)
        if (a[i] != b[i]) return false;
    return true;
}
bool operator!=(const String& a, const String& b) {
    return !(a == b);
}
// 支持 range-for 循环
char*       begin(String& s)       { return &s[0]; }
char*       end(String& s)         { return &s[0] + s.size(); }
const char* begin(const String& s) { return &s[0]; }
const char* end(const String& s)   { return &s[0] + s.size(); }
// 字符串拼接
String& operator+=(String& a, const String& b) {
    for (char c : b) a += c;
    return a;
}
String operator+(const String& a, const String& b) {
    String res{a};
    res += b;
    return res;
}
// 用户自定义字面量:让 "hello"_S 产生 String 对象
String operator"" _S(const char* p, std::size_t) {
    return String{p};
}
// ---- 测试 ----
int main() {
    String s1{"Hello"};           // 短字符串(≤15),存在对象内部
    String s2{"World"};
    String s3 = s1 + " " + s2;   // 拼接
    std::cout << s3 << "\n";      // Hello World
    std::cout << "长度: " << s3.size() << "\n"; // 11
    // 追加字符,测试扩容
    String s4{"0123456789abcde"}; // 恰好 15 个字符(short_max)
    std::cout << "容量: " << s4.capacity() << "\n"; // 15
    s4 += 'X'; // 触发从短字符串到长字符串的转换
    std::cout << "追加后: " << s4 << "\n";
    std::cout << "新容量: " << s4.capacity() << "\n"; // 变大了
    // range-for
    std::cout << "逐字符: ";
    for (char c : s1) std::cout << c << "-";
    std::cout << "\n";
    // 自定义字面量
    String s5 = "Literal"_S;
    std::cout << s5 << "\n";
    // 移动
    String s6 = std::move(s3); // s3 变空,s6 得到内容
    std::cout << "移动后 s6: " << s6 << "\n";
    return 0;
}

19.4 友元(friend)

友元让一个函数(或类)访问另一个类的私有/保护成员,但它本身不是那个类的成员。
三种访问级别对比:

普通成员函数:
  [1] 可访问私有成员
  [2] 在类的作用域内
  [3] 需要通过对象调用(有 this 指针)
静态成员函数:
  [1] 可访问私有成员
  [2] 在类的作用域内
  [3] 不需要对象(没有 this)
友元函数:
  [1] 可访问私有成员
  [2] 不在类的作用域内(是独立函数)
  [3] 不需要对象
#include <iostream>
// 二维矩阵与向量,需要内积运算
const int N = 3;
class Vector3D {
    float v[N];
    // 声明 dot 为友元:它可以访问 v[]
    friend float dot(const Vector3D&, const Vector3D&);
    friend class Matrix3D; // 整个 Matrix3D 都是友元
public:
    Vector3D(float a, float b, float c) { v[0]=a; v[1]=b; v[2]=c; }
};
class Matrix3D {
    float m[N][N];
public:
    Matrix3D() {
        for (int i=0;i<N;i++) for(int j=0;j<N;j++) m[i][j]=(i==j)?1:0;
    }
    // Matrix3D 是 Vector3D 的友元,可以访问 v[]
    Vector3D operator*(const Vector3D& vec) const {
        Vector3D result{0,0,0};
        for (int i = 0; i < N; i++) {
            result.v[i] = 0;
            for (int j = 0; j < N; j++)
                result.v[i] += m[i][j] * vec.v[j]; // 直接访问私有 v[]
        }
        return result;
    }
};
// 友元函数:可以访问两个类的私有成员
float dot(const Vector3D& a, const Vector3D& b) {
    float sum = 0;
    for (int i = 0; i < N; i++)
        sum += a.v[i] * b.v[i]; // 直接访问私有 v[]
    return sum;
}
int main() {
    Vector3D u{1, 0, 0};
    Vector3D v{0, 1, 0};
    std::cout << "u·v = " << dot(u, v) << "\n"; // 0(垂直)
    Vector3D w{1, 1, 1};
    std::cout << "u·w = " << dot(u, w) << "\n"; // 1
    Matrix3D I; // 单位矩阵
    Vector3D Iv = I * w;
    std::cout << "I*w = (" << Iv.v[0] << "," << Iv.v[1] << "," << Iv.v[2] << ")\n"; // (1,1,1)
    return 0;
}

什么时候用友元?

  • 实现对称二元运算符(如 a * b,两个参数地位相等)
  • 操作两个不同类的私有数据(如矩阵×向量)
  • 避免暴露不必要的 getter/setter
    友元的查找规则:友元必须在类外的某个封闭作用域中声明,或者通过参数类型找到(ADL)。

第20章:派生类

20.1 概述:为什么需要继承?

继承(Inheritance)是面向对象编程的核心机制,用来表达概念之间的**“是一种”(is-a)关系**:

圆是一种形状     Circle is-a Shape
经理是一种员工   Manager is-a Employee
卡车是一种车辆   Truck is-a Vehicle

继承的两种目的:

目的 名称 说明
代码复用 实现继承 派生类共享基类的实现
统一接口 接口继承 不同派生类可以通过基类指针统一访问

20.2 派生类基础

#include <iostream>
#include <string>
#include <list>
// ---- 基类 ----
class Employee {
public:
    Employee(const std::string& name, int dept)
        : name_{name}, dept_{dept} {}
    virtual void print() const { // virtual:允许派生类覆盖
        std::cout << name_ << "\t部门:" << dept_ << "\n";
    }
    std::string full_name() const { return name_; }
    virtual ~Employee() {} // 虚析构函数(有虚函数就要有虚析构)
private: // 派生类不能直接访问 private 成员
    std::string name_;
    int         dept_;
};
// ---- 派生类(public 继承:Manager "是一种" Employee)----
class Manager : public Employee {
public:
    Manager(const std::string& name, int dept, int lvl)
        : Employee{name, dept}, // 先初始化基类
          level_{lvl}
    {}
    // 覆盖(override)基类的 print()
    void print() const override {
        Employee::print(); // 调用基类版本(加 :: 防止无限递归)
        std::cout << "\t级别:" << level_ << "\n";
    }
private:
    std::list<Employee*> group; // 管理的员工列表
    int level_;
};
int main() {
    Employee e{"Brown", 1234};
    Manager  m{"Smith", 1234, 2};
    // Manager* 可以隐式转换为 Employee*(is-a 关系)
    Employee* pe = &m; // OK
    pe->print();       // 调用 Manager::print()(多态!)
    // Employee* 不能隐式转换为 Manager*(不是每个员工都是经理)
    // Manager* pm = &e; // 编译错误
    return 0;
}

内存布局(ASCII):

Employee 对象:
  +-----------+
  | vtbl_ptr  |  → 指向 Employee 的虚函数表
  | name_     |
  | dept_     |
  +-----------+
Manager 对象:
  +-----------+
  | vtbl_ptr  |  → 指向 Manager 的虚函数表
  | name_     |  ← 继承自 Employee 的部分
  | dept_     |
  +-----------+
  | group     |  ← Manager 自己的成员
  | level_    |
  +-----------+

20.3 类层次结构

20.3.1 类型字段方案(不推荐)

用一个枚举字段记录对象类型,然后 switch 分支处理:

// 反面教材:不要这样做
struct Employee {
    enum Type { EMPLOYEE, MANAGER };
    Type type; // 类型字段
    // ... 其他成员 ...
};
void print(const Employee* e) {
    switch (e->type) {       // 每次添加新类型都要修改这里!
        case Employee::EMPLOYEE:
            // ...
            break;
        case Employee::MANAGER: {
            // 需要手动强制转换
            const Manager* m = static_cast<const Manager*>(e);
            // ...
            break;
        }
    }
}

这种方式的问题:

  • 添加新类型(如 Director)必须修改所有 switch 语句
  • 程序规模越大,维护越困难
  • 违反开闭原则(对修改开放,对扩展关闭)
20.3.2 虚函数:正确的解决方案
#include <iostream>
#include <list>
#include <string>
#include <memory>
class Employee {
public:
    Employee(const std::string& n, int d) : name{n}, dept{d} {}
    // virtual 关键字:声明"可被覆盖的函数"
    virtual void print() const {
        std::cout << name << "\t" << dept << "\n";
    }
    virtual ~Employee() {}
private:
    std::string name;
    int         dept;
};
class Manager : public Employee {
public:
    Manager(const std::string& n, int d, int lvl)
        : Employee{n, d}, level{lvl} {}
    // override:明确声明"我要覆盖基类的虚函数"
    void print() const override {
        Employee::print();
        std::cout << "\t级别:" << level << "\n";
    }
private:
    int level;
};
class Director : public Manager {
public:
    Director(const std::string& n, int d)
        : Manager{n, d, 5}, division{"未指定"} {}
    void print() const override {
        Manager::print();
        std::cout << "\t部门主管:" << division << "\n";
    }
private:
    std::string division;
};
// 这个函数不需要知道具体是哪种 Employee,多态自动处理
void print_list(const std::list<Employee*>& lst) {
    for (auto* p : lst)
        p->print(); // 自动调用正确的版本!
}
int main() {
    Employee  e{"Brown",  1001};
    Manager   m{"Smith",  1002, 2};
    Director  d{"Johnson",1003};
    std::list<Employee*> org{&e, &m, &d};
    print_list(org); // 每个对象打印自己正确的信息
    return 0;
}

虚函数表(vtbl)实现原理(ASCII):

Employee 对象:                  Employee 的 vtbl:
  +----------+                  +------------------+
  | vtbl_ptr |→ →→ →→ →→ →→ → | Employee::print  |
  | name     |                  +------------------+
  | dept     |
  +----------+
Manager 对象:                   Manager 的 vtbl:
  +----------+                  +------------------+
  | vtbl_ptr |→ →→ →→ →→ →→ → | Manager::print   |
  | name     |                  +------------------+
  | dept     |
  | level    |
  +----------+
调用 p->print():
  1. 通过 vtbl_ptr 找到虚函数表
  2. 在表中查找 print() 的地址
  3. 调用正确版本
  (开销:约一次额外的指针跳转,比直接调用慢约 25%)
20.3.3 类层次结构图

Employee
(基类)

Manager
(派生类)

Assistant
(派生类)

Director
(派生类)

Temporary
(基类)

Temp
(多重继承)

Consultant
(多重继承)

20.3.4 覆盖控制关键字

关键字 位置 含义
virtual 基类函数前 声明该函数可被覆盖
= 0 函数声明后 纯虚函数,子类必须覆盖
override 函数声明后 明确声明"我要覆盖基类虚函数"(推荐)
final 函数/类后 禁止进一步覆盖/继承

struct Base {
    virtual void f(int);    // 虚函数
    virtual void g(double);
};
struct Derived : Base {
    // override 让编译器帮你检查:
    void f(int) override;   // OK:Base 有 virtual void f(int)
    // 下面这些 override 会报错:
    // void f(double) override; // 错:参数类型不同,不是覆盖
    // void h() override;       // 错:Base 没有 h()
};
struct Leaf : Derived {
    // final:禁止进一步覆盖
    void f(int) override final; // 覆盖 Derived::f,且之后不能再覆盖
};
// final 类:禁止继承
class Terminal final : public Base {
    void f(int) override;
};
// class CannotDerive : public Terminal {}; // 错:Terminal 是 final

override 关键字的价值: 避免手误导致的"以为在覆盖,实际上在隐藏"的 bug。

20.3.5 using 基类成员

函数不跨作用域重载。派生类的同名函数会隐藏基类的所有同名函数:

#include <iostream>
struct Base {
    void f(int) { std::cout << "Base::f(int)\n"; }
};
struct Derived : Base {
    void f(double) { std::cout << "Derived::f(double)\n"; }
    // Base::f(int) 被隐藏了!
};
struct Derived2 : Base {
    using Base::f;            // 把 Base 的所有 f 引入 Derived2 的作用域
    void f(double) { std::cout << "Derived2::f(double)\n"; }
    // 现在 f(int) 和 f(double) 都可用
};
int main() {
    Derived d;
    d.f(1);     // 调用 Derived::f(double),不是 Base::f(int)!
    d.f(1.0);   // 调用 Derived::f(double)
    Derived2 d2;
    d2.f(1);    // 调用 Base::f(int)(通过 using 引入)
    d2.f(1.0);  // 调用 Derived2::f(double)
    return 0;
}

继承构造函数:

#include <vector>
#include <iostream>
#include <stdexcept>
// 想要一个带范围检查的 vector
template<class T>
struct SafeVector : std::vector<T> {
    // 继承 std::vector 的所有构造函数
    using std::vector<T>::vector;
    // 覆盖下标运算符,加范围检查
    T& operator[](std::size_t i) {
        if (i >= this->size())
            throw std::out_of_range("SafeVector: index out of range");
        return std::vector<T>::operator[](i);
    }
    const T& operator[](std::size_t i) const {
        if (i >= this->size())
            throw std::out_of_range("SafeVector: index out of range");
        return std::vector<T>::operator[](i);
    }
};
int main() {
    SafeVector<int> v{1, 2, 3, 5, 8}; // 使用继承的 initializer_list 构造函数
    std::cout << v[2] << "\n"; // 3
    try {
        v[10]; // 越界!
    } catch (const std::out_of_range& e) {
        std::cout << "异常: " << e.what() << "\n";
    }
    return 0;
}

20.4 抽象类(Abstract Class)

纯虚函数(= 0:声明接口但不提供实现,派生类必须覆盖。
含有至少一个纯虚函数的类叫抽象类,不能直接实例化。

#include <iostream>
#include <vector>
#include <memory>
#include <cmath>  // for M_PI
// ---- 抽象基类:定义接口 ----
class Shape {
public:
    // 纯虚函数:所有形状都必须能绘制
    virtual void   draw()  const = 0; // = 0 表示"纯虚"
    virtual double area()  const = 0;
    virtual double perimeter() const = 0;
    // 非纯虚函数:提供默认行为(派生类可选择覆盖)
    virtual void describe() const {
        std::cout << "形状,面积=" << area()
                  << ",周长=" << perimeter() << "\n";
    }
    // 虚析构函数:通过基类指针删除派生类对象时,确保调用正确的析构函数
    virtual ~Shape() {}
};
// ---- 具体派生类:必须实现所有纯虚函数 ----
class Circle : public Shape {
    double r;
public:
    Circle(double radius) : r{radius} {}
    void draw() const override {
        std::cout << "画圆(半径=" << r << ")\n";
    }
    double area()      const override { return M_PI * r * r; }
    double perimeter() const override { return 2 * M_PI * r; }
};
class Rectangle : public Shape {
    double w, h;
public:
    Rectangle(double w, double h) : w{w}, h{h} {}
    void draw() const override {
        std::cout << "画矩形(" << w << "×" << h << ")\n";
    }
    double area()      const override { return w * h; }
    double perimeter() const override { return 2 * (w + h); }
};
// 部分抽象类:实现了 area() 但没有实现 draw() 和 perimeter()
class Polygon : public Shape {
public:
    // 多边形默认是封闭的(实现了 is_closed),但 draw() 和 perimeter() 留给子类
    bool is_closed() const { return true; }
    // draw() 和 perimeter() 仍然是纯虚函数
    // Polygon 依然是抽象类,不能实例化
};
// 继续实现
class Triangle : public Polygon {
    double a, b, c; // 三边长
public:
    Triangle(double a, double b, double c) : a{a}, b{b}, c{c} {}
    void draw() const override {
        std::cout << "画三角形(边长:" << a << "," << b << "," << c << ")\n";
    }
    double area() const override {
        // 海伦公式
        double s = (a + b + c) / 2;
        return std::sqrt(s * (s-a) * (s-b) * (s-c));
    }
    double perimeter() const override { return a + b + c; }
};
int main() {
    // Shape s;  // 错误:抽象类不能实例化
    // 通过基类指针(多态)管理不同形状
    std::vector<std::unique_ptr<Shape>> shapes;
    shapes.push_back(std::make_unique<Circle>(5.0));
    shapes.push_back(std::make_unique<Rectangle>(3.0, 4.0));
    shapes.push_back(std::make_unique<Triangle>(3.0, 4.0, 5.0));
    for (const auto& s : shapes) {
        s->draw();
        s->describe(); // 调用非纯虚函数的默认实现
        std::cout << "---\n";
    }
    return 0;
}

海伦公式(Heron’s formula):
s = a + b + c 2 s = \frac{a+b+c}{2} s=2a+b+c
面积 = s ( s − a ) ( s − b ) ( s − c ) \text{面积} = \sqrt{s(s-a)(s-b)(s-c)} 面积=s(sa)(sb)(sc)

20.5 访问控制

三种访问级别
+----------------------------------+
|          public                  |   任何人都可以访问
|  +--------------------------+    |
|  |       protected          |    |   只有本类和派生类可以访问
|  |  +--------------------+  |   |
|  |  |      private       |  |   |   只有本类自己(和友元)可以访问
|  |  +--------------------+  |   |
|  +--------------------------+    |
+----------------------------------+
class Base {
public:
    int pub;    // 任何人可访问
protected:
    int prot;   // 只有 Base 和派生类可访问
private:
    int priv;   // 只有 Base 自己(和友元)可访问
};
class Derived : public Base {
    void f() {
        pub  = 1; // OK:public 成员
        prot = 2; // OK:protected 成员
        // priv = 3; // 错误:private 成员不可访问
    }
};
void outside_func(Base& b) {
    b.pub  = 1; // OK
    // b.prot = 2; // 错误:外部不能访问 protected
    // b.priv = 3; // 错误:外部不能访问 private
}
继承方式与访问控制

private 继承(has-a 实现)

Base
public→private
protected→private

protected 继承

Base
public→protected
protected→protected

public 继承(is-a 关系)

Base
public→public
protected→protected


基类成员 public 继承 protected 继承 private 继承
public public protected private
protected protected protected private
private 不可访问 不可访问 不可访问

protected 数据成员是设计错误的信号!

  • protected 数据相当于"对派生类开放的全局变量"
  • 难以维护(派生类可以随意修改)
  • 推荐:用 private 数据 + protected 函数

20.6 指向成员的指针

指向成员的指针(pointer to member)是一种特殊的"偏移量",可以间接指代类的某个成员:

#include <iostream>
#include <map>
#include <string>
class Obj {
public:
    virtual void start()   { std::cout << "start\n"; }
    virtual void stop()    { std::cout << "stop\n"; }
    virtual void reset()   { std::cout << "reset\n"; }
    int value;
    Obj() : value{42} {}
};
int main() {
    Obj obj;
    Obj* ptr = &obj;
    // ---- 指向成员函数的指针 ----
    // 类型:返回值 (类名::*指针名)(参数列表)
    using MemberFn = void (Obj::*)(); // Obj 的无参无返回值成员函数指针类型
    MemberFn pf = &Obj::start; // 指向 start
    (obj.*pf)();  // 通过对象调用:obj.start()
    (ptr->*pf)(); // 通过指针调用:ptr->start()
    pf = &Obj::stop; // 改变指向
    (obj.*pf)();  // obj.stop()
    // ---- 用 map 实现"命令分发" ----
    std::map<std::string, MemberFn> commands{
        {"start", &Obj::start},
        {"stop",  &Obj::stop},
        {"reset", &Obj::reset}
    };
    std::string cmd = "reset";
    auto it = commands.find(cmd);
    if (it != commands.end())
        (obj.*(it->second))(); // 动态调用 reset()
    // ---- 指向数据成员的指针 ----
    int Obj::* pm = &Obj::value; // 指向 Obj::value 的数据成员指针
    std::cout << "obj.value = " << obj.*pm << "\n";   // 42
    std::cout << "ptr->value = " << ptr->*pm << "\n"; // 42
    obj.*pm = 100; // 修改值
    std::cout << "修改后: " << obj.value << "\n";     // 100
    return 0;
}

与普通指针的区别:

普通指针:指向具体内存地址(一个固定的位置)
成员指针:指向"相对于对象起始位置的偏移"(一个偏移量)
使用:
  普通指针 p:  *p 或 p->m
  成员函数指针 pf:(obj.*pf)() 或 (ptr->*pf)()
  数据成员指针 pm:obj.*pm 或 ptr->*pm

逆变性规则(Contravariance):
基类成员指针 → 派生类成员指针:安全(派生类有基类的所有成员) \text{基类成员指针} \rightarrow \text{派生类成员指针}:\text{安全(派生类有基类的所有成员)} 基类成员指针派生类成员指针安全(派生类有基类的所有成员)
派生类成员指针 → 基类成员指针:不安全(基类对象可能没有派生类的成员) \text{派生类成员指针} \rightarrow \text{基类成员指针}:\text{不安全(基类对象可能没有派生类的成员)} 派生类成员指针基类成员指针不安全(基类对象可能没有派生类的成员)

综合示例:完整的形状层次体系

#include <iostream>
#include <vector>
#include <memory>
#include <string>
#include <cmath>
// ---- 抽象基类 ----
class Shape {
public:
    virtual void   draw()      const = 0;
    virtual double area()      const = 0;
    virtual std::string name() const = 0;
    virtual ~Shape() {}
};
// ---- 具体类 ----
class Circle final : public Shape { // final:不能继续派生
    double r;
public:
    explicit Circle(double r) : r{r} {}
    void draw() const override { std::cout << "O(r=" << r << ")"; }
    double area() const override { return M_PI * r * r; }
    std::string name() const override { return "Circle"; }
};
class Rect : public Shape {
protected:
    double w, h;
public:
    Rect(double w, double h) : w{w}, h{h} {}
    void draw() const override {
        std::cout << "[](" << w << "x" << h << ")";
    }
    double area() const override { return w * h; }
    std::string name() const override { return "Rect"; }
};
class Square final : public Rect { // 正方形是特殊的矩形
public:
    explicit Square(double side) : Rect{side, side} {}
    std::string name() const override { return "Square"; }
};
// ---- 工厂函数 ----
std::unique_ptr<Shape> make_shape(const std::string& type, double a, double b = 0) {
    if (type == "circle") return std::make_unique<Circle>(a);
    if (type == "rect")   return std::make_unique<Rect>(a, b);
    if (type == "square") return std::make_unique<Square>(a);
    return nullptr;
}
int main() {
    std::vector<std::unique_ptr<Shape>> shapes;
    shapes.push_back(make_shape("circle", 5));
    shapes.push_back(make_shape("rect",   3, 4));
    shapes.push_back(make_shape("square", 6));
    double total_area = 0;
    for (const auto& s : shapes) {
        s->draw();
        std::cout << " 名称=" << s->name()
                  << " 面积=" << s->area() << "\n";
        total_area += s->area();
    }
    std::cout << "总面积 = " << total_area << "\n";
    return 0;
}

总结

第19章核心要点


运算符 用途 限制
operator[] 下标访问、关联容器 必须是成员函数
operator() 函数对象(含状态的"函数") 必须是成员函数
operator-> 智能指针、迭代器 必须是成员函数,返回指针或支持->的对象
operator++/-- 迭代器移动 后缀版本用int哑参数区分
operator new/delete 专用内存池 类内版本是隐式 static
operator"" _后缀 用户自定义字面量 后缀必须以_开头

第20章核心要点

继承三原则:
  1. 公有继承(public)表达 is-a 关系
  2. 虚函数 + 指针/引用 = 运行时多态
  3. 有虚函数的类必须有虚析构函数
四个覆盖控制关键字:
  virtual  → "我可以被覆盖"(在基类写)
  = 0      → "我必须被覆盖"(纯虚,使类成为抽象类)
  override → "我在覆盖基类虚函数"(在派生类写,让编译器帮你检查)
  final    → "我不能被继续覆盖/继承"
访问控制规则:
  private   → 只有自己和友元
  protected → 自己、友元、派生类
  public    → 所有人
不要把数据放在 protected:会变成"派生类的全局变量"

C++ 类层次结构与运行时类型识别(RTTI)深度解析

原著:Bjarne Stroustrup《The C++ Programming Language》第21、22章
核心思想:抽象是选择性的忽视。 —— Andrew Koenig

第21章:类层次结构(Class Hierarchies)

21.1 问题引入:整数输入框

假设我们要给程序提供一种"让用户输入整数"的机制。输入方式千变万化:滑块、文本框、拨号盘、语音……
我们希望程序代码不必关心具体是哪种输入方式,只需要面对一个统一的接口 Ival_box(integer value input box,整数值输入框)。

用户(鼠标/键盘)
      |
      v
  [Ival_box]  ←→  应用程序
  - 记录值范围
  - get_value()   ← 应用程序读取
  - set_value()   ← 用户操作触发
  - was_changed() ← 判断是否有变化

21.2 设计方案的演进

21.2.1 方案一:实现继承(Implementation Inheritance)

最直接的想法:用一个基类 Ival_box 包含数据和默认实现,派生类覆盖需要的函数。

#include <iostream>
#include <memory>
// 基类:包含数据成员和默认实现
class Ival_box {
protected:
    int val;           // 当前值
    int low, high;     // 合法范围 [low, high]
    bool changed{false}; // 用户是否修改过(通过set_value)
public:
    // 构造时指定合法范围,初始值为下界
    Ival_box(int ll, int hh) : val{ll}, low{ll}, high{hh} {}
    // 应用程序调用:读取值,并清除"已修改"标记
    virtual int  get_value()       { changed = false; return val; }
    // 用户(通过界面系统)调用:修改值,并设置"已修改"标记
    virtual void set_value(int i)  { changed = true;  val = i; }
    // 应用程序调用:直接重置值,不触发"已修改"
    virtual void reset_value(int i){ changed = false; val = i; }
    // 提示用户进行输入(默认什么也不做)
    virtual void prompt() {}
    // 查询用户自上次读取后是否改动过
    virtual bool was_changed() const { return changed; }
    // 虚析构:保证派生类对象被正确销毁
    virtual ~Ival_box() {}
};

派生类 Ival_slider(滑块)继承并覆盖部分函数:

// 滑块式输入框(具体实现略去图形细节)
class Ival_slider : public Ival_box {
public:
    Ival_slider(int lo, int hi) : Ival_box(lo, hi) {}
    // override 关键字:明确声明这是覆盖基类虚函数,编译器会检查
    int  get_value() override; // 从滑块当前位置读出值
    void prompt()    override; // 在屏幕上显示滑块,提示用户
};

应用程序代码写成面向基类指针的风格,不必知道具体是哪种框:

// 应用程序只知道 Ival_box*,不需要知道具体是滑块还是拨号盘
void interact(Ival_box* pb)
{
    pb->prompt();           // 提示用户(具体行为由派生类决定)
    int i = pb->get_value(); // 读取用户输入的值
    if (pb->was_changed()) {
        std::cout << "用户改变了值: " << i << "\n";
    } else {
        std::cout << "值没有变化\n";
    }
}
void some_fct()
{
    // unique_ptr 自动管理内存,避免忘记 delete
    std::unique_ptr<Ival_box> p1{new Ival_slider{0, 5}};
    interact(p1.get()); // p1.get() 返回裸指针
    // Ival_dial 是另一种派生类(拨号盘式),此处假设已定义
    // std::unique_ptr<Ival_box> p2{new Ival_dial{1, 12}};
    // interact(p2.get());
}

如果我们使用"Big Bucks Inc."的图形库,需要让 Ival_box 继承其 BBwidget

BBwidget
    |
Ival_box
    |--- Ival_slider
    |       |--- Popup_ival_slider
    |       |--- Flashing_ival_slider
    |--- Ival_dial

方案一的问题(批判):

  1. 紧耦合BBwidget 成了设计的一等公民,而它只是实现细节
  2. 多版本噩梦:支持不同图形库(CW/IB/LS)需要维护多套代码
  3. 数据污染val/low/high/changed 这些数据放在基类里,所有派生类都带着,但很多时候(如滑块)不需要单独存储 val
  4. 重编译问题BBwidget 改变大小 → 所有派生类必须重新编译
21.2.2 方案二:接口继承(Interface Inheritance)

核心改进:Ival_box 成为纯抽象类(只有接口,没有数据和实现)

// 纯接口类:= 0 表示纯虚函数,该类不能直接实例化
class Ival_box {
public:
    virtual int  get_value()            = 0; // 必须由派生类实现
    virtual void set_value(int i)       = 0;
    virtual void reset_value(int i)     = 0;
    virtual void prompt()               = 0;
    virtual bool was_changed() const    = 0;
    virtual ~Ival_box() {}  // 虚析构:通过基类指针 delete 派生类对象时调用正确的析构
    // 注意:没有数据成员!没有构造函数!
};

派生类同时继承接口Ival_boxpublic)和实现工具BBwidgetprotected):

// Ival_slider 同时继承接口类和实现辅助类
class Ival_slider : public    Ival_box,   // public:  向外暴露 Ival_box 的接口
                    protected BBwidget    // protected: BBwidget 只是实现细节,用户看不到
{
public:
    Ival_slider(int lo, int hi);
    ~Ival_slider() override;
    int  get_value() override; // 实现接口
    void set_value(int i) override;
    // ...
protected:
    // 覆盖 BBwidget 的虚函数,如 draw()、mouse1hit() 等
    // ...
private:
    // 滑块自己需要的数据(位置、大小等)
    // ...
};
(实线 = public继承,虚线 = protected继承)
Ival_box      BBwidget
    \          /
     \        /(protected)
      Ival_slider
           |
     Popup_ival_slider
     Flashing_ival_slider
Ival_box      BBwidget
    \          /
     \        /(protected)
      Ival_dial

优点:

  • 应用代码完全不需要改动(interact() 仍然照样用)
  • BBwidget 被正确降格为实现细节
  • 数据放在派生类里,符合实际需要
21.2.3 方案三:多种实现(Alternative Implementations)

为了同时支持多个图形库(BB、CW……),用带前缀的不同类名

Ival_box(纯接口)
    |
Ival_slider(抽象接口)
    |----------- BB_ival_slider(继承 BBslider 实现)
    |----------- CW_ival_slider(继承 CWslider 实现)
Ival_dial(抽象接口)
    |----------- BB_ival_dial
    |----------- CW_ival_dial

完整层次:

                    Ival_box
                    /      \
             Ival_slider   Ival_dial
             /    \
    BB_ival_slider  CW_ival_slider
    (用BBslider)  (用CWslider)

这样,不同图形库的实现类可以共存于同一程序中。

21.2.4 工厂模式:集中对象创建

问题:虽然大部分代码只用 Ival_box*,但创建对象时必须写 new BB_ival_slider{…},这些具体名字会散落各处。
解决:引入工厂类(Factory),把对象创建集中管理:

// 抽象工厂:声明创建各种 Ival_box 的接口
class Ival_maker {
public:
    virtual Ival_dial*         dial(int lo, int hi)          = 0; // 创建拨号盘
    virtual Popup_ival_slider* popup_slider(int lo, int hi)  = 0; // 创建弹出滑块
    // ...
};
// 针对 BB 图形库的具体工厂
class BB_maker : public Ival_maker {
public:
    Ival_dial* dial(int lo, int hi) override {
        return new BB_ival_dial{lo, hi}; // 内部才出现具体类名
    }
    Popup_ival_slider* popup_slider(int lo, int hi) override {
        return new BB_popup_ival_slider{lo, hi};
    }
};
// 针对 LS 图形库的具体工厂
class LS_maker : public Ival_maker {
public:
    Ival_dial* dial(int lo, int hi) override {
        return new LS_ival_dial{lo, hi};
    }
    Popup_ival_slider* popup_slider(int lo, int hi) override {
        return new LS_popup_ival_slider{lo, hi};
    }
};
// 用户代码:只依赖 Ival_maker,不知道具体是 BB 还是 LS
void user(Ival_maker& im)
{
    std::unique_ptr<Ival_box> pb{im.dial(0, 99)}; // 创建一个 0~99 的拨号盘
    // ... 使用 pb,代码完全独立于图形库
}
BB_maker BB_impl; // BB 图形库的工厂对象
LS_maker LS_impl; // LS 图形库的工厂对象
void driver()
{
    user(BB_impl); // 用 BB 库
    user(LS_impl); // 用 LS 库
}

工厂模式的价值:对象创建的知识被封装在工厂类里,整个应用程序只需要在一处切换工厂对象,就能换掉整套图形库。

21.3 多重继承(Multiple Inheritance)

21.3.1 多接口继承

一个类可以同时实现多个接口(多个纯抽象类)。抽象类没有数据,可以自由复制或共享,没有复杂性:

IFly      ISwim      IWalk
  \         |         /
        Duck(实现三个接口)

这在 Java/C# 中用 interface 关键字表达,C++ 用纯虚类表达,概念完全相同。

21.3.2 多实现类继承

真实案例:卫星仿真系统

// 卫星:提供轨道计算、属性修改等
class Satellite {
public:
    virtual Pos center() const = 0; // 质心位置
    // ...
};
// 显示对象:提供屏幕绘制等
class Displayed {
public:
    virtual void draw() = 0;
    // ...
};
// 通信卫星:同时是卫星(有轨道),也是显示对象(能画在屏幕上)
class Comm_sat : public Satellite, public Displayed {
public:
    Pos  center() const override; // 覆盖 Satellite::center()
    void draw()         override; // 覆盖 Displayed::draw()
    void transmit();              // Comm_sat 自己的功能
};
Satellite      Displayed
      \          /
       Comm_sat

使用时,Comm_sat 对象可以被当成 Satellite* 传给轨道计算函数,也可以被当成 Displayed* 传给绘图函数:

void highlight(Displayed*);           // 只关心显示
Pos  center_of_gravity(const Satellite*); // 只关心轨道
void g(Comm_sat* p)
{
    highlight(p);              // 编译器自动找到 Displayed 部分
    Pos x = center_of_gravity(p); // 编译器自动找到 Satellite 部分
}
21.3.3 歧义消解(Ambiguity Resolution)

如果两个基类有同名函数:

class Satellite {
public:
    virtual Debug_info get_debug();
};
class Displayed {
public:
    virtual Debug_info get_debug();
};
class Comm_sat : public Satellite, public Displayed {
public:
    // 必须明确覆盖,消除歧义
    Debug_info get_debug() override {
        Debug_info d1 = Satellite::get_debug();  // 显式限定调用哪个
        Debug_info d2 = Displayed::get_debug();
        return merge_info(d1, d2);               // 合并两份信息
    }
};

极端情况:两个基类的同名函数语义完全不同(如牛仔的"draw"=拔枪,窗口的"draw"=绘制),需要加一层中间类:

// 中间层:把 draw() 转发给新的纯虚函数
struct WWindow : Window {
    virtual void win_draw() = 0;          // 新的纯虚函数
    void draw() override final { win_draw(); } // draw() → win_draw()
};
struct CCowboy : Cowboy {
    virtual void cow_draw() = 0;
    void draw() override final { cow_draw(); } // draw() → cow_draw()
};
// 最终类:分别实现两个语义不同的操作
class Cowboy_window : public CCowboy, public WWindow {
public:
    void cow_draw() override; // 拔枪
    void win_draw() override; // 绘制
};
21.3.4 基类的重复出现(Replicated Base Classes)

当一个基类在层次中出现多次时,默认情况下每次出现都有独立的一份子对象:

struct Storable {
    virtual string get_file() = 0;
    virtual void read()  = 0;
    virtual void write() = 0;
    virtual ~Storable() {}
};
class Transmitter : public Storable { public: void write() override; };
class Receiver    : public Storable { public: void write() override; };
// Radio 继承了两个都继承自 Storable 的类
class Radio : public Transmitter, public Receiver {
public:
    string get_file() override;
    void   read()     override;
    void   write()    override {
        Transmitter::write(); // 调用发射器的 write
        Receiver::write();    // 调用接收器的 write
        // ... Radio 特有的写操作 ...
    }
};

此时 Radio 对象的内存布局(概念图):

Radio 对象
├── Transmitter 子对象
│       └── Storable 子对象 A (val, file_name...)
└── Receiver 子对象
        └── Storable 子对象 B (val, file_name...)

两份 Storable,如果 Storable 有状态数据(如文件名),就可能出现不一致。

21.3.5 虚基类(Virtual Base Classes)

如果希望多个继承路径共享同一份基类对象,用 virtual 关键字:

// 带数据的 Storable:记录文件名
class Storable {
public:
    Storable(const string& s);
    virtual void read()  = 0;
    virtual void write() = 0;
    virtual ~Storable();
protected:
    string file_name; // 所有派生类应该共享同一个文件名
    // 禁止复制,确保唯一性
    Storable(const Storable&) = delete;
    Storable& operator=(const Storable&) = delete;
};
// 关键字 virtual:声明虚基类,确保整个层次中只有一份 Storable
class Transmitter : public virtual Storable {
public:
    void write() override;
};
class Receiver : public virtual Storable {
public:
    void write() override;
};
class Radio : public Transmitter, public Receiver {
public:
    void write() override;
    // 注意:Radio 必须直接初始化虚基类 Storable
};

内存布局对比:

-- 普通继承(两份 Storable)--
Radio
├── Transmitter
│       └── Storable [A]
└── Receiver
        └── Storable [B]
-- 虚继承(共享一份 Storable)--
Radio
├── Transmitter ──┐
├── Receiver   ──┤→  Storable [共享,只有一份]
└──(指针指向共享的 Storable)

虚基类的构造顺序规则:最派生类(most derived class)负责初始化虚基类,无论它在继承图中离虚基类有多远。

struct V { V(int i); };  // 虚基类,需要参数构造
struct B : virtual V {
    B() : V{1} {} // B 初始化 V(当 B 是最派生类时生效)
};
class C : virtual V {
public:
    C(int i) : V{i} {} // C 初始化 V
};
class D : virtual public B, virtual public C {
public:
    // D 是最派生类,它必须直接初始化 V,
    // B 和 C 对 V 的初始化在这里被忽略
    D(int i, int j) : V{i}, C{j} {} // OK
};

为什么最派生类要管? 因为如果 B 和 C 都初始化 V,编译器不知道听谁的,所以规定由最终的"老板"类来负责。

21.3.6 虚基类 vs 复制基类

纯接口的抽象类(没有数据)可以安全复制,两种方案在运行时没有根本的速度差异,但有逻辑差异:

对比项 复制基类 虚基类
对象大小 略小(无共享指针) 略大(每个虚基多一个指针)
隐式转换 多份基类时有歧义,转换失败 共享一份,转换唯一
适用场景 纯接口(无数据)类 有状态需要共享的类

21.3.6.1 覆盖虚基类的函数(Mixin 模式)

不同派生类可以各自覆盖虚基类的不同虚函数,合力完成一个完整接口:

class Window {
public:
    virtual void set_color(Color) = 0; // 设置背景色
    virtual void prompt()         = 0; // 用户交互提示
};
class Window_with_border : public virtual Window {
public:
    void set_color(Color) override; // 边框负责颜色方案
};
class Window_with_menu : public virtual Window {
public:
    void prompt() override; // 菜单负责用户交互
};
// My_window 自动拥有两者的实现
class My_window : public Window_with_menu,
                  public Window_with_border {
    // set_color 来自 Window_with_border
    // prompt    来自 Window_with_menu
    // 也可以再覆盖 prompt() 来改进行为
};

这种"每个基类贡献一部分实现"的风格叫做 Mixin(混入)
图示(虚继承用虚线):

Window { set_color(), prompt() }
   /                  \
Window_with_border   Window_with_menu
{ set_color() }       { prompt() }
          \           /
           My_window

21.4 本章建议

  1. unique_ptr/shared_ptr 管理 new 创建的对象,避免内存泄漏
  2. 接口基类不应有数据成员
  3. 用纯抽象类表达接口
  4. 抽象类一定要有虚析构函数
  5. 在大型层次中用 override 关键字明确覆盖意图
  6. 用抽象类实现接口继承,用有实现的基类实现实现继承
  7. 用多重继承表达"特性的联合"
  8. 用多重继承分离接口和实现
  9. 用虚基类表达层次中某些(非全部)类共享的东西

第22章:运行时类型识别(RTTI)

22.1 问题:类型信息的丢失与恢复

图形界面系统(“the system”)只知道自己的类(如 BBwindow),不知道我们的 Ival_box。当系统把对象还给我们时,类型信息就"丢失"了。
RTTI 的核心问题:只有基类指针/引用,如何安全地得到派生类类型的信息?
C++ 提供两个工具:

  • dynamic_cast:安全地转换类型,失败返回 nullptr 或抛异常
  • typeid:获取对象的精确类型信息

22.2 类层次导航

22.2.1 dynamic_cast

基本用法(指针版本,失败返回 nullptr):

#include <iostream>
#include <memory>
class BBwindow {
public:
    virtual ~BBwindow() {}
};
class Ival_box {
public:
    virtual int get_value() = 0;
    virtual ~Ival_box() {}
};
// BB 版滑块:同时是 Ival_box(接口)和 BBwindow(实现)
class BB_ival_slider : public Ival_box,
                       protected BBwindow {
public:
    int get_value() override { return 42; }
};
// 场景:系统回调,给我们一个 BBwindow*,我们想知道它是不是 Ival_box
void my_event_handler(BBwindow* pw)
{
    // dynamic_cast:运行时检查 pw 指向的对象是否有 Ival_box 这部分
    if (auto pb = dynamic_cast<Ival_box*>(pw)) {
        // 成功:pb 是有效的 Ival_box* 指针
        int x = pb->get_value();
        std::cout << "读到值: " << x << "\n";
    } else {
        // 失败:pw 指向的对象不是 Ival_box 的派生类
        std::cout << "不是 Ival_box,忽略\n";
    }
}

三种转换方向的名称:

          基类
          / \
       向上  向下
     (upcast)(downcast)
        /     \
    派生类A   派生类B
         \   /
          交叉
        (crosscast)

转换方向 名称 说明
派生类 → 基类 upcast(上转型) 安全,编译时完成
基类 → 派生类 downcast(下转型) 需要运行时检查
基类A → 基类B(同层) crosscast(交叉转型) 需要运行时检查

引用版本(失败抛出 std::bad_cast):

#include <typeinfo> // for std::bad_cast
void fr(Ival_box& r)
{
    // 断言式转换:我确信 r 引用的是 Ival_slider
    // 如果不是,抛出 std::bad_cast 异常
    try {
        Ival_slider& is = dynamic_cast<Ival_slider&>(r);
        // ... 使用 is ...
    } catch (std::bad_cast&) {
        std::cerr << "类型转换失败\n";
    }
}

何时用指针版本,何时用引用版本?

  • 指针版本:转换失败是正常情况,检查 nullptr,分支处理
  • 引用版本:转换必须成功,失败是程序错误,用异常报告
22.2.2 多重继承下的 dynamic_cast

当一个类在层次中出现多次时,转换可能有歧义:

class Storable { public: virtual ~Storable() {} };
class Component : public virtual Storable { };
class Receiver   : public Component { };
class Transmitter: public Component { };
class Radio      : public Receiver, public Transmitter { };

层次结构:

       Storable(虚基,只有一份)
           |
       Component(出现两次!)
       /         \
  Receiver     Transmitter
       \         /
          Radio
void h1(Radio& r)
{
    Storable* ps = &r;   // OK:Radio 只有唯一一份 Storable(虚基)
    // 试图从 Storable 转换到 Component
    Component* pc = dynamic_cast<Component*>(ps);
    // 结果:pc == nullptr!
    // 原因:Radio 有两份 Component(来自 Receiver 和 Transmitter)
    //       dynamic_cast 不知道要哪一份,返回 nullptr
}
22.2.3 static_cast vs dynamic_cast
void g(Radio& r)
{
    // Receiver 是 Radio 的普通基类(非虚继承)
    Receiver* prec = &r;
    // static_cast:不检查,直接转换(程序员保证正确)
    Radio* pr1 = static_cast<Radio*>(prec); // 可以,未检查
    // dynamic_cast:运行时检查
    Radio* pr2 = dynamic_cast<Radio*>(prec); // 可以,有检查
    // Storable 是虚基类
    Storable* ps = &r;
    // static_cast 不能从虚基类向下转换!
    // Radio* pr3 = static_cast<Radio*>(ps); // 编译错误!
    // dynamic_cast 可以
    Radio* pr4 = dynamic_cast<Radio*>(ps); // OK,运行时检查
}

特性 static_cast dynamic_cast
检查时机 编译期 运行期
虚基类向下转换 不支持 支持
失败行为 未定义行为 返回 nullptr / 抛异常
运行时开销 有(查 vptr)
要求多态类型 是(有虚函数)

22.2.4 对象 I/O 系统:恢复接口

经典使用场景:从流中读对象,然后恢复其真实类型:

#include <iostream>
#include <map>
#include <string>
#include <memory>
#include <stdexcept>
// 对象 I/O 系统的基类:所有可序列化对象都继承它
class Io_obj {
public:
    virtual Io_obj* clone() const = 0; // 用于复制对象
    virtual ~Io_obj() {}
};
// 图形基类(假设已存在)
class Shape {
public:
    virtual void draw() const = 0;
    virtual ~Shape() {}
};
// 圆形:同时继承 Shape 和 Io_obj
class Io_circle : public Shape, public Io_obj {
public:
    // 从流读取初始化(简化示意)
    Io_circle() { std::cout << "[创建圆形]\n"; }
    void draw() const override { std::cout << "绘制圆形\n"; }
    Io_circle* clone() const override { return new Io_circle{*this}; }
    // 注册到 io_map 的工厂函数
    static Io_obj* new_circle() { return new Io_circle{}; }
};
// 类型名 → 工厂函数 的映射表
using Pf = Io_obj*();
std::map<std::string, Pf*> io_map;
// 模拟从流中读取对象(实际应读取流中的类名字符串)
Io_obj* get_obj(const std::string& class_name)
{
    auto it = io_map.find(class_name);
    if (it != io_map.end()) {
        return it->second(); // 调用工厂函数创建对象
    }
    throw std::runtime_error{"未知类型: " + class_name};
}
void user(const std::string& class_name)
{
    // 从"流"中读取对象,只知道它是 Io_obj*
    std::unique_ptr<Io_obj> p{get_obj(class_name)};
    // 用 dynamic_cast 检查它是否是 Shape(是否可以画)
    if (auto sp = dynamic_cast<Shape*>(p.get())) {
        sp->draw(); // 是 Shape,画出来
    } else {
        std::cout << "不是图形,无法绘制\n";
    }
}
int main()
{
    // 注册已知类型
    io_map["Io_circle"] = &Io_circle::new_circle;
    user("Io_circle"); // 输出:[创建圆形] 绘制圆形
    return 0;
}

整个流程:

"Io_circle"(字符串)
      |
      v
io_map 查找 → Io_circle::new_circle()
      |
      v
  Io_obj* p(只知道是 Io_obj)
      |
      v
dynamic_cast<Shape*>(p.get())
      |
   成功?
   /    \
 是      否
 |        |
draw()  错误处理

22.3 双重分派与访问者模式

22.3.1 双重分派(Double Dispatch)

问题:如何基于两个对象的动态类型选择函数?
例子:计算两个图形是否相交,结果取决于两种图形的类型(圆-圆、圆-三角、三角-三角)。
技巧:第一次虚函数调用选 s1 的类型,在那个函数里再用 *this 反转参数调用,完成第二次选择:

#include <iostream>
#include <vector>
#include <utility>
class Circle;
class Triangle;
class Shape {
public:
    // 第一次调用:对 s2 的类型一无所知(用基类引用)
    virtual bool intersect(const Shape&)    const = 0;
    // 第二次调用:s2 的类型已知(Circle 或 Triangle)
    virtual bool intersect(const Circle&)   const = 0;
    virtual bool intersect(const Triangle&) const = 0;
    virtual ~Shape() {}
};
class Circle : public Shape {
public:
    // 第一次分派:s 是什么类型?反转参数再调一次
    bool intersect(const Shape& s)    const override { return s.intersect(*this); }
    bool intersect(const Circle&)     const override {
        std::cout << "圆 ∩ 圆\n"; return true;
    }
    bool intersect(const Triangle&)   const override {
        std::cout << "圆 ∩ 三角\n"; return true;
    }
};
class Triangle : public Shape {
public:
    bool intersect(const Shape& s)    const override { return s.intersect(*this); }
    bool intersect(const Circle&)     const override {
        std::cout << "三角 ∩ 圆\n"; return true;
    }
    bool intersect(const Triangle&)   const override {
        std::cout << "三角 ∩ 三角\n"; return true;
    }
};
int main()
{
    Triangle t;
    Circle   c;
    // 用基类指针,确保运行时才知道类型
    std::vector<std::pair<Shape*, Shape*>> vs{
        {&t, &t}, {&t, &c}, {&c, &t}, {&c, &c}
    };
    for (auto& p : vs) {
        p.first->intersect(*p.second);
    }
    return 0;
}
// 输出:
// 三角 ∩ 三角
// 三角 ∩ 圆
// 圆 ∩ 三角
// 圆 ∩ 圆

双重分派的执行流程(以 t.intersect(c) 为例):

p.first = Triangle*,  p.second = Circle*
第1步:p.first->intersect(*p.second)
        ↓ 虚函数调用,选 Triangle::intersect(const Shape&)
        参数是 Shape&(动态类型是 Circle)
Triangle::intersect(const Shape& s)
        ↓ s.intersect(*this) 反转!
        s 的动态类型是 Circle,this 是 Triangle
第2步:Circle::intersect(const Triangle&)
        ↓ 精确匹配,直接调用
        输出:"三角 ∩ 圆"

缺点: 每添加一种新图形,必须修改所有已有图形类,代码量指数增长。

22.3.2 访问者模式(Visitor Pattern)

访问者模式是双重分派的改进版:把"操作"从节点类中分离出去,放到独立的 Visitor 类里。
核心设计:

  • 节点层次(Node hierarchy):每个节点类只需要一个 accept(Visitor&) 函数
  • 访问者层次(Visitor hierarchy):不同操作各自是一个 Visitor 的派生类
#include <iostream>
#include <vector>
#include <utility>
// 前置声明
class Visitor;
// 节点基类:只有一个 accept 方法
class Node {
public:
    virtual void accept(Visitor&) = 0;
    virtual ~Node() {}
};
// 节点类型:表达式
class Expr : public Node {
public:
    void accept(Visitor& v) override;
};
// 节点类型:语句
class Stmt : public Node {
public:
    void accept(Visitor& v) override;
};
// 访问者基类:声明对每种节点的操作接口
class Visitor {
public:
    virtual void accept(Expr&) = 0; // 处理 Expr 的操作
    virtual void accept(Stmt&) = 0; // 处理 Stmt 的操作
    virtual ~Visitor() {}
};
// accept 的实现:把自己(*this)传给访问者,触发第二次分派
void Expr::accept(Visitor& v) { v.accept(*this); } // 告诉 v:"我是 Expr"
void Stmt::accept(Visitor& v) { v.accept(*this); } // 告诉 v:"我是 Stmt"
// 具体操作 1
class Do1_visitor : public Visitor {
public:
    void accept(Expr&) override { std::cout << "操作1 处理 Expr\n"; }
    void accept(Stmt&) override { std::cout << "操作1 处理 Stmt\n"; }
};
// 具体操作 2
class Do2_visitor : public Visitor {
public:
    void accept(Expr&) override { std::cout << "操作2 处理 Expr\n"; }
    void accept(Stmt&) override { std::cout << "操作2 处理 Stmt\n"; }
};
int main()
{
    Expr e;
    Stmt s;
    Do1_visitor do1;
    Do2_visitor do2;
    // 用基类指针,确保运行时分派
    std::vector<std::pair<Node*, Visitor*>> vn{
        {&e, &do1}, {&s, &do1}, {&e, &do2}, {&s, &do2}
    };
    for (auto& p : vn) {
        p.first->accept(*p.second);
    }
    return 0;
}
// 输出:
// 操作1 处理 Expr
// 操作1 处理 Stmt
// 操作2 处理 Expr
// 操作2 处理 Stmt

访问者模式的执行流程:

调用:node->accept(visitor)
           ↓ 第1次虚函数分派:选 Expr::accept 或 Stmt::accept
      v.accept(*this)        ← *this 类型已知(Expr 或 Stmt)
           ↓ 第2次虚函数分派:选 Do1_visitor::accept(Expr&) 等
      执行具体操作

添加新操作:只需要新增一个 Visitor 派生类,不需要修改任何 Node 类

对比 双重分派 访问者模式
侵入性 高(修改所有类) 低(只需 accept() 函数)
添加新类型 修改所有操作 修改所有 Visitor
添加新操作 修改所有类型 只需新增 Visitor
适合场景 类型少、操作少 类型稳定、操作多变

22.4 构造和析构期间的类型

对象在构造过程中"逐渐变成"最终类型,析构时"逐渐退回"。
在构造函数中调用虚函数,得到的是当前已构造层次的版本,不是最派生类的版本:

构造顺序(从下往上):
Storable 构造 → Component 构造 → Receiver 构造 → Radio 构造
在 Component 的构造函数里调虚函数 f():
  → 调的是 Component::f(),不是 Radio::f()
  (因为 Radio 部分还没构造好)

建议:不要在构造函数或析构函数中调用虚函数。

22.5 类型标识(typeid

当需要精确的类型名称(而非仅仅"是否是某基类")时,用 typeid

#include <iostream>
#include <typeinfo>
struct Poly {
    virtual void f() {} // 有虚函数 → 多态类型
};
struct Non_poly {
    // 没有虚函数 → 非多态类型
};
struct D1 : Poly    {};
struct D2 : Non_poly {};
void show_type(Non_poly& npr, Poly& pr)
{
    // typeid(非多态类型) → 编译期确定,总是返回声明类型
    std::cout << typeid(npr).name() << "\n"; // 输出 "Non_poly"
    // typeid(多态类型) → 运行期确定,返回实际类型
    std::cout << typeid(pr).name()  << "\n"; // 若 pr 实际是 D1,输出 "D1"
}
int main()
{
    D1 d1;
    D2 d2;
    show_type(d2, d1); // 输出:Non_poly   D1(实现相关,可能带修饰符)
    return 0;
}

type_info 类的主要成员:

class type_info {
public:
    bool operator==(const type_info&) const noexcept; // 类型相等判断
    bool operator!=(const type_info&) const noexcept;
    bool before(const type_info&) const noexcept;     // 用于 map 排序
    size_t hash_code() const noexcept;                // 用于 unordered_map
    const char* name() const noexcept;                // 返回类型名字符串
    // 禁止复制
    type_info(const type_info&) = delete;
    type_info& operator=(const type_info&) = delete;
};

typeid 的关键使用场景:扩展类型信息(Extended Type Information)

#include <typeinfo>
#include <map>
#include <string>
// 假设每种类型对应一个布局描述
struct Layout { /* ... */ };
std::map<std::string, Layout> layout_table; // 类名 → 布局信息
void f(Poly* p) // p 指向某个 Poly 的派生类
{
    // typeid(*p).name() 运行期获取真实类名
    std::string type_name = typeid(*p).name();
    Layout& x = layout_table[type_name]; // 查表获取该类型的布局
    // ... 使用布局信息 ...
}

这样,可以在不修改类定义的前提下,为任意类型关联额外信息。

22.6 RTTI 的正确使用与滥用

滥用:用 RTTI 模拟 switch 语句(这是反模式!)

// 错误示范:应该用虚函数,不要这样写
void rotate(const Shape& r)
{
    if (typeid(r) == typeid(Circle)) {
        // 圆不旋转,什么也不做
    } else if (typeid(r) == typeid(Triangle)) {
        // ... 旋转三角形 ...
    } else if (typeid(r) == typeid(Square)) {
        // ... 旋转正方形 ...
    }
}
// 正确做法:用虚函数
class Shape {
public:
    virtual void rotate() = 0; // 每个子类自己知道怎么旋转
};

何时应该用 RTTI:

  1. 与不可修改的库交互(如上面的事件处理场景)
  2. 对象 I/O 系统(从流恢复类型)
  3. 需要精确类型名做日志/调试
    何时不该用 RTTI:
  4. 可以用虚函数解决的问题
  5. typeid 写成变相的类型分支(switch)
  6. 用通用基类 Object* 代替模板(C++ 有泛型,不需要这样)

22.7 本章建议

  1. 用虚函数保证无论通过哪个接口调用,行为一致
  2. 不可避免时才用 dynamic_cast 导航类层次
  3. 向下转型用 dynamic_cast,保证类型安全
  4. 转换必须成功时,用引用版本 dynamic_cast<T&>(失败抛异常)
  5. 转换可能失败时,用指针版本 dynamic_cast<T*>(失败返回 nullptr)
  6. 两种类型的操作用双重分派或访问者模式
  7. 不要在构造/析构期间调用虚函数
  8. typeid 实现扩展类型信息
  9. typeid 找类型,而不是找接口
  10. 优先用虚函数,而非反复使用 typeid/dynamic_cast 做类型分支

总体设计原则总结

接口设计原则
  ↓
纯虚函数组成的抽象类 = 接口(类似 Java 的 interface)
      |
      |--- 用 public 继承:向外暴露接口
      |--- 用 protected 继承:引入实现工具(隐藏细节)
      |
工厂类集中管理对象创建
      |
运行时操作
  ├── dynamic_cast:安全地在层次中导航
  ├── typeid:获取精确类型名
  └── 虚函数(首选):无需知道类型,自动选择正确行为

C++ 模板与泛型编程深度解析

原著:Bjarne Stroustrup《The C++ Programming Language》第23、24章
核心思想:模板让我们用类型作为参数进行编程,实现真正的泛型。

第23章:模板(Templates)

23.1 什么是模板?为什么需要它?

想象一下:你写了一个"整数排序"函数,又需要写一个"字符串排序"函数,再写一个"浮点数排序"……每次都要复制粘贴,只改类型名。这太浪费了。
模板的解决方案:让"类型"本身成为参数,一次写,到处用。

普通函数:固定类型的操作
模板函数:类型也是参数,可以"填入"任意类型

模板在标准库中无处不在:stringvectormapunique_ptrthread……都是模板。
模板提供三种能力:

  1. 传递类型作为参数:不丢失任何信息,编译器可以充分内联优化
  2. 延迟类型检查:在实例化时才检查,可以融合不同来源的信息
  3. 传递常量值作为参数:支持编译期计算

23.2 从一个例子开始:字符串模板

23.2.1 问题场景

我们有一个 String<char> 字符串类,但有时需要 String<wchar_t>(宽字符),甚至 String<Jchar>(日文字符)。用模板一次搞定:

// 模板声明:C 是类型参数(占位符),可以是任何字符类型
template<typename C>   // typename C 等价于 class C,完全相同
class String {
public:
    String();                       // 默认构造函数
    explicit String(const C* p);    // 从 C 风格字符串构造
    String(const String&);          // 拷贝构造
    String& operator=(const String&); // 拷贝赋值
    C& operator[](int n) { return ptr[n]; } // 下标访问(未检查边界)
    String& operator+=(C c);        // 在末尾追加一个字符 c
private:
    static const int short_max = 15; // 短字符串优化阈值
    int sz;                          // 字符串当前长度
    C* ptr;                          // 指向 sz 个 C 类型字符的指针
};

使用方式:

String<char>          cs;  // 普通 char 字符串
String<unsigned char> us;  // 无符号 char 字符串
String<wchar_t>       ws;  // 宽字符字符串
struct Jchar { /* 日文字符的定义 */ };
String<Jchar>         js;  // 日文字符字符串

String<char> 就像手写的 char 字符串类一样,没有任何运行时额外开销。
标准库中已经有类似的东西:

// 标准库中:
using string = std::basic_string<char>;
// string 就是 basic_string<char> 的别名,basic_string 是一个模板
23.2.2 在类外定义成员函数

模板的成员函数在类外定义时,必须加上模板前缀:

// 在类外定义构造函数:需要重复写 template<typename C>
template<typename C>
String<C>::String()       // String<C> 的构造函数(String<C>:: 是作用域)
    : sz{0}, ptr{ch}      // 初始化列表
{
    ch[0] = {};           // 用对应字符类型的"零值"终止(如 char 的 '\0')
}
// 在类外定义 += 操作符
template<typename C>
String<C>& String<C>::operator+=(C c)
{
    // ... 在末尾追加字符 c 的逻辑 ...
    return *this;
}

注意:String<C>::String 是构造函数名,在 String<C> 的作用域内可以省略 <C>

23.2.3 模板实例化(Template Instantiation)

实例化:用具体类型替换模板参数,生成实际代码的过程。生成的每个版本叫做特化(specialization)

String<char>  cs;  // 触发实例化:编译器生成 String<char> 类
String<Jchar> js;  // 触发实例化:编译器生成 String<Jchar> 类
void f()
{
    cs = "hello"; // 还触发 String<char>::operator=(char*) 的实例化
    // 注意:只有实际使用的成员函数才会被生成!
    // 没有被调用的成员函数不会产生任何代码
}

关键优化:模板的成员函数只在被使用时才生成,这可以减少而不是增加代码量。

23.3 类型检查

23.3.1 类型等价规则

模板参数不同 → 生成不同的类型,即使参数"看起来相似":

using Uchar = unsigned char;   // Uchar 是 unsigned char 的别名(不是新类型)
using uchar = unsigned char;   // uchar 也是别名
String<Uchar> s4;  // 和 String<unsigned char> 完全相同的类型
String<uchar> s5;  // 和 String<unsigned char> 完全相同的类型
String<char>  s6;  // 和上面不同!char 和 unsigned char 是不同类型
// 非类型模板参数(整数):编译器会计算常量表达式
template<typename T, int N>
class Buffer { /* ... */ };
Buffer<char, 10>    b1;
Buffer<char, 20-10> b2; // 20-10 = 10,和 b1 类型相同!
Buffer<char, 20>    b3; // 不同,N 不同

重要警告:派生关系不会自动传递给模板!

// Circle 是 Shape 的派生类
Shape* p = new Circle{};      // OK:Circle* 可以转换为 Shape*
// 但是模板不行!
vector<Shape>* q = new vector<Circle>{}; // 错误!没有这种转换!
// 原因:如果允许,会导致类型安全问题(向 vector<Shape> 里放非 Circle 的 Shape)
23.3.2 错误检测时机

模板的错误检测分两个阶段:
阶段一:定义时(部分错误立即被发现)

template<typename T>
struct Link {
    Link* pre;
    Link* suc   // 错误:缺少分号(语法错误,立即报告)
    T val;
};
template<typename T>
class List {
    Link<T>* head;
public:
    List() : head{7} {}              // 错误:指针不能用整数 7 初始化(类型错误)
    List(const T& t) : head{new Link<T>{0, o, t}} {} // 错误:o 未定义(名字查找错误)
};

阶段二:实例化时(依赖模板参数的错误)

class Rec {
    string name;
    string address;
    // 注意:没有定义 << 输出操作符
};
void f(const List<int>& li, const List<Rec>& lr)
{
    li.print_all(); // OK:int 有 << 操作符
    lr.print_all(); // 错误!实例化时才发现:Rec 没有 << 操作符
}

23.4 类模板的成员

类模板可以拥有与普通类完全相同类型的成员:

23.4.1 数据成员
template<typename T>
struct X {
    int m1 = 7;   // 普通数据成员,有默认值
    T   m2;        // 类型为 T 的数据成员
    X(const T& x) : m2{x} {} // 构造函数:初始化 m2
};
X<int>    xi{9};           // m1=7, m2=9
X<string> xs{"Rapperswil"}; // m1=7, m2="Rapperswil"
23.4.2 成员函数
template<typename T>
struct X {
    void mf1() { /* 类内定义 */ }  // 类内定义(相当于 inline)
    void mf2();                     // 只有声明,定义在外面
};
// 类外定义:必须重复模板前缀
template<typename T>
void X<T>::mf2() { /* 类外定义 */ }
23.4.3 成员类型别名

类型别名是泛型编程的重要工具,让外部代码可以"查询"模板类的相关类型:

template<typename T>
class Vector {
public:
    using value_type = T;              // 元素类型(外部可通过 Vector<int>::value_type 访问)
    using iterator   = Vector_iter<T>; // 迭代器类型
    // ...
};
// 外部代码使用:
void f()
{
    Vector<int>::value_type x = 42; // x 的类型是 int
}

这些成员别名又叫关联类型(associated types),是标准库容器设计的核心。

23.4.4 静态成员
template<typename T>
struct X {
    static constexpr Point p{100, 250}; // 编译期常量,可以类内初始化
    static const int m1 = 7;            // 整型 const,可以类内初始化
    // static int m2 = 8;              // 错误!非 const 静态成员必须在类外定义
    static int m3;                      // 声明,定义在类外
    static void f1() { /* ... */ }      // 类内定义的静态函数
    static void f2();                   // 声明,定义在类外
};
// 类外定义:
template<typename T> int  X<T>::m3 = 99;
template<typename T> void X<T>::f2() { /* ... */ }

静态成员只在被使用时才被定义(链接时才确定)。

23.4.5 成员类型(嵌套类)
template<typename T>
struct X {
    enum E1 { a, b };    // 枚举定义在类内
    enum class E3;        // 只声明,在类外定义
    struct C1 { /* ... */ }; // 嵌套类,定义在类内
    struct C2;               // 只声明,在类外定义
};
// 类外定义嵌套类型(注意模板前缀):
template<typename T>
enum class X<T>::E3 { a, b };
template<typename T>
struct X<T>::C2 { /* ... */ };
23.4.6 成员模板(Template within Template)

一个模板类可以有成员模板:该成员本身也是模板,有自己的类型参数。
经典用例:complex 数复数类,允许不同精度之间的转换:

template<typename Scalar>
class complex {
    Scalar re, im;  // 实部和虚部
public:
    // 普通默认构造
    complex() : re{}, im{} {}
    // 成员模板构造:从任意类型 T 构造(T 可以转换为 Scalar 时)
    template<typename T>
    complex(T rr, T ii = 0) : re{rr}, im{ii} {}
    // 默认拷贝构造(明确声明,见下面的注意)
    complex(const complex&) = default;
    // 成员模板拷贝构造:从 complex<T> 构造,T != Scalar 时使用
    template<typename T>
    complex(const complex<T>& c) : re{c.real()}, im{c.imag()} {}
    // 用 {} 初始化:如果 T→Scalar 会窄化(如 double→float),编译器会报错!
};
// 使用示例:
complex<float>  cf;
complex<double> cd{cf};   // OK:float → double(扩宽),没问题
// complex<float> cf2{cd}; // 错误:double → float(窄化),用 {} 语法被拒绝
complex<double> cd2{2.0F, 3.0F}; // OK:float → double

重要规则:模板构造函数永远不会自动生成拷贝构造函数,所以要显式写 = default

23.4.6.2 模板成员不能是虚函数
class Shape {
    template<typename T>
    virtual bool intersect(const T&) const = 0; // 错误!虚函数不能是模板!
};

为什么?虚函数表(vtable)在编译时大小固定,而模板会随着不同 T 的使用无限扩展,两者矛盾。

23.4.6.3 嵌套类型的陷阱

把类型定义为模板的成员,该类型会依赖模板的所有参数,即使它根本用不着某些参数。
问题示例:

template<typename T, typename Allocator>
class List {
public:
    // Iterator 只用到了 T,但形式上依赖 Allocator
    class Iterator {
        Link<T>* current_position;
    public:
        // ...
    };
    Iterator begin();
    Iterator end();
};

后果:List<int, My_alloc>::IteratorList<int, Your_alloc>::Iterator不同类型,无法写通用函数:

// 无法写这样的函数(List 需要两个参数):
void fct(List<int>::Iterator b, List<int>::Iterator e) // 错误!
{
    auto p = find(b, e, 17);
}

解决方案:把 Iterator 移出模板类,只参数化它实际用到的类型参数:

// Iterator 只依赖 T,不依赖 Allocator
template<typename T>
struct Iterator {
    Link<T>* current_position;
    // ...
};
template<typename T, typename Allocator>
class List {
public:
    Iterator<T> begin(); // Iterator<T>,不依赖 Allocator
    Iterator<T> end();
};
// 现在可以写通用函数了!
void fct(Iterator<int> b, Iterator<int> e) // OK
{
    auto p = find(b, e, 17);
}

原则:只有当嵌套类型真正依赖类模板的所有参数时,才把它定义为成员。

23.4.7 友元(Friends)

模板类可以声明友元函数,友元函数本身也可以是模板:

template<typename T> class Matrix; // 前置声明
template<typename T>
class Vector {
    T v[4];
public:
    // 声明 operator* 为友元(<> 表示这是一个模板函数,而非普通函数)
    friend Vector operator*<>(const Matrix<T>&, const Vector&);
};
template<typename T>
class Matrix {
    Vector<T> v[4];
public:
    friend Vector<T> operator*<>(const Matrix&, const Vector<T>&);
};
// 友元函数的定义:可以直接访问 Matrix 和 Vector 的私有成员
template<typename T>
Vector<T> operator*(const Matrix<T>& m, const Vector<T>& v)
{
    Vector<T> r;
    // 直接访问 m.v[i] 和 v.v[i](私有成员)
    return r;
}

23.5 函数模板

23.5.1 基本用法

函数模板的经典例子:通用排序

#include <vector>
#include <string>
#include <iostream>
// 函数模板声明:T 是元素类型
template<typename T>
void sort(std::vector<T>& v); // 对任意类型的 vector 排序
// 函数模板定义(Shell 排序算法)
template<typename T>
void sort(std::vector<T>& v)
{
    const size_t n = v.size();
    // Shell 排序:缩小增量排序
    for (int gap = n/2; 0 < gap; gap /= 2)
        for (int i = gap; i < n; i++)
            for (int j = i-gap; 0 <= j; j -= gap)
                if (v[j+gap] < v[j]) {   // 用 < 比较(要求 T 有 < 操作)
                    T temp  = v[j];
                    v[j]    = v[j+gap];
                    v[j+gap]= temp;
                }
}
int main()
{
    std::vector<int>    vi{5, 3, 1, 4, 2};
    std::vector<std::string> vs{"banana", "apple", "cherry"};
    sort(vi); // 编译器自动推导 T = int
    sort(vs); // 编译器自动推导 T = string
    for (int x : vi) std::cout << x << " ";
    std::cout << "\n";
    for (auto& s : vs) std::cout << s << " ";
    std::cout << "\n";
    return 0;
}
// 输出:
// 1 2 3 4 5
// apple banana cherry

与 §12.5 中用函数指针实现的排序相比,模板版本:

  • 更简洁:不需要传入比较函数指针
  • 更快:编译器知道 < 的具体实现,可以内联,无间接函数调用
    添加自定义比较操作(用默认模板参数):
#include <functional> // std::less, std::greater
// Compare 有默认值 std::less<T>(即默认用 < 升序排序)
template<typename T, typename Compare = std::less<T>>
void sort(std::vector<T>& v)
{
    Compare cmp;  // 创建比较对象(默认是 std::less<T>)
    const size_t n = v.size();
    for (int gap = n/2; 0 < gap; gap /= 2)
        for (int i = gap; i < n; i++)
            for (int j = i-gap; 0 <= j; j -= gap)
                if (cmp(v[j+gap], v[j])) // 用 cmp 代替直接用 <
                    std::swap(v[j], v[j+gap]);
}
// 自定义大小写不敏感的字符串比较
struct No_case {
    bool operator()(const std::string& a, const std::string& b) const
    {
        // 简单示意:实际需要转小写比较
        return a < b;
    }
};
int main()
{
    std::vector<int> vi{5, 3, 1, 4, 2};
    std::vector<std::string> vs{"Banana", "apple", "Cherry"};
    sort(vi);                          // 用默认 less<int>,升序
    sort<int, std::greater<int>>(vi);  // 用 greater<int>,降序(必须显式指定 T)
    sort(vs);                          // 用默认 less<string>
    sort<std::string, No_case>(vs);    // 用自定义比较
    return 0;
}
23.5.2 函数模板参数推导

编译器可以从函数实参自动推导模板参数类型:

template<typename T, int max>
struct Buffer { T buf[max]; };
template<typename T, int max>
T& lookup(Buffer<T, max>& b, const char* p);
// 调用时:编译器从 buf 的类型 Buffer<string,128> 推导出 T=string, max=128
Buffer<std::string, 128> buf;
std::string& r = lookup(buf, "key"); // T 被推导为 string,max 被推导为 128

推导规则:同一个模板参数从多个实参推导时,结果必须一致:

template<typename T>
void f(T i, T* p);
int x = 5;
f(x, &x);    // OK:两次都推导 T = int,一致
// f(x, "str"); // 错误:第一次 T=int,第二次 T=char,矛盾!

引用推导(左值/右值区分)

template<typename T>
class Xref {
public:
    Xref(int i, T*  p)  : index{i}, elem{p},           owned{true}  {} // 接管指针所有权
    Xref(int i, T&  r)  : index{i}, elem{&r},           owned{false} {} // 引用(不拥有)
    Xref(int i, T&& r)  : index{i}, elem{new T{std::move(r)}}, owned{true}  {} // 移动构造
    ~Xref() { if (owned) delete elem; }
private:
    int  index;
    T*   elem;
    bool owned;
};
std::string x{"There and back again"};
Xref<std::string> r1{7, "Here"};        // "Here" 是右值 → 调用 Xref(int,T&&),移动构造
Xref<std::string> r2{9, x};             // x 是左值  → 调用 Xref(int,T&),只引用
Xref<std::string> r3{3, new std::string{"There"}}; // 指针 → 调用 Xref(int,T*),接管所有权

推导规则:

  • 左值 x → 推导为 T&
  • 右值 "Here" → 推导为 T(无引用修饰)
23.5.3 函数模板的重载

函数模板可以重载,也可以和普通函数共存:

#include <complex>
#include <cmath>
#include <iostream>
template<typename T>
T sqrt(T x);                          // 模板版本(通用)
template<typename T>
std::complex<T> sqrt(std::complex<T> x); // 对 complex 的特化模板
double sqrt(double x);                // 普通函数版本
void f(std::complex<double> z)
{
    sqrt(2);     // 推导 T=int → sqrt<int>(int)
    sqrt(2.0);   // 精确匹配普通函数 sqrt(double),优先于模板
    sqrt(z);     // 推导 T=double → sqrt<double>(complex<double>)
}

重载决议顺序(简化版):

1. 对每个函数模板,推导出最佳特化
2. 如果两个模板都能匹配,选更特化(specialized)的那个
3. 在所有候选(模板特化 + 普通函数)中做正常重载决议
4. 普通函数和模板特化同样好时,优先选普通函数

歧义情况:

template<typename T>
T max(T, T);
void k()
{
    max(1, 2);      // OK:max<int>(1,2)
    max('a', 'b');  // OK:max<char>('a','b')
    // max('a', 1); // 错误:歧义!max<char> 还是 max<int>?
    // max(2.7, 4); // 错误:歧义!max<double> 还是 max<int>?
}

消除歧义的方法:

// 方法一:显式指定模板参数
max<int>('a', 1);     // 明确是 max<int>
max<double>(2.7, 4);  // 明确是 max<double>
// 方法二:提供具体的普通函数重载
inline int    max(int i,    int j)    { return max<int>(i, j); }
inline double max(double d, double x) { return max<double>(d, x); }
23.5.3.2 SFINAE:替换失败不是错误

SFINAE = Substitution Failure Is Not An Error(替换失败不是错误)

// 模板 #1:要求 Iter 有成员类型 value_type
template<typename Iter>
typename Iter::value_type mean(Iter first, Iter last);
// 模板 #2:对指针类型的重载
template<typename T>
T mean(T* first, T* last);
void f(std::vector<int>& v, int* p, int n)
{
    auto x = mean(v.begin(), v.end()); // 调用 #1:vector::iterator 有 value_type
    auto y = mean(p, p+n);             // 调用 #2:int* 没有 value_type → #1 的替换失败
                                       // SFINAE:替换失败不是错误,只是把 #1 从候选集中排除
                                       // 然后选 #2
}

如果没有 SFINAE,mean(p, p+n) 会直接报错说 int* 没有 value_type,即使有 #2 可以用。SFINAE 让编译器"跳过"不合适的重载,继续寻找合适的。
SFINAE 是后面 enable_ifstd::is_* 等技术的基础(第28章)。

23.6 模板别名(Template Aliases)

using 语法可以给模板(部分绑定参数)创建别名:

// 绑定所有参数 → 得到具体类型
using Cvec = std::vector<char>;
Cvec vc = {'a', 'b', 'c'}; // vc 是 vector<char>
// 绑定部分参数 → 得到新模板(只绑定第二个参数,第一个仍是参数)
template<typename T>
using Vec = std::vector<T, My_alloc<T>>; // 使用自定义分配器
Vec<int> fib = {0, 1, 1, 2, 3, 5, 8, 13}; // fib 是 vector<int, My_alloc<int>>

别名的本质是透明的:用别名和用原始模板完全等价,类型完全相同:

std::vector<char, std::allocator<char>> vc2 = vc; // vc2 和 vc 类型相同
std::vector<int, My_alloc<int>> verbose = fib;      // verbose 和 fib 类型相同

实用技巧:用别名隐藏复杂的实现细节,给"精确整数类型"起别名:

// 基础模板:int_exact_traits<N>::type 是 N 位整数类型
template<int N>
struct int_exact_traits { using type = int; }; // 默认
template<>
struct int_exact_traits<8>  { using type = char;  }; // 8 位特化
template<>
struct int_exact_traits<16> { using type = short; }; // 16 位特化
// 用别名简化使用:
template<int N>
using int_exact = typename int_exact_traits<N>::type;
int_exact<8>  a = 7;  // a 是 char 类型(8位)
int_exact<16> b = 42; // b 是 short 类型(16位)

23.7 源码组织

问题:模板的定义应该放在哪里?
有三种策略:

策略一(最常用):模板定义放在头文件中,每个用到模板的翻译单元都包含该头文件。
策略二:头文件只放声明,定义放在同一翻译单元的后面(#include 进来)。
策略三:声明和定义分离编译(C++不支持!)

为什么不能分离编译(策略三不可用)?
编译器需要在看到模板使用点时,同时看到完整的模板定义,才能生成代码。与普通函数不同,普通函数可以"先声明后链接",模板不行。
推荐做法(策略一)

// 文件:out.h(包含完整定义)
#include <iostream>
template<typename T>
void out(const T& t)
{
    std::cerr << t; // 直接写定义
}
// 文件:user1.cpp
#include "out.h" // 包含完整定义
// 使用 out()...
// 文件:user2.cpp
#include "out.h" // 同样包含完整定义
// 使用 out()...

两个文件都包含了 out 的定义,但编译器/链接器会确保只生成一份代码(类似 inline 函数)。

23.7.1 用普通函数封装模板(减少依赖)

当模板库复杂或频繁变动时,可以用普通函数封装:

// 只在这个 .cpp 文件里包含复杂的模板库头文件
#include <numeric>   // std::accumulate
// 提供一个简洁的非模板接口
double accum(const std::vector<double>& v)
{
    return std::accumulate(v.begin(), v.end(), 0.0);
}

外部代码只需要:

// 声明(不需要 #include <numeric>!)
double accum(const std::vector<double>& v);

好处:模板库改变时,只需重新编译一个 .cpp 文件,其他代码不受影响。

23.8 本章建议

  1. 用模板表达适用于多种参数类型的算法
  2. 用模板表达容器类
  3. template<class T>template<typename T> 完全等价
  4. 先设计和调试具体版本,再提炼成模板
  5. 模板类型安全,但检查时机较晚(实例化时)
  6. 设计模板时仔细考虑对参数类型的要求(概念)
  7. 可复制的类模板,给它一个非模板拷贝构造和赋值
  8. 虚成员函数不能是模板成员
  9. 嵌套类型只在它真正依赖所有模板参数时才定义为成员
  10. 用函数模板推导类模板参数类型(如 make_pair
  11. 用别名简化符号,隐藏实现细节
  12. 模板不能分离编译,每个翻译单元都要包含定义

第24章:泛型编程(Generic Programming)

24.1 什么是泛型编程?

模板提供三种核心能力:

  1. 类型作为参数传递:不丢失信息 → 编译器可以充分内联,性能极佳
  2. 延迟类型检查(实例化时):可以融合不同上下文的信息
  3. 常量值作为参数:支持编译期计算
    泛型编程:专注于设计、实现和使用通用算法(general algorithms)的编程风格。
    C++ 的模板提供编译时的参数多态(parametric polymorphism),而虚函数提供运行时的子类型多态(subtype polymorphism)。
    鸭子类型(Duck typing):模板不关心类型是什么,只关心它能做什么。“如果它走路像鸭子,叫声像鸭子,它就是鸭子。” 模板就是这样检查类型的——检查它是否有需要的操作,而不是检查它的继承关系。

24.2 算法的提炼(Lifting)

提炼(Lifting):从一个或多个具体算法出发,归纳出通用算法的过程。
下面通过一个完整示例来演示提炼的全过程:
起点:两个具体的累加函数

具体函数一:对 double 数组求和
具体函数二:对 int 链表求和
         ↓ 提炼
通用算法:accumulate

第一步:识别共性

// 具体实现 1:对 double 数组求和
double add_all(double* array, int n)
{
    double s{0};
    for (int i = 0; i < n; ++i)
        s = s + array[i];
    return s;
}
// 具体实现 2:对链表中的 int 求和
struct Node { Node* next; int data; };
int sum_elements(Node* first, Node* last)
{
    int s = 0;
    while (first != last) {
        s     += first->data;
        first  = first->next;
    }
    return s;
}

两段代码的共同结构(伪代码):

T sum(data) {
    T s = 0;                     // 初始化累加器
    while (还没到末尾) {
        s = s + 当前元素值;       // 累加
        移动到下一个元素;
    }
    return s;
}

需要三种访问序列的操作:

  • 判断"还没到末尾"(!=
  • 读取当前值(*
  • 移动到下一个(++
    第二步:参数化,得到 sum
// 第一次提炼:参数化序列的遍历方式
template<typename Iter, typename Val>
Val sum(Iter first, Iter last)
{
    Val s = 0; // 初始值固定为 0,有局限性
    while (first != last) {
        s = s + *first; // * 读当前值,first 是迭代器
        ++first;        // 移到下一个
    }
    return s;
}

对数组使用(指针本身就是迭代器):

double ad[] = {1, 2, 3, 4};
double s = sum<double*>(ad, ad+4); // T=double,Iter=double*

第三步:去掉硬编码的初始值 0

// 第二次提炼:让调用者提供初始值,同时自动推导 Val 的类型
template<typename Iter, typename Val>
Val accumulate(Iter first, Iter last, Val s) // s 是初始值(由调用者提供)
{
    while (first != last) {
        s = s + *first;
        ++first;
    }
    return s;
}
double ad[] = {1, 2, 3, 4};
double s1 = accumulate(ad, ad+4, 0.0); // Val=double,结果累加为 double
double s2 = accumulate(ad, ad+4, 0);   // Val=int,结果截断为 int!

第四步:把"操作"也参数化

// 第三次提炼:把 + 也作为参数,可以做乘法、最大值等任意操作
template<typename Iter, typename Val, typename Oper>
Val accumulate(Iter first, Iter last, Val s, Oper op)
{
    while (first != last) {
        s = op(s, *first); // 用 op 代替 +
        ++first;
    }
    return s;
}
double ad[] = {1, 2, 3, 4};
// 求和(1+2+3+4 = 10)
double s1 = accumulate(ad, ad+4, 0.0, std::plus<double>{});
// 求积(1*2*3*4 = 24,初始值为 1 而非 0!)
double s2 = accumulate(ad, ad+4, 1.0, std::multiplies<double>{});

提炼过程图示:

add_all(double*, n)          sum_elements(Node*, Node*)
    |                               |
    +----------- 识别共性 ----------+
                    |
             val = init
             while first != last
                 val = op(val, *first)
                 ++first
                    |
         参数化 Iter, Val, Oper
                    |
            accumulate(first, last, init, op)

提炼的原则

  • 从具体例子出发,而非凭空设计
  • 只归纳必要的性质,不过度设计
  • 保持性能(不引入不必要的间接调用)

24.3 概念(Concepts)

24.3.1 什么是概念?

概念(Concept):模板对其参数类型的一组要求,是一个谓词(predicate)——给定一个类型,返回 true 或 false。
例子:"容器(Container)"是一个概念:

  • Container<vector<int>>() → true
  • Container<list<string>>() → true
  • Container<int>() → false(int 不是容器)
  • Container<shared_ptr<string>>() → false
    C++11/14 中没有直接的语言支持,概念通过注释和设计规范表达,但可以用代码近似。
24.3.2 发现 String<C> 的概念

以我们的 String<C> 为例,分析字符类型 C 需要满足什么要求:
分析过程(三步)
第一步:看当前实现实际用了 C 的哪些性质:

1. C 可以被拷贝(拷贝赋值和拷贝初始化)
2. String 用 == 和 != 比较 C
3. String 创建 C 的数组(需要默认构造)
4. String 取 C 的地址
5. C 有析构函数
6. String 的 >> 和 << 操作需要 C 也有 >> 和 <<

第二步:哪些是可选的?

  • 要求 6(I/O):只有当用户真正对 String 做 I/O 时才需要,可以只对那些成员函数要求
  • 要求 4、5:几乎所有类型都满足,不用单独列出
    第三步:对应到已知概念:
    "Regular(规则类型)"概念:
  • 可以拷贝(赋值和初始化),且拷贝语义正确(副本独立于原件)
  • 可以默认构造
  • 可以用 ==!= 比较相等
  • 满足各种技术性小要求(可以取地址等)
    更进一步,考虑排序(<):字符串常常需要排序、放进 set、比较大小,所以要求更强的概念:
    "Ordered(有序类型)"概念 = Regular + 全序比较(<, <=, >, >=

需求 对应概念
可拷贝、可默认构造、有==比较 Regular
在 Regular 基础上加全序关系 Ordered
需要 I/O 时 Streamable

什么类型不是 Regular?

  • std::unique_ptr:不可拷贝(只能移动)——Movable 但不 Regular
  • std::type_info:不可拷贝也不可移动——非常特殊
24.3.3 概念 vs 约束(Constraint)

概念(Concept) 约束(Constraint)
稳定性 高,来自领域基础知识 低,只为当前实现
通用性 广,适用于多个算法 窄,只为单一用途
语义 有明确语义(可形式化) 只有语法要求
例子 InputIterator, Regular Balancer(平衡树内部)

24.4 用代码表达概念

C++ 没有直接的概念语言支持(C++20 才加入),但可以用 constexpr 函数 + static_assert 近似:

24.4.1 用 static_assert 进行约束检查
#include <type_traits>
// 辅助函数:检查类型是否有 < 操作(简化实现)
template<typename T>
constexpr bool Has_less()
{
    // 实际实现用 enable_if 和 SFINAE(见第28章)
    return true; // 简化示意
}
// Regular 检查
template<typename T>
constexpr bool Semiregular()
{
    return std::is_destructible<T>::value
        && std::is_default_constructible<T>::value
        && std::is_move_constructible<T>::value
        && std::is_move_assignable<T>::value
        && std::is_copy_constructible<T>::value
        && std::is_copy_assignable<T>::value;
}
template<typename T>
constexpr bool Equality_comparable()
{
    return Semiregular<T>(); // 简化:实际还检查 == 和 != 操作符
}
template<typename T>
constexpr bool Regular()
{
    return Semiregular<T>() && Equality_comparable<T>();
}
template<typename T>
constexpr bool Totally_ordered()
{
    return Equality_comparable<T>() && Has_less<T>(); // 简化
}
template<typename T>
constexpr bool Ordered()
{
    return Regular<T>() && Totally_ordered<T>();
}
// 在类模板中使用约束检查:
template<typename C>
class String {
    // 实例化时检查:如果 C 不满足 Ordered,给出清晰错误信息
    static_assert(Ordered<C>(), "String的字符类型必须是Ordered");
    // ...
};
// 在函数模板中使用:
template<typename C>
std::ostream& operator<<(std::ostream& out, const String<C>& s)
{
    // 只有在使用 << 时才检查 C 是否支持输出
    // static_assert(Streamable<C>(), "String的字符类型不支持流输出");
    out << '"';
    for (int i = 0; i < s.size(); ++i)
        out << s[i];
    return out << '"';
}

全局检查(强制特定类型满足某概念,不管是否使用):

// 这两行可以放在全局作用域,作为"单元测试"
static_assert(Ordered<std::string>(),      "std::string 应当是 Ordered 的"); // 通过
// static_assert(Ordered<String<char>>(),  "String<char> 应当是 Ordered 的"); // 失败(如果忘了定义 <)
24.4.2 公理(Axioms):语义要求的表达

公理是无法用编译器验证的语义要求,但可以用可执行代码表达供测试使用:

// 公理:拷贝产生的副本与原件相等
template<typename T>
bool Copy_equality(T x)
{
    return T{x} == x; // 如果拷贝语义正确,副本应该等于原件
}
// 公理:赋值后,目标与源相等
template<typename T>
bool Copy_assign_equality(T x, T& y)
{
    return (y = x, y == x); // 赋值后 y 应该等于 x
}
// 公理:移动后的值等于原来的值(被移动的对象可以被销毁)
template<typename T>
bool Move_effect(T x, T& y)
{
    return (x == y ? T{std::move(x)} == y : true); // 移动结果等于原值
    // 被移动的 y 之后只需要能被销毁,不需要有具体值
}

这些公理可以用于测试:如果一个类型声称满足 Regular,可以用这些函数验证其实现是否符合语义。

24.4.3 多参数概念

很多概念涉及两种类型之间的关系

// find() 需要迭代器和值类型之间可以比较
template<typename Iter, typename Val>
Iter find(Iter b, Iter e, Val x)
{
    static_assert(/* Input_iterator<Iter>() */ true,
                  "find() 需要输入迭代器");
    static_assert(/* Equality_comparable<value_type_of(Iter), Val>() */ true,
                  "find() 的迭代器元素类型和值类型必须可以用==比较");
    while (b != e) {
        if (*b == x) return b; // 这里需要 *Iter 和 Val 能用 == 比较
        ++b;
    }
    return b;
}
24.4.4 值概念(Value Concepts)

概念也可以用于非类型(整数)参数:

constexpr int stack_limit = 2048; // 栈上允许的最大字节数
// 检查"T 的 N 个元素能放进小栈"
template<typename T, int N>
constexpr bool Stackable()
{
    return /* Regular<T>() && */ sizeof(T) * N <= stack_limit;
}
template<typename T, int N>
struct Buffer {
    // 实例化时检查:缓冲区大小是否合适
    static_assert(Stackable<T, N>(), "Buffer 太大,放不进栈!");
    T data[N];
};
24.4.5 原型(Archetype):测试模板实现

原型(archetype):一个恰好满足概念要求(不多不少)的类型,用于测试模板实现是否偷用了它不应该使用的额外性质。
问题:模板的约束检查只保护用户(用错误类型时报错),但不保护模板作者(实现中可能用了约束之外的操作)。
例如:声称只需要 ForwardIteratorfind,但实现里用了 + 运算:

template<typename Iter, typename Val>
Iter find(Iter b, Iter e, Val x)
{
    while (b != e) {
        if (*b == x) return b;
        b = b + 1; // 错误!ForwardIterator 只保证 ++,不保证 +
    }
    return b;
}

测试方法:定义一个恰好有前向迭代器功能的原型类型,然后用它测试:

// 前向迭代器原型:只提供 Forward_iterator 要求的操作,不多一个
template<typename Val>
struct Forward {
    Forward();
    Forward(const Forward&);
    Forward& operator=(const Forward&);
    bool operator==(const Forward&) const;
    bool operator!=(const Forward&) const;
    void operator++();         // 前缀 ++(前向迭代器的核心)
    Val& operator*();          // 解引用
    // 注意:没有 operator+,没有 operator[],没有 operator--
};
// 用原型类型测试 find() 的实现:
void test()
{
    Forward<int> p = find(Forward<int>{}, Forward<int>{}, 7);
    // 如果 find() 的实现用了 +,这里会编译失败
    // 编译失败 = 发现了实现超出了约束范围的 bug!
}

测试流程:

1. 确定算法声称需要的概念(如 Forward_iterator)
2. 根据概念列出所有操作
3. 定义只有这些操作的原型类型
4. 用原型类型调用算法
5. 编译成功 → 实现没有"偷用"额外操作
   编译失败 → 找到了实现中多余的依赖

24.5 本章建议

  1. 模板传递类型参数不丢失信息
  2. 模板提供通用的编译时编程机制
  3. 模板提供编译时的"鸭子类型"
  4. 从具体例子提炼通用算法(Lifting)
  5. 用概念规范模板参数的要求
  6. 不要给普通符号赋予非常规含义
  7. 把概念作为设计工具
  8. 追求"即插即用"的兼容性:用通用而规则的模板参数要求
  9. 通过最小化算法对参数的要求来发现概念
  10. 概念不只是某个具体实现的需求描述
  11. 尽量从已知概念列表中选取(如 Regular、Ordered 等)
  12. 模板参数的默认概念是 Regular
  13. 不是所有模板参数类型都是 Regular(如 unique_ptr
  14. 概念有语义要求,不只是语法
  15. 用代码(constexpr 函数)表达概念
  16. static_assertenable_if 进行约束检查
  17. 用公理作为设计和测试指导
  18. 某些概念涉及多种类型(多参数概念)
  19. 概念可以涉及数值参数(值概念)
  20. 用概念指导模板实现的测试(原型测试)

关键概念汇总

模板机制全景

template<typename T>
class/function/alias 定义
          ↓ 实例化(使用时)
template<int>       template<double>    template<string>
  实例              实例                实例
每个实例都是完整的普通类/函数,性能与手写代码相同

概念层次(从弱到强)

Semiregular
    可析构、可默认构造、可移动、可拷贝
        |
    + ==, !=
        |
    Regular
        |
    + <, <=, >, >=
        |
    Ordered(又称 Totally_ordered + Regular)

提炼(Lifting)的完整过程

具体函数 A        具体函数 B
(特定类型)        (特定类型)
    \                /
     识别共同结构
          |
     伪代码模型
          |
     参数化类型
          |
     参数化操作
          |
     通用算法(函数模板)
          |
     确定参数的概念要求
          |
     用原型类型测试实现

SFINAE 的作用

调用 f(x)
编译器依次尝试每个重载:
  重载 1:模板替换 → 失败(不是错误!跳过)
  重载 2:模板替换 → 成功 → 加入候选集
  普通函数:匹配 → 加入候选集
从候选集中选最佳匹配
Logo

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

更多推荐