本文对应《A Tour of C++》第 1、2 章,力求用最通俗的语言解释每个概念。

第一章:C++ 基础语法

1.1 C++ 是什么?

C++ 是一门 编译型语言,这意味着你写的源代码不能直接运行,必须先经过一套"翻译流程":

源代码(.cpp) → [编译器] → 目标文件(.obj/.o) → [链接器] → 可执行程序(.exe)

用 ASCII 图表示:

┌─────────────┐     编译     ┌─────────────┐    链接    ┌─────────────┐
│  源文件.cpp  │ ──────────> │  目标文件.o  │ ────────> │  可执行文件  │
└─────────────┘             └─────────────┘           └─────────────┘

两个重要概念:

  • 可移植性:可执行文件是针对特定操作系统/硬件生成的,不能跨平台直接运行;但源代码可以在不同平台上重新编译。
  • 静态类型:每个变量、表达式的类型在编译时就必须确定,不能含糊。

1.2 Hello, World!

// 包含标准输入输出流的声明
// 在较老的编译器上用这行替代 import std;
#include <iostream>
// main 函数:程序的入口,每个 C++ 程序有且仅有一个 main
int main()
{
    // std::cout 是标准输出流(控制台)
    // <<  是"放入"运算符,把右边的内容发送到左边的流
    // "\n" 是换行符
    std::cout << "Hello, World!\n";
    // 返回 0 表示程序正常结束
    // 操作系统(如 Linux)会读取这个值
    return 0;
}

运行结果:

Hello, World!

1.3 函数

函数是"把一件事情打包起来,给它起个名字"的机制。

函数声明 vs 函数定义
// 【声明】:告诉编译器"有这么一个函数存在"
// 格式:返回类型 函数名(参数类型列表);
double sqrt(double);      // 接收一个 double,返回一个 double
void   exit(int);         // 接收一个 int,不返回值(void = 空)
// 【定义】:给出函数的具体实现
double square(double x)   // 参数名 x 可以在定义中使用
{
    return x * x;         // 返回 x 的平方
}
完整示例
#include <iostream>
// 声明命名空间,省去每次写 std::
using namespace std;
// 计算一个数的平方
// 参数 x:要计算的数(double 类型,即双精度浮点数)
// 返回值:x 的平方
double square(double x)
{
    return x * x;
}
// 打印 x 的平方,void 表示不返回任何值
void print_square(double x)
{
    // 多个 << 可以串联,依次输出
    cout << "the square of " << x << " is " << square(x) << "\n";
}
int main()
{
    print_square(1.234);  // 输出:the square of 1.234 is 1.52276
    return 0;
}
函数重载(同名不同参数)
#include <iostream>
#include <string>
using namespace std;
// 三个同名函数,参数类型不同——这叫"函数重载"
void print(int i)    { cout << "int: "    << i << "\n"; }
void print(double d) { cout << "double: " << d << "\n"; }
void print(string s) { cout << "string: " << s << "\n"; }
int main()
{
    print(42);           // 编译器自动选择 print(int)
    print(9.65);         // 编译器自动选择 print(double)
    print("Barcelona");  // 编译器自动选择 print(string)
    return 0;
}

选择过程用 mermaid 表示:

整数 42

浮点 9.65

字符串 Barcelona

调用 print 函数

实参类型是什么?

调用 print int

调用 print double

调用 print string

输出 int: 42

输出 double: 9.65

输出 string: Barcelona

1.4 类型、变量与算术

基本类型一览

类型 含义 示例值
bool 布尔值 true, false
char 单个字符(1字节) 'a', 'z', '9'
int 整数(通常4字节) -273, 42, 1066
double 双精度浮点数 3.14, -273.15, 6.626e-34
unsigned 非负整数 0, 1, 999

每种基本类型的大小(字节数)可用 sizeof 获取,例如:

  • sizeof ( char ) = 1 \text{sizeof}(\text{char}) = 1 sizeof(char)=1
  • sizeof ( int ) \text{sizeof}(\text{int}) sizeof(int) 通常 = 4 = 4 =4
算术运算符
加法:x + y        减法:x - y        乘法:x * y
除法:x / y        取余:x % y(仅整数)

比较运算符结果为 truefalse

相等:x == y       不等:x != y
小于:x < y        大于:x > y

逻辑运算符:

逻辑与:x && y     逻辑或:x || y     逻辑非:!x
位与:  x & y      位或:  x | y      位异或:x ^ y
类型转换注意事项
#include <iostream>
using namespace std;
int main()
{
    double d = 2.2;   // 浮点数
    int    i = 7;     // 整数
    d = d + i;        // i 自动转为 double,d 变为 9.2
    // 注意:double 赋值给 int 会截断小数部分!
    // d * i = 9.2 * 7 = 64.4,截断后 i = 64
    i = d * i;
    cout << "d = " << d << "\n";  // 输出 9.2
    cout << "i = " << i << "\n";  // 输出 64(小数被丢弃)
    return 0;
}
初始化的两种方式
#include <iostream>
#include <vector>
using namespace std;
int main()
{
    // = 号初始化(传统 C 风格)
    double d1 = 2.3;
    // {} 初始化(推荐!可以防止"窄化转换")
    double d2 {2.3};
    // 危险示例:= 允许浮点转整数(丢失小数)
    int i1 = 7.8;    // i1 = 7,小数被悄悄截掉,容易出 bug
    // 安全示例:{} 会报错,保护你不犯错
    // int i2 {7.8};  // 编译错误:不允许 double → int 的窄化转换
    // auto 自动推断类型
    auto b  = true;    // bool
    auto ch = 'x';     // char
    auto n  = 123;     // int
    auto x  = 1.2;     // double
    cout << "d1=" << d1 << " d2=" << d2 << " i1=" << i1 << "\n";
    return 0;
}

**窄化转换(Narrowing Conversion)**的概念:
信息量大的类型 → 强制转换 信息量小的类型 ⇒ 数据丢失 \text{信息量大的类型} \xrightarrow{\text{强制转换}} \text{信息量小的类型} \Rightarrow \text{数据丢失} 信息量大的类型强制转换 信息量小的类型数据丢失
例如: double ( 7.8 ) → int ( 7 ) \text{double}(7.8) \rightarrow \text{int}(7) double(7.8)int(7),小数部分 0.8 0.8 0.8 永远消失。

1.5 作用域与生命周期

变量"活"在哪个范围内?这就是作用域的概念。

名字的作用域类型

局部作用域
函数/lambda 内部声明

类作用域
在 class/struct 内声明的成员

命名空间作用域
在 namespace 内声明

全局作用域
不在任何结构内声明

#include <iostream>
#include <string>
using namespace std;
// 全局变量:程序整个运行期间都存在
int global_count = 0;
void demo_scope()
{
    // 局部变量:函数结束时自动销毁
    string local_msg = "我是局部变量";
    {
        // 内层块:更小的作用域
        int inner = 42;
        cout << inner << "\n";  // OK:inner 在作用域内
    }
    // cout << inner;  // 错误!inner 已经超出作用域,不存在了
    cout << local_msg << "\n";
}  // local_msg 在这里被销毁
int main()
{
    demo_scope();
    cout << "全局计数:" << global_count << "\n";
    return 0;
}

生命周期规律:

局部变量  → 出了 { } 就消亡
全局变量  → 程序结束才消亡
new 创建的对象 → 必须手动 delete,否则内存泄漏

1.6 常量:const 与 constexpr

C++ 有两种"不可变":

关键字 何时求值 用途
const 可以在运行时 声明"我承诺不改它"
constexpr 必须在编译时 真正的编译期常量,可放只读内存

#include <iostream>
#include <cmath>
using namespace std;
// constexpr 函数:编译期就能算出结果
// 条件:函数体必须足够简单,没有副作用
constexpr double square(double x)
{
    return x * x;
}
// consteval 函数:只允许在编译期调用,不能用于运行时
consteval int compile_only(int x)
{
    return x * x;
}
int main()
{
    constexpr int   dmv = 17;             // 编译期常量 17
    const     double sqv = sqrt(2.0);     // 运行时计算,但之后不可修改
    // constexpr 配合常量参数 → 编译期计算
    constexpr double max1 = 1.4 * square(17);  // OK:17 是常量
    int var = 17;
    // constexpr double max2 = 1.4 * square(var); // 错误!var 不是常量
    const     double max3 = 1.4 * square(var);   // OK:运行时计算
    cout << "dmv=" << dmv << " max1=" << max1 << " max3=" << max3 << "\n";
    return 0;
}

constexpr 函数的执行规则总结:
参数是常量 ⇒ 编译期计算(结果是 constexpr) \text{参数是常量} \Rightarrow \text{编译期计算(结果是 constexpr)} 参数是常量编译期计算(结果是 constexpr
参数是变量 ⇒ 运行期计算(结果只是 const 或普通值) \text{参数是变量} \Rightarrow \text{运行期计算(结果只是 const 或普通值)} 参数是变量运行期计算(结果只是 const 或普通值)

1.7 指针、数组与引用

数组

数组是"连续存放的同类型元素序列",从下标 0 开始:

char v[6];   // 声明 6 个 char 的数组:v[0] ~ v[5]

内存布局(ASCII):

下标:  [0]  [1]  [2]  [3]  [4]  [5]
内存:  |'H'|'e'|'l'|'l'|'o'|'\0'|
地址:  100  101  102  103  104  105
指针

指针存储的是某个变量的内存地址

char* p = &v[3];  // p 指向 v[3],即地址 103
char  x = *p;     // *p = 取 p 指向的内容 = 'l'

用 ASCII 图表示指针关系:

p ──────────────────┐
                    ↓
v: [H][e][l][l][o][\0]
   100 101 102 103 104 105
*p = v[3] = 'l'
引用

引用是变量的"别名",一旦绑定就不能改指其他对象:

int  x = 5;
int& r = x;  // r 是 x 的别名
r = 10;      // 等同于 x = 10
// x 现在是 10

特性 指针 * 引用 &
访问值 需要写 *p 直接用 r
可否为空 可以是 nullptr 必须绑定有效对象
可否重新绑定 可以 不可以

遍历数组的三种方式
#include <iostream>
using namespace std;
int main()
{
    int v[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    // 方式一:传统 for 循环(需要手动管理下标)
    for (int i = 0; i != 10; ++i)
        cout << v[i] << " ";
    cout << "\n";
    // 方式二:范围 for 循环(推荐!简洁安全)
    // auto x:x 是元素的副本,修改 x 不影响 v
    for (auto x : v)
        cout << x << " ";
    cout << "\n";
    // 方式三:引用范围 for(可以直接修改原数组)
    // auto& x:x 是元素的引用,修改 x 就是修改 v[i]
    for (auto& x : v)
        ++x;  // 每个元素加 1
    for (auto x : v)
        cout << x << " ";  // 输出 1 2 3 4 5 6 7 8 9 10
    cout << "\n";
    return 0;
}
空指针 nullptr

当指针没有指向任何有效对象时,用 nullptr 表示:

#include <iostream>
using namespace std;
// 统计字符串中某字符出现的次数
// p 指向以 '\0' 结尾的字符数组(C 风格字符串)
int count_x(const char* p, char x)
{
    // 先检查指针是否为空,防止非法访问
    if (p == nullptr)
        return 0;
    int count = 0;
    // *p 非零(不是字符串结尾)就继续循环
    while (*p)
    {
        if (*p == x)
            ++count;
        ++p;       // 指针移动到下一个字符
    }
    return count;
}
int main()
{
    cout << count_x("Hello", 'l') << "\n";  // 输出 2
    cout << count_x(nullptr, 'a') << "\n";  // 输出 0(安全)
    return 0;
}

1.8 控制流:测试与循环

if 语句
#include <iostream>
#include <vector>
using namespace std;
int main()
{
    vector<int> v = {1, 2, 3};
    // 在 if 条件中声明变量:变量 n 只在 if/else 块内有效
    // n = v.size() = 3,非零即为 true
    if (auto n = v.size(); n != 0)
    {
        cout << "向量有 " << n << " 个元素\n";
    }
    // 更简洁的写法:省略 != 0,直接用值本身做条件
    if (auto n = v.size())
    {
        cout << "非空向量,大小为 " << n << "\n";
    }
    return 0;
}
switch 语句
#include <iostream>
using namespace std;
bool ask_user()
{
    cout << "继续吗?(y/n)\n";
    char answer = 0;
    cin >> answer;
    // switch 比多个 if-else 更清晰
    switch (answer)
    {
    case 'y':
        return true;
    case 'n':
        return false;
    default:                              // 不匹配任何 case
        cout << "我当作 no 处理。\n";
        return false;
    }
}
int main()
{
    bool result = ask_user();
    cout << (result ? "继续!" : "停止。") << "\n";
    return 0;
}

控制流图:

y

n

其他

读取用户输入 answer

switch answer

return true

return false

输出提示

return false

1.9 映射到硬件

C++ 的设计哲学:语言原语直接对应硬件操作

赋值
#include <iostream>
using namespace std;
int main()
{
    int x = 2;
    int y = 3;
    // 赋值:把 y 的值复制给 x
    // 之后 x 和 y 完全独立,修改一个不影响另一个
    x = y;   // x 变为 3,y 仍是 3
    cout << "x=" << x << " y=" << y << "\n";  // x=3 y=3
    // 指针赋值
    int* p = &x;   // p 指向 x
    int* q = &y;   // q 指向 y
    p = q;         // 让 p 也指向 y(注意:x 本身没有变化!)
    cout << "*p=" << *p << "\n";  // 输出 3(即 y 的值)
    return 0;
}

用 ASCII 演示指针赋值前后:

【赋值前】
p ──> x(地址88,值3)
q ──> y(地址92,值3)
【p = q 之后】
p ──────────────┐
q ──> y(地址92,值3)
引用赋值 vs 指针赋值的区别
#include <iostream>
using namespace std;
int main()
{
    int x = 2;
    int y = 3;
    int& r  = x;   // r 绑定到 x(r 是 x 的别名)
    int& r2 = y;   // r2 绑定到 y
    // 给引用赋值 = 修改引用所指向的那个变量
    // 不是让 r 改指 y!而是把 y 的值写入 x
    r = r2;        // 等同于 x = y,x 变为 3
    cout << "x=" << x << " y=" << y << "\n";  // x=3 y=3
    return 0;
}

操作 指针 p = q 引用 r = r2
含义 p 改为指向 q 所指的对象 把 r2 指向的值写入 r 所指的对象
改变了什么 指针本身 被引用的变量的值

第二章:用户自定义类型

2.1 为什么需要自定义类型?

C++ 的内置类型(int、double 等)太底层,不够表达现实世界的概念。自定义类型让我们可以创建更有意义的抽象。

用户自定义类型

struct 结构体
数据打包

class 类
数据 + 操作 + 封装

enum class 枚举
有限集合的命名常量

union 联合体
多类型共享内存

2.2 结构体 struct

结构体是把相关数据组合在一起的最简单方式:

#include <iostream>
using namespace std;
// 定义一个"向量"结构体(不是 std::vector,是我们自己的)
struct Vector
{
    double* elem;  // 指向元素数组的指针
    int     sz;    // 元素个数
};
// 初始化 Vector(通过引用传入,可以修改它)
void vector_init(Vector& v, int s)
{
    v.elem = new double[s];  // 从堆上分配 s 个 double 的空间
    v.sz   = s;
}
int main()
{
    Vector v;
    vector_init(v, 3);  // 分配 3 个元素
    v.elem[0] = 1.1;
    v.elem[1] = 2.2;
    v.elem[2] = 3.3;
    // 用 . 访问结构体成员(通过名字或引用)
    cout << "大小:" << v.sz << "\n";
    for (int i = 0; i < v.sz; ++i)
        cout << v.elem[i] << " ";
    cout << "\n";
    delete[] v.elem;  // 释放堆内存,防止内存泄漏
    return 0;
}

结构体内存示意:

Vector v:
┌──────────┬────┐
│  elem    │ sz │
│  (指针)  │ 3  │
└────┬─────┴────┘
     │
     ↓ 堆内存
  ┌──────┬──────┬──────┐
  │ 1.1  │ 2.2  │ 3.3  │
  └──────┴──────┴──────┘

访问方式:

Vector  v;    int i1 = v.sz;    // 通过名字,用 .
Vector& rv;   int i2 = rv.sz;   // 通过引用,用 .
Vector* pv;   int i3 = pv->sz;  // 通过指针,用 ->

2.3 类 class

类在结构体基础上增加了访问控制(公有/私有)和成员函数

#include <iostream>
using namespace std;
// 改进版 Vector,封装了数据和操作
class Vector
{
public:
    // 构造函数:与类同名,创建对象时自动调用
    // :elem{new double[s]}, sz{s} 是成员初始化列表
    Vector(int s) : elem{new double[s]}, sz{s}
    {
        // 把所有元素初始化为 0
        for (int i = 0; i < s; ++i)
            elem[i] = 0;
    }
    // 析构函数:对象销毁时自动调用,释放资源
    ~Vector()
    {
        delete[] elem;
    }
    // 下标运算符重载:让 v[i] 语法可用
    // 返回引用,既可读也可写
    double& operator[](int i)
    {
        return elem[i];
    }
    // 返回元素个数
    int size() const
    {
        return sz;
    }
private:
    // 私有成员:外部代码无法直接访问
    double* elem;  // 指向元素数组
    int     sz;    // 元素个数
};
int main()
{
    Vector v(5);   // 创建含 5 个元素的向量
    // 通过 operator[] 写入
    for (int i = 0; i < v.size(); ++i)
        v[i] = i * 1.5;
    // 通过 operator[] 读取
    for (int i = 0; i < v.size(); ++i)
        cout << v[i] << " ";
    cout << "\n";
    // 离开作用域时,析构函数自动调用,释放 elem 内存
    return 0;
}

struct 和 class 的唯一区别:

struct:成员默认是 public(外部可以直接访问)
class :成员默认是 private(外部不能直接访问)

访问控制示意:

Vector类

只能访问

不能访问

内部可以

public 区域
Vector 构造函数
operator[]
size

private 区域
elem 指针
sz 大小

外部代码

2.4 枚举 enum class

枚举用来表示"一组有限的命名常量",让代码更易读:

#include <iostream>
using namespace std;
// 强类型枚举:枚举值在自己的作用域内,不会污染全局
enum class Color        { red, blue, green };
enum class Traffic_light { green, yellow, red };
// 为 Traffic_light 定义前缀 ++ 运算符
Traffic_light& operator++(Traffic_light& t)
{
    using enum Traffic_light;  // 在此作用域内直接用 green/yellow/red
    switch (t)
    {
    case green:  return t = yellow;
    case yellow: return t = red;
    case red:    return t = green;
    }
    return t;  // 不会到达这里,仅为消除编译警告
}
int main()
{
    Color        col  = Color::red;           // 必须加作用域前缀
    Traffic_light sig = Traffic_light::green;
    // 两个枚举类的 red 不会混淆
    // Color::red 和 Traffic_light::red 是完全不同的值
    // 模拟交通灯循环三次
    for (int i = 0; i < 3; ++i)
    {
        ++sig;
        if (sig == Traffic_light::red)
            cout << "红灯!停车\n";
        else if (sig == Traffic_light::yellow)
            cout << "黄灯!准备\n";
        else
            cout << "绿灯!通行\n";
    }
    return 0;
}

交通灯状态转换:

++

++

++

green

yellow

red

枚举的类型安全:

Color x = red;              // 错误:哪个 red?作用域不明
Color x = Traffic_light::red; // 错误:类型不匹配
Color x = Color::red;       // 正确
int   i = Color::red;       // 错误:不能隐式转为 int
int   i = int(Color::red);  // 正确:显式转换

2.5 联合体 union

联合体让多个成员共用同一块内存,同一时刻只有一个成员有效:

#include <iostream>
#include <string>
#include <variant>
using namespace std;
// 方式一:传统 union(不推荐,需要手动追踪当前存的是什么类型)
struct Node { int val; };  // 简化示例
enum class Type { ptr, num };
union Value
{
    Node* p;  // 指针:占 8 字节(64 位系统)
    int   i;  // 整数:占 4 字节
    // 联合体大小 = max(8, 4) = 8 字节(两者共用)
};
struct Entry_old
{
    string name;
    Type   t;     // 标记当前存的是哪种类型
    Value  v;     // 实际值
};
// 方式二:std::variant(推荐!类型安全,不会出错)
struct Entry
{
    string             name;
    variant<Node*, int> v;  // 只能存 Node* 或 int,自动追踪类型
};
int main()
{
    // 使用 variant 版本
    Entry e;
    e.name = "example";
    e.v    = 42;  // 存入 int
    if (holds_alternative<int>(e.v))
        cout << "存的是整数:" << get<int>(e.v) << "\n";
    e.v = new Node{100};  // 换成指针
    if (holds_alternative<Node*>(e.v))
        cout << "存的是指针,值:" << get<Node*>(e.v)->val << "\n";
    return 0;
}

内存布局对比(ASCII):

【struct 普通结构体】           【union 联合体】
┌──────┬──────┐               ┌──────────────┐
│  p   │  i   │               │  p  /  i     │  ← 共用同一地址
└──────┴──────┘               └──────────────┘
总大小 = 8+4 = 12 字节         总大小 = max(8,4) = 8 字节

方式 优点 缺点
union 节省内存 需手动追踪类型,容易出错
std::variant 类型安全 轻微额外开销

总结:第 1-2 章知识图谱

C++ 基础

程序结构

类型系统

控制流

用户自定义类型

编译 → 链接 → 可执行

main 函数入口

函数声明与定义

函数重载

基本类型
int double char bool

const / constexpr

指针 * 与引用 &

auto 类型推断

if / switch

for / while

范围 for 循环

struct 结构体

class 类

enum class 枚举

union / variant

核心建议速查


建议 原因
{} 初始化 防止窄化转换,更安全
auto 省略冗余类型 减少重复,提升可读性
nullptr 而非 0/NULL 类型明确,不与整数混淆
constexpr 表示编译期常量 性能更好,可放只读内存
用范围 for 代替下标 for 更简洁,不容易越界
enum class 而非裸 enum 防止枚举值污染外部作用域
variant 而非裸 union 类型安全,自动追踪当前存储的类型
变量声明尽量推迟 到有初始值时再声明,避免未初始化变量

C++ 模块化与错误处理:从零开始理解

本文对应《A Tour of C++》第 3、4 章,用最通俗的语言解释每个概念。

第三章:模块化

3.1 什么是模块化?为什么需要它?

一个真实的 C++ 程序由很多"零件"组成——函数、类、模板等。
模块化的核心思想就是:把"对外公开的接口"和"内部实现细节"分开
就像一台洗衣机:你只需要知道怎么按按钮(接口),不需要知道内部电路怎么工作(实现)。
C++ 用**声明(declaration)**来表示接口:

// 【声明】:告诉使用者"有这个东西,可以这样用"
// 只有签名,没有函数体
double sqrt(double);   // sqrt 接收一个 double,返回一个 double
class Vector {         // Vector 类的声明
public:
    Vector(int s);           // 构造函数
    double& operator[](int i); // 下标访问
    int size();              // 获取大小
private:
    double* elem;   // 私有:使用者看不到实现细节
    int sz;
};
// 【定义】:实际的实现代码,可以放在另一个文件里
double& Vector::operator[](int i)
{
    return elem[i];
}

核心规则:

  • 声明可以有多个(在多个地方告诉编译器"有这东西")
  • 定义只能有一个(实际实现只有一份)

3.2 分离编译

把代码拆分成多个文件分别编译,好处是:

- 修改一个文件,只需重新编译那一个文件
- 不同开发者可以同时开发不同模块
- 隐藏实现细节,保护知识产权

C++ 提供两种方式实现分离编译:

分离编译的两种方式

头文件方式
传统 C 风格
用 include 引入

模块方式
C++20 新特性
用 import 引入

历史悠久
但有缺点

更现代
速度更快
更安全

3.2.1 头文件方式(传统)

三个文件协作的完整示例:

项目结构:
Vector.h    ← 接口声明(头文件)
Vector.cpp  ← 实现定义
user.cpp    ← 使用者代码

Vector.h(接口文件):

// Vector.h:只放声明,不放实现
// #pragma once 防止被同一文件重复包含
#pragma once
class Vector {
public:
    Vector(int s);             // 声明:构造函数
    double& operator[](int i); // 声明:下标运算符
    int size();                // 声明:获取大小
private:
    double* elem;  // 指向元素数组的指针
    int sz;        // 元素个数
};

Vector.cpp(实现文件):

// Vector.cpp:放实现代码
// 自己也要 include 自己的头文件,让编译器检查一致性
#include "Vector.h"
// 构造函数的实现
// Vector:: 表示这是 Vector 类的成员函数
Vector::Vector(int s)
    : elem{new double[s]},  // 成员初始化列表:分配 s 个 double 的内存
      sz{s}                 // 记录大小
{
    // 函数体可以为空,初始化已在上面完成
}
// 下标运算符的实现
double& Vector::operator[](int i)
{
    return elem[i];  // 返回第 i 个元素的引用
}
// 获取大小的实现
int Vector::size()
{
    return sz;
}

user.cpp(使用者代码):

// user.cpp:使用 Vector 的代码
#include "Vector.h"   // 引入 Vector 的接口声明
#include <cmath>      // 引入标准库数学函数(含 sqrt)
#include <iostream>
using namespace std;
// 计算向量中所有元素的平方根之和
double sqrt_sum(const Vector& v)
{
    double sum = 0;
    for (int i = 0; i != v.size(); ++i)
        sum += std::sqrt(v[i]);   // sqrt 来自 <cmath>
    return sum;
}
int main()
{
    // 注意:这个完整示例需要 Vector.cpp 一起编译
    // 编译命令:g++ Vector.cpp user.cpp -o program
    cout << "程序需要 Vector.cpp 一起编译\n";
    return 0;
}

文件关系用 ASCII 表示:

Vector.h (接口声明)
    |
    |── #include ──> Vector.cpp (实现)
    |                   编译生成 Vector.o
    |
    └── #include ──> user.cpp (使用者)
                        编译生成 user.o
最终:Vector.o + user.o ──[链接器]──> 可执行程序

头文件方式的四大缺点:

缺点 说明
编译慢 100 个文件都 include 同一个头,就编译 100 次
顺序依赖 include 的顺序不同可能导致不同结果,容易出 bug
不一致 同一个东西在两个文件里定义略有不同,导致崩溃
传递性污染 头文件 A include 了头文件 B,用 A 的人意外获得 B

3.2.2 模块方式(C++20 新特性)

模块是对头文件缺点的彻底解决方案:

// Vector_module.cppm(模块文件,扩展名因编译器而异)
// export module 声明这是一个叫 "Vector" 的模块
export module Vector;
// export class:对外导出这个类(用户 import 后能看到)
export class Vector {
public:
    Vector(int s);
    double& operator[](int i);
    int size();
private:
    double* elem;
    int sz;
};
// 实现部分——不加 export,用户看不到这些细节
Vector::Vector(int s)
    : elem{new double[s]}, sz{s}
{
}
double& Vector::operator[](int i)
{
    return elem[i];
}
int Vector::size()
{
    return sz;
}
// export 一个非成员函数:比较两个 Vector 是否相等
export bool operator==(const Vector& v1, const Vector& v2)
{
    if (v1.size() != v2.size())
        return false;
    for (int i = 0; i < v1.size(); ++i)
        if (v1[i] != v2[i])
            return false;
    return true;
}

使用模块的代码:

// user_module.cpp
#include <cmath>
#include <iostream>
import Vector;   // 导入模块,只获得 export 的部分
using namespace std;
double sqrt_sum(Vector& v)
{
    double sum = 0;
    for (int i = 0; i != v.size(); ++i)
        sum += std::sqrt(v[i]);
    return sum;
}

模块与头文件的对比:

特性 头文件 #include 模块 import
编译次数 每次 include 都重新编译 只编译一次
顺序影响 有(顺序不同结果不同) 无(顺序无关)
传递性 有(会传递依赖) 无(不传递)
编译速度 快(快约 10 倍)
支持版本 所有 C++ 版本 C++20 及以上

3.3 命名空间

命名空间解决"名字冲突"问题。
想象两家公司都有叫"张伟"的员工,说"张伟来了"就不知道是哪个。
加上公司名就清楚了:“腾讯.张伟"和"阿里.张伟”。

#include <iostream>
#include <complex>  // 标准库的复数
// 定义自己的命名空间,避免与标准库名字冲突
namespace My_code {
    // 这个 complex 是 My_code::complex,不是 std::complex
    class complex {
    public:
        complex(double r, double i) : re{r}, im{i} {}
        double real() const { return re; }
        double imag() const { return im; }
    private:
        double re, im;
    };
    // 这个 sqrt 是 My_code::sqrt,不是 std::sqrt
    complex sqrt(complex c)
    {
        // 简化实现
        return {c.real(), c.imag()};
    }
} // namespace My_code
int main()
{
    // 用 :: 指定来自哪个命名空间
    My_code::complex z{1, 2};          // My_code 里的 complex
    std::complex<double> sz{1.0, 2.0}; // std 里的 complex
    // 两者完全不冲突
    auto z2  = My_code::sqrt(z);
    std::cout << z2.real() << "\n";
    return 0;
}
三种使用命名空间的方式
#include <iostream>
#include <vector>
#include <algorithm>
int main()
{
    std::vector<int> x = {3, 1, 2};
    std::vector<int> y = {5, 4};
    // 方式一:每次都写全名(最安全,但啰嗦)
    std::sort(x.begin(), x.end());
    // 方式二:using 声明——只把特定名字引入作用域
    {
        using std::swap;   // 只把 swap 引进来
        swap(x, y);        // 现在可以直接写 swap,不用 std::swap
        // 其他 std 里的名字仍然需要 std:: 前缀
    }
    // 方式三:using 指令——把整个命名空间引入(慎用!)
    using namespace std;   // std 里所有名字都可以直接用
    cout << "x 大小: " << x.size() << "\n";  // 不需要 std::cout
    return 0;
}

方式 语法 风险 适用场景
全限定名 std::cout 任何时候都可用
using 声明 using std::swap 极低 局部使用某个名字
using 指令 using namespace std 中等 简单程序或局部作用域

注意:不要在头文件中写 using namespace std;
因为所有 include 这个头文件的代码都会被污染。

3.4 函数参数与返回值

函数是程序各部分传递信息的主要方式。

3.4.1 参数传递

三种传递方式:

#include <iostream>
#include <vector>
using namespace std;
// 方式一:传值(pass-by-value)
// 函数得到的是副本,修改副本不影响原变量
// 适合:小型数据(int、double、char 等)
void by_value(int x)
{
    x = 999;  // 只修改了副本,调用者的 x 不变
}
// 方式二:传引用(pass-by-reference)
// 函数直接操作原变量,修改会影响调用者
// 适合:需要修改原变量,或避免拷贝大型数据
void by_ref(vector<int>& v)
{
    v[0] = 999;  // 直接修改原向量!
}
// 方式三:传 const 引用(pass-by-const-reference)
// 可以避免拷贝大数据,同时保证不会修改原变量
// 这是最常用的传参方式!
int sum_const_ref(const vector<int>& v)
{
    int s = 0;
    for (int x : v)
        s += x;
    return s;  // v 没有被修改
}
// 带默认参数的函数
// 不传第二个参数时,base 默认为 10
void print_value(int value, int base = 10)
{
    // 简化演示:只打印值和进制
    cout << "值=" << value << " 进制=" << base << "\n";
}
int main()
{
    int n = 42;
    by_value(n);
    cout << "传值后 n=" << n << "\n";   // 仍然是 42
    vector<int> v = {1, 2, 3};
    by_ref(v);
    cout << "传引用后 v[0]=" << v[0] << "\n";  // 变成 999
    vector<int> data = {1, 2, 3, 5, 8};
    cout << "求和=" << sum_const_ref(data) << "\n";  // 19
    print_value(255, 16);  // 传十六进制标记
    print_value(42);       // 使用默认十进制
    return 0;
}

传参方式选择规则:

小 int double 等

大 vector string 等

选择传参方式

需要修改原变量?

传引用 int& x

数据大吗?

传值 int x

传 const 引用
const vector& v

3.4.2 返回值
#include <iostream>
#include <vector>
using namespace std;
// 返回小对象:直接返回值(编译器会优化掉多余的拷贝)
int add(int a, int b)
{
    return a + b;
}
// 返回引用:只在返回的对象生命周期比函数长时才合法
class Vector {
public:
    Vector(int s) : elem{new double[s]}, sz{s} {}
    ~Vector() { delete[] elem; }
    // 返回引用:elem[i] 是成员,在对象销毁前一直存在
    double& operator[](int i) { return elem[i]; }
    int size() const { return sz; }
private:
    double* elem;
    int sz;
};
// 错误示范(不要这样写!)
// int& bad_return()
// {
//     int x = 42;
//     return x;  // 危险!x 在函数返回后就消亡了,返回悬空引用
// }
// 返回大对象:现代 C++ 会自动用"移动语义",不会真正拷贝
// 不需要像旧代码那样返回指针
vector<int> make_vector(int n)
{
    vector<int> result(n);
    for (int i = 0; i < n; ++i)
        result[i] = i * i;
    return result;  // 编译器优化:直接在调用处构造,不拷贝
}
int main()
{
    cout << add(3, 4) << "\n";   // 7
    Vector v(5);
    v[2] = 3.14;                  // 通过引用写入
    cout << v[2] << "\n";         // 通过引用读取:3.14
    auto squares = make_vector(5); // {0, 1, 4, 9, 16}
    for (int x : squares)
        cout << x << " ";
    cout << "\n";
    return 0;
}

旧风格 vs 新风格对比(返回大对象):

【旧风格:返回指针,容易出错】        【新风格:直接返回,编译器优化】
Matrix* add(Matrix& a, Matrix& b)      Matrix operator+(Matrix& a, Matrix& b)
{                                       {
    Matrix* p = new Matrix;                 Matrix res;
    // ...填充 p...                          // ...填充 res...
    return p;  // 别人要记得 delete!        return res;  // 编译器自动优化
}                                       }
Matrix* m = add(a, b);
// ... 用完之后
delete m;  // 很容易忘记!
3.4.3 返回类型推断
#include <iostream>
using namespace std;
// auto 作为返回类型:让编译器自动推断
// i*d 的结果是 double(int * double = double)
// 所以函数的返回类型自动推断为 double
auto mul(int i, double d)
{
    return i * d;
}
// 后置返回类型语法(Suffix Return Type)
// 在参数列表后面用 -> 指定返回类型
// 好处:可以看完参数再写返回类型,有时更清晰
auto mul2(int i, double d) -> double
{
    return i * d;
}
int main()
{
    auto result = mul(3, 2.5);    // result 是 double,值为 7.5
    cout << result << "\n";
    auto result2 = mul2(4, 1.5);  // result2 也是 double,值为 6.0
    cout << result2 << "\n";
    return 0;
}
3.4.5 结构化绑定(Structured Binding)

函数只能返回一个值,但这个值可以是包含多个成员的结构体:

#include <iostream>
#include <map>
#include <string>
using namespace std;
// 定义一个包含多个字段的结构体
struct Entry {
    string name;
    int    value;
};
// 函数"返回多个值"的技巧:返回结构体
// {s, i} 是聚合初始化,相当于 Entry{s, i}
Entry make_entry(string s, int i)
{
    return {s, i};
}
int main()
{
    // 方式一:传统方式,通过成员名访问
    Entry e = make_entry("answer", 42);
    cout << e.name << " = " << e.value << "\n";
    // 方式二:结构化绑定(C++17)
    // auto [n, v] 自动拆解 Entry 的两个成员
    // n 绑定到 name,v 绑定到 value
    auto [n, v] = make_entry("pi", 314);
    cout << n << " = " << v << "\n";
    // 在范围 for 循环中使用结构化绑定
    // map 的每个元素是 pair<key, value>
    map<string, int> scores = {
        {"Alice", 95},
        {"Bob",   87},
        {"Carol", 92}
    };
    cout << "\n成绩单:\n";
    for (const auto& [name, score] : scores)
        cout << name << ": " << score << "\n";
    // 修改 map 的值(用引用绑定)
    for (auto& [name, score] : scores)
        score += 5;  // 每人加 5 分
    cout << "\n加分后:\n";
    for (const auto& [name, score] : scores)
        cout << name << ": " << score << "\n";
    // 复数的结构化绑定
    // complex 有 real() 和 imag() 两个访问函数
    // auto [re, im] = some_complex;  // 也可以这样用
    return 0;
}

结构化绑定的本质:

Entry e = {name="pi", value=314};
auto [n, v] = e;
等价于:
auto n = e.name;   // n 是 string,值 "pi"
auto v = e.value;  // v 是 int,值 314
注意:不会拷贝 Entry 本身,只是给成员起了别名

第四章:错误处理

4.1 为什么错误处理很重要?

程序在运行时会遇到各种意外情况:

  • 用户输入了非法值
  • 文件不存在
  • 内存不够用
  • 数组访问越界
    C++ 提供了一套机制来优雅地处理这些情况,而不是让程序直接崩溃。

错误处理方式

异常 exception
throw / catch

返回错误码
return -1 等

终止程序
terminate abort exit

断言 assert
调试期检查

适合:错误罕见
错误需要跨层传递

适合:错误常见
调用者立刻处理

适合:无法恢复
内存耗尽等严重情况

适合:开发期
检查不变量

4.2 异常(Exceptions)

异常的流程: 发现问题的地方 throw,处理问题的地方 catch

#include <iostream>
#include <stdexcept>   // 包含标准异常类:out_of_range, length_error 等
using namespace std;
// 一个带边界检查的 Vector 类
class Vector {
public:
    Vector(int s) : elem{new double[s]}, sz{s} {}
    ~Vector() { delete[] elem; }
    // 带边界检查的下标访问
    double& operator[](int i)
    {
        // 检查下标是否合法:必须在 [0, size()) 范围内
        // 0<=i && i<sz 等价于数学中的 $0 \leq i < sz$
        if (!(0 <= i && i < sz))
            // throw:抛出异常,把控制权交给 catch
            // out_of_range 是标准库定义的异常类型,继承自 exception
            throw out_of_range{"Vector::operator[]: 下标越界"};
        return elem[i];
    }
    int size() const { return sz; }
private:
    double* elem;
    int sz;
};
// 模拟一些计算函数(可能触发越界)
void compute(Vector& v, int idx)
{
    // 如果 idx 越界,这里会 throw,compute 函数直接退出
    // 不需要写任何错误处理代码,异常会自动向上传播
    double val = v[idx];
    cout << "v[" << idx << "] = " << val << "\n";
}
int main()
{
    Vector v(5);
    v[0] = 1.1;
    v[1] = 2.2;
    // try 块:在这里面发生的异常,会被下面的 catch 捕获
    try {
        compute(v, 1);   // OK:下标 1 合法
        compute(v, 10);  // 触发越界!throw out_of_range
        // 上面 throw 之后,下面这行不会执行
        compute(v, 2);
    }
    // catch:捕获 out_of_range 类型的异常
    // 用引用捕获,避免拷贝
    catch (const out_of_range& err) {
        // err.what() 返回创建异常时传入的错误消息字符串
        cerr << "范围错误:" << err.what() << "\n";
    }
    cout << "程序继续运行(异常已被处理)\n";
    return 0;
}

异常传播过程(调用栈展开):

main()
  └── try { compute(v, 10) }
          └── compute(v, 10)
                  └── v[10]  ← 这里 throw out_of_range
                      |
                      | 异常向上冒泡,逐层退出函数
                      |(每退出一层,该层的局部变量被销毁)
                      ↓
              compute 退出(没有 catch,继续冒泡)
                      ↓
          main 的 catch 捕获到异常
          执行 catch 块内的处理代码
          程序继续运行

4.3 不变量(Invariants)

不变量是类必须始终满足的条件。构造函数负责建立不变量,成员函数负责维持它。
以 Vector 为例,不变量是:elem 指向一个包含 sz 个 double 的数组
如果构造函数收到非法参数(如负数大小),就应该 throw 阻止对象被创建:

#include <iostream>
#include <stdexcept>
using namespace std;
class Vector {
public:
    // 构造函数:建立不变量
    // 不变量:elem 指向 sz 个 double 的数组,且 sz >= 0
    Vector(int s)
    {
        // 检查前置条件(precondition):s 必须非负
        if (s < 0)
            // length_error 是标准库异常,表示"长度不合法"
            throw length_error{"Vector构造函数:大小不能为负"};
        // new 失败时会抛出 std::bad_alloc
        // 如果内存不够,这里会自动 throw,我们不用手动检查
        elem = new double[s];
        sz   = s;
    }
    ~Vector() { delete[] elem; }
    double& operator[](int i)
    {
        if (!(0 <= i && i < sz))
            throw out_of_range{"Vector::operator[]: 越界"};
        return elem[i];
    }
    int size() const { return sz; }
private:
    double* elem;
    int sz;
};
// 测试不同场景
void test(int n)
{
    try {
        Vector v(n);
        v[0] = 1.0;
        cout << "Vector(" << n << ") 创建成功\n";
    }
    catch (std::length_error& err) {
        cerr << "长度错误:" << err.what() << "\n";
    }
    catch (std::bad_alloc& err) {
        // 内存耗尽是严重错误,通常直接终止
        cerr << "内存不足:" << err.what() << "\n";
        std::terminate();  // 终止程序
    }
}
int main()
{
    test(10);    // 正常:创建 10 个元素的 Vector
    test(-27);   // 异常:长度为负,抛出 length_error
    return 0;
}

不变量的概念图:

建立不变量
检查参数合法性

成员函数调用

构造函数 Constructor

对象处于有效状态

不变量仍然成立?

函数正常执行
返回时维持不变量

抛出异常
拒绝执行

调用者处理异常

4.4 错误处理策略选择

什么时候用异常,什么时候用错误码,什么时候直接终止?

无法恢复
如内存耗尽

可以恢复

常见
如文件不存在

罕见
如越界访问


需跨层处理

发现了错误

错误有多严重?

terminate
终止程序

错误常见吗?

调用者能立即处理?

throw 异常
让异常向上传播

返回错误码
如 return -1

完整的对比示例:

#include <iostream>
#include <fstream>
#include <stdexcept>
#include <cassert>
using namespace std;
// ===== 方式一:返回错误码 =====
// 适合:打开文件这种"失败很正常"的操作
// 调用者可以立刻判断并处理
bool open_file(const string& filename, ifstream& file)
{
    file.open(filename);
    return file.is_open();  // 返回 true 表示成功
}
// ===== 方式二:抛出异常 =====
// 适合:越界访问——调用者不应该访问越界,这是 bug
double get_element(const double* arr, int size, int i)
{
    if (i < 0 || i >= size)
        throw out_of_range{"get_element: 下标越界"};
    return arr[i];
}
// ===== 方式三:noexcept + terminate =====
// 适合:这个函数保证不抛异常(如析构函数、移动操作)
void safe_cleanup(double* ptr) noexcept
{
    delete[] ptr;
    // 如果这里意外抛了异常,noexcept 会让程序调用 terminate()
    // 保证析构函数不会因为异常半途而废
}
int main()
{
    // 使用错误码方式
    ifstream f;
    if (!open_file("不存在的文件.txt", f)) {
        cout << "文件打开失败(这很正常)\n";
    }
    // 使用异常方式
    double arr[] = {1.0, 2.0, 3.0};
    try {
        cout << get_element(arr, 3, 1) << "\n";  // OK: 2.0
        cout << get_element(arr, 3, 5) << "\n";  // 抛出异常
    }
    catch (const out_of_range& e) {
        cerr << "捕获异常:" << e.what() << "\n";
    }
    // noexcept 函数
    double* p = new double[10];
    safe_cleanup(p);
    return 0;
}

情形 推荐方式
错误很罕见,调用者不会主动检查 throw 异常
错误跨越多层函数传播 throw 异常
构造函数中遇到非法参数 throw 异常
错误很常见(如文件不存在) 返回错误码
调用者立刻处理错误 返回错误码
程序根本无法继续运行 terminate

4.5 断言(Assertions)

断言是"我相信这个条件一定成立,如果不成立说明有 bug"的表达方式。

4.5.1 运行时断言 assert()
#include <iostream>
#include <cassert>   // 包含 assert 宏
using namespace std;
// 计算字符串长度(假设输入不为 nullptr)
int string_length(const char* p)
{
    // assert:如果括号内为 false,程序立刻终止并报告位置
    // 在 Release 模式下(定义了 NDEBUG),assert 被编译器忽略,无性能开销
    assert(p != nullptr);  // "我断言 p 不为空指针"
    int len = 0;
    while (*p++)
        ++len;
    return len;
}
int main()
{
    cout << string_length("Hello") << "\n";  // 5(正常)
    // assert(p != nullptr) 会在调试模式下触发,终止程序并打印位置
    // 在发布模式(Release)下 assert 不起作用,什么都不做
    // cout << string_length(nullptr) << "\n";  // 调试时触发 assert
    return 0;
}
4.5.2 编译期断言 static_assert
#include <iostream>
using namespace std;
// static_assert:在编译期就检查条件,不需要运行程序
// 如果条件为 false,编译器报错并显示消息
static_assert(4 <= sizeof(int), "整数太小了,至少需要 4 字节");
// 光速常量(km/s)
constexpr double C = 299792.458;
void check_speed()
{
    // 160 km/h 转换为 km/s:$\frac{160}{3600}$
    constexpr double local_max = 160.0 / (60 * 60);
    // 编译期检查:local_max 一定小于光速(这是物理定律)
    static_assert(local_max < C, "比光速还快?不可能!");
    // 注意:static_assert 的条件必须是编译期常量
    // double speed = 100.0;
    // static_assert(speed < C, "...");  // 错误!speed 不是常量
    cout << "速度检查通过:" << local_max << " km/s < " << C << " km/s\n";
}
// 用于泛型编程时检查类型
template<typename T>
void require_at_least_4_bytes()
{
    static_assert(sizeof(T) >= 4, "类型 T 必须至少 4 字节");
}
int main()
{
    check_speed();
    require_at_least_4_bytes<int>();    // OK
    require_at_least_4_bytes<double>(); // OK
    // require_at_least_4_bytes<char>();// 编译错误!char 只有 1 字节
    return 0;
}
4.5.3 noexcept 关键字
#include <iostream>
#include <stdexcept>
#include <vector>
using namespace std;
class Resource {
public:
    Resource() : data{new int[100]} {}
    // 析构函数标记 noexcept:保证析构不会抛出异常
    // 析构函数中如果抛异常,程序行为未定义(非常危险)
    ~Resource() noexcept
    {
        delete[] data;
        // 如果这里意外发生异常,noexcept 确保程序调用 terminate()
        // 而不是让异常从析构函数中泄漏出去
    }
    // 移动操作也应该标记 noexcept,让标准库容器能高效地使用它
    Resource(Resource&& other) noexcept : data{other.data}
    {
        other.data = nullptr;
    }
    // 普通函数:标记 noexcept 表示"保证不抛异常"
    // 如果内部真的抛了,std::terminate() 被调用
    int get_first() const noexcept
    {
        return data[0];
    }
private:
    int* data;
};
// 演示 noexcept 的传播
void guaranteed_safe() noexcept
{
    // 如果这里调用了可能抛异常的函数,而那个异常真的发生了
    // noexcept 会让程序调用 terminate(),而不是让异常传播
    Resource r;
    r.get_first();
    cout << "安全完成\n";
}
int main()
{
    guaranteed_safe();
    return 0;
}

assert、static_assert、noexcept 对比:

工具 何时检查 失败时 适用场景
assert() 运行时 调试模式终止 开发调试期检查 bug
static_assert 编译时 编译错误 检查类型大小、常量条件
noexcept 运行时 调用terminate 保证函数不抛异常

总结:第 3-4 章知识图谱

第3章 模块化

分离接口与实现
声明 vs 定义

分离编译

命名空间

函数参数与返回

头文件方式
include

模块方式
import C++20

避免名字冲突

using 声明

using 指令 慎用

传值 小型数据

传引用 需要修改

传const引用 最常用

返回类型推断 auto

结构化绑定
auto n v = entry

第4章 错误处理

异常 exception

不变量 invariant

错误处理策略

断言 assertions

throw 抛出

catch 捕获

调用栈展开

构造函数建立不变量

成员函数维持不变量

异常:罕见错误
跨层传播

错误码:常见错误
立刻处理

terminate:无法恢复

assert 运行时调试

static_assert 编译期

noexcept 保证不抛

核心建议速查


建议 原因
优先使用模块,而非头文件 编译更快,没有传递性污染
用 const 引用传大型参数 避免拷贝,同时防止误修改
不要返回局部变量的引用或指针 局部变量函数返回后消亡,引用变悬空
直接返回大对象,不用返回指针 编译器自动优化(移动语义/RVO)
用异常处理"不应该发生"的错误 调用者不会忘记检查;可以跨层传播
用错误码处理"预期中"的失败 打开文件失败很正常,用 bool 返回即可
构造函数中用 throw 建立不变量 保证对象被创建后一定处于有效状态
析构函数标记 noexcept 析构中抛异常是大忌
不要滥用 noexcept 标错了反而更危险
能在编译期检查的就用 static_assert 越早发现错误越好

C++ 类与基本操作:从零开始理解

本文对应《A Tour of C++》第 5、6 章,用最通俗的语言解释每个概念。

第五章:类

5.1 三种核心类

C++ 的类分三大类型,就像建房子有三种结构方式:

C++ 的类

具体类 Concrete Class
行为像内置类型
int double 那样

抽象类 Abstract Class
只定义接口
不能直接创建对象

类层次结构 Class Hierarchy
继承关系
父类子类

例如:complex 复数
Vector 向量

例如:Container 容器接口
Shape 形状接口

例如:Shape → Circle → Smiley
父子继承链

5.2 具体类(Concrete Types)

具体类的特点:表示(数据结构)是定义的一部分,行为完全像内置类型
你可以:

  • 把它放在栈上(局部变量)
  • 直接拷贝、移动
  • 立刻完整地初始化
5.2.1 算术类型:复数 complex

复数 z = a + b i z = a + bi z=a+bi,其中 a a a 是实部, b b b 是虚部, i i i 满足 i 2 = − 1 i^2 = -1 i2=1

#include <iostream>
using namespace std;
class complex {
    // 私有数据:实部和虚部,默认值都是 0
    double re, im;
public:
    // 构造函数一:用两个 double 初始化(实部 + 虚部)
    complex(double r, double i) : re{r}, im{i} {}
    // 构造函数二:只传实部,虚部默认为 0
    // 例如:complex(3.14) → {3.14, 0}
    complex(double r) : re{r}, im{0} {}
    // 构造函数三:默认构造,两者都是 0
    // 称为"默认构造函数",保证不会有未初始化的复数对象
    complex() : re{0}, im{0} {}
    // const 成员函数:读取实部,不修改对象
    // const 表示"我保证不会改变对象的状态"
    double real() const { return re; }
    // 非 const 成员函数:设置实部
    void real(double d) { re = d; }
    double imag() const { return im; }
    void imag(double d) { im = d; }
    // 复合赋值运算符:z1 += z2 等价于 z1 = z1 + z2
    // 返回 *this(当前对象的引用),支持连续操作
    // 加法:$(a_1 + b_1 i) + (a_2 + b_2 i) = (a_1+a_2) + (b_1+b_2)i$
    complex& operator+=(complex z)
    {
        re += z.re;
        im += z.im;
        return *this;  // 返回自身引用,支持 z1 += z2 += z3
    }
    complex& operator-=(complex z)
    {
        re -= z.re;
        im -= z.im;
        return *this;
    }
};
// 类外定义的运算符:不需要直接访问私有成员
// 使用传值参数:a 是副本,可以放心修改
// 加法:$(a + bi) + (c + di) = (a+c) + (b+d)i$
complex operator+(complex a, complex b) { return a += b; }
complex operator-(complex a, complex b) { return a -= b; }
// 取负:$-(a + bi) = -a - bi$
complex operator-(complex a) { return {-a.real(), -a.imag()}; }
complex operator*(complex a, complex b)
{
    // $(a+bi)(c+di) = ac - bd + (ad+bc)i$
    double r = a.real()*b.real() - a.imag()*b.imag();
    double i = a.real()*b.imag() + a.imag()*b.real();
    return {r, i};
}
// 相等判断:实部和虚部都相等
bool operator==(complex a, complex b)
{
    return a.real() == b.real() && a.imag() == b.imag();
}
bool operator!=(complex a, complex b) { return !(a == b); }
int main()
{
    complex a{2.3};            // a = {2.3, 0}
    complex b{1.0, 2.0};      // b = {1, 2}
    complex c = a + b;        // c = {3.3, 2}
    cout << "a = " << a.real() << " + " << a.imag() << "i\n";
    cout << "b = " << b.real() << " + " << b.imag() << "i\n";
    cout << "a+b = " << c.real() << " + " << c.imag() << "i\n";
    // 编译器自动把 c != b 转换为 operator!=(c, b)
    if (c != b)
        cout << "c 不等于 b\n";
    complex z{1, 2};
    complex w = z * complex{1, 2.3};
    cout << "z*{1,2.3} = " << w.real() << " + " << w.imag() << "i\n";
    return 0;
}

const 成员函数的规则:

const 对象  → 只能调用 const 成员函数
非 const 对象 → 可以调用所有成员函数
complex z = {1, 0};        // 非 const
const complex cz = {1, 3}; // const
z = cz;            // OK:给非 const 变量赋值
// cz = z;         // 错误:不能修改 const 对象
double x = z.real();  // OK:real() 是 const 函数
5.2.2 容器类:Vector 与 RAII

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

  • 构造函数:获取资源(如分配内存)
  • 析构函数:释放资源(如释放内存)
  • 这样资源就不会泄漏,因为析构函数一定会被调用
#include <iostream>
#include <stdexcept>
using namespace std;
class Vector {
public:
    // 构造函数:分配内存(获取资源)
    // 初始化列表:elem 指向 new 分配的数组,sz 记录大小
    Vector(int s) : elem{new double[s]}, sz{s}
    {
        if (s < 0)
            throw length_error{"Vector: 大小不能为负"};
        // 初始化所有元素为 0
        for (int i = 0; i != s; ++i)
            elem[i] = 0;
    }
    // 析构函数:释放内存(释放资源)
    // 名字 = ~ + 类名,对象生命周期结束时自动调用
    // delete[] 释放 new[] 分配的数组
    ~Vector() { delete[] elem; }
    // 带边界检查的下标访问
    double& operator[](int i)
    {
        // 合法范围:$0 \leq i < sz$
        if (!(0 <= i && i < sz))
            throw out_of_range{"Vector::operator[]: 越界"};
        return elem[i];
    }
    // const 版本:用于 const Vector 对象
    const double& operator[](int i) const
    {
        if (!(0 <= i && i < sz))
            throw out_of_range{"Vector::operator[]: 越界"};
        return elem[i];
    }
    int size() const { return sz; }
private:
    double* elem;  // 指向堆上的数组
    int sz;        // 元素个数
};
int main()
{
    // gv 是全局变量:程序结束时析构
    // 局部变量:出了 {} 就析构
    try {
        Vector v(5);
        v[0] = 1.1;
        v[1] = 2.2;
        v[2] = 3.3;
        for (int i = 0; i < v.size(); ++i)
            cout << "v[" << i << "] = " << v[i] << "\n";
        // 离开这个 {} 时,v 的析构函数自动调用
        // delete[] elem 被执行,内存被释放
    }
    catch (const out_of_range& e) {
        cerr << "越界:" << e.what() << "\n";
    }
    return 0;
}

Vector 的内存模型(ASCII):

Vector 对象(栈上)        堆上分配的数组
┌──────────┬────┐         ┌──────┬──────┬──────┬──────┬──────┐
│  elem ───┼──>─┼───────> │ 1.1  │ 2.2  │ 3.3  │ 0.0  │ 0.0  │
│  sz = 5  │    │         └──────┴──────┴──────┴──────┴──────┘
└──────────┴────┘           [0]    [1]    [2]    [3]    [4]

对象的生命周期与析构时机:

Vector gv(10);          // 全局变量:程序结束时析构
void fct(int n) {
    Vector v(n);        // 局部变量
    {
        Vector v2(2*n); // 内层局部变量
        // ...
    }  // <-- v2 在这里析构(delete[] v2.elem)
    // ...
}  // <-- v 在这里析构(delete[] v.elem)
5.2.3 初始化容器

两种常用方式:初始化列表 和 push_back:

#include <iostream>
#include <initializer_list>  // initializer_list 需要这个头文件
#include <algorithm>         // copy
#include <stdexcept>
using namespace std;
class Vector {
public:
    // 默认构造函数:空向量
    Vector() : elem{nullptr}, sz{0} {}
    // 普通构造函数:指定大小
    explicit Vector(int s) : elem{new double[s]}, sz{s}
    {
        for (int i = 0; i < s; ++i) elem[i] = 0;
    }
    // 初始化列表构造函数:支持 Vector v = {1.0, 2.0, 3.0}
    // initializer_list<double> 是编译器看到 {1, 2, 3} 时自动创建的类型
    Vector(initializer_list<double> lst)
        : elem{new double[lst.size()]},
          // static_cast:把 size_t(无符号)转为 int
          // 因为 lst.size() 返回 size_t,而 sz 是 int
          sz{static_cast<int>(lst.size())}
    {
        // 把初始化列表中的元素复制到 elem 数组
        copy(lst.begin(), lst.end(), elem);
    }
    ~Vector() { delete[] elem; }
    // push_back:在末尾添加元素
    void push_back(double d)
    {
        // 简化版:重新分配更大的数组
        double* new_elem = new double[sz + 1];
        for (int i = 0; i < sz; ++i)
            new_elem[i] = elem[i];
        new_elem[sz] = d;
        delete[] elem;
        elem = new_elem;
        ++sz;
    }
    double& operator[](int i)
    {
        if (!(0 <= i && i < sz))
            throw out_of_range{"越界"};
        return elem[i];
    }
    int size() const { return sz; }
private:
    double* elem;
    int sz;
};
int main()
{
    // 方式一:初始化列表(直接给定元素值)
    Vector v1 = {1.0, 2.0, 3.0, 4.0, 5.0};
    cout << "v1 大小:" << v1.size() << "\n";
    // 方式二:push_back 逐个添加
    Vector v2;
    v2.push_back(1.1);
    v2.push_back(2.2);
    v2.push_back(3.3);
    cout << "v2 大小:" << v2.size() << "\n";
    for (int i = 0; i < v1.size(); ++i)
        cout << v1[i] << " ";
    cout << "\n";
    return 0;
}

5.3 抽象类(Abstract Types)

抽象类只定义"接口",不定义"实现"。
就像插座标准:规定了形状(接口),但具体怎么发电不管。
核心语法:纯虚函数 = 0

#include <iostream>
#include <list>
#include <stdexcept>
using namespace std;
// 抽象类:纯接口,不包含数据(除了 vtbl 指针)
class Container {
public:
    // 纯虚函数(pure virtual function):= 0 表示"必须在子类中实现"
    // 包含纯虚函数的类 = 抽象类,不能直接创建对象
    virtual double& operator[](int) = 0;
    // virtual + const:子类可以重写,且不修改对象
    virtual int size() const = 0;
    // 虚析构函数:通过基类指针 delete 时,保证调用正确的析构函数
    virtual ~Container() {}
};
// Container c;  // 错误!抽象类不能实例化
// 具体实现一:用 Vector 实现 Container 接口
// ": public Container" = "继承自 Container",即"是一种 Container"
class Vector_container : public Container {
public:
    // 构造 s 个元素的向量
    Vector_container(int s) : data(new double[s]), sz(s)
    {
        for (int i = 0; i < s; ++i) data[i] = 0;
    }
    ~Vector_container() { delete[] data; }
    // override:明确表示"这是在重写基类的虚函数"
    // 如果拼错了函数名,编译器会报错(很有用!)
    double& operator[](int i) override { return data[i]; }
    int size() const override { return sz; }
private:
    double* data;
    int sz;
};
// 具体实现二:用链表实现 Container 接口
// 内部完全不同,但对外接口相同
class List_container : public Container {
public:
    // initializer_list 构造:List_container lc = {1, 2, 3}
    List_container(initializer_list<double> il) : ld{il} {}
    ~List_container() {}
    // 链表不支持随机访问,用遍历实现下标
    double& operator[](int i) override
    {
        for (auto& x : ld) {
            if (i == 0) return x;
            --i;
        }
        throw out_of_range{"List_container: 越界"};
    }
    int size() const override { return static_cast<int>(ld.size()); }
private:
    list<double> ld;  // 标准库双向链表
};
// 使用抽象接口的函数:完全不知道 c 的真实类型!
// 无论传入 Vector_container 还是 List_container,都能工作
void use(Container& c)
{
    const int sz = c.size();
    for (int i = 0; i != sz; ++i)
        cout << c[i] << " ";
    cout << "\n";
}
int main()
{
    Vector_container vc(5);
    vc[0] = 1.0; vc[1] = 2.0; vc[2] = 3.0;
    cout << "Vector_container: ";
    use(vc);   // use 不知道 vc 是 Vector_container
    List_container lc = {10.0, 20.0, 30.0, 40.0};
    cout << "List_container: ";
    use(lc);   // use 不知道 lc 是 List_container
    // 两种完全不同的实现,同一个 use 函数都能处理
    return 0;
}

多态的本质: 同样的代码 c[i],因为 c 的实际类型不同,执行了不同的函数。

5.4 虚函数表(vtbl)

虚函数调用的实现原理:

每个有虚函数的类,都有一张"虚函数表"(vtbl)
每个该类的对象,都有一个指向 vtbl 的隐藏指针
Vector_container 对象:
┌─────────────┐       vtbl(Vector_container)
│  vptr ──────┼──>   ┌────────────────────────┐
│  data       │      │ operator[] → vc版本    │
│  sz         │      │ size()     → vc版本    │
└─────────────┘      │ ~destructor→ vc版本    │
                     └────────────────────────┘
List_container 对象:
┌─────────────┐       vtbl(List_container)
│  vptr ──────┼──>   ┌────────────────────────┐
│  ld         │      │ operator[] → lc版本    │
└─────────────┘      │ size()     → lc版本    │
                     │ ~destructor→ lc版本    │
                     └────────────────────────┘
调用 c[i]:
  1. 通过 c 找到 vptr
  2. 通过 vptr 找到 vtbl
  3. 在 vtbl 中找到 operator[] 的地址
  4. 调用那个具体实现

虚函数调用的性能开销很小(一次指针间接访问),通常可以忽略。

5.5 类层次结构

类层次结构表示"is-a"(是一种)关系:

Shape
抽象基类
center move draw rotate

Circle
圆形
center x radius r

Triangle
三角形

Smiley
笑脸
eyes mouth

完整的形状层次结构示例:

#include <iostream>
#include <vector>
#include <memory>   // unique_ptr
#include <stdexcept>
using namespace std;
// 简单的点结构
struct Point {
    double x, y;
};
// 抽象基类:定义所有形状共有的接口
class Shape {
public:
    virtual Point center() const = 0;   // 中心点
    virtual void move(Point to) = 0;    // 移动
    virtual void draw() const = 0;      // 绘制
    virtual void rotate(int angle) = 0; // 旋转
    // 虚析构函数:通过基类指针 delete 时,调用正确的析构函数
    // 如果没有这个,delete Shape* 只会调用 Shape 的析构函数
    // 导致派生类的资源(如 eyes, mouth)不被释放!
    virtual ~Shape() {}
};
// 派生类一:圆形
class Circle : public Shape {
public:
    // Circle 的构造函数:初始化圆心和半径
    Circle(Point p, int rad) : x{p}, r{rad} {}
    // override:实现基类的纯虚函数
    Point center() const override { return x; }
    void  move(Point to) override { x = to; }
    void  draw() const override
    {
        cout << "绘制圆形,圆心(" << x.x << "," << x.y
             << "),半径=" << r << "\n";
    }
    void rotate(int) override {}  // 圆旋转后不变,空实现
private:
    Point x;  // 圆心
    int   r;  // 半径
};
// 派生类二:笑脸(继承自 Circle)
// Smiley "是一种" Circle,Circle "是一种" Shape
class Smiley : public Circle {
public:
    Smiley(Point p, int rad) : Circle{p, rad}, mouth{nullptr} {}
    // 析构函数:释放 eyes 和 mouth
    // unique_ptr 会自动释放,所以其实可以省略这个析构函数
    ~Smiley()
    {
        // unique_ptr 自动 delete,不需要手动写
    }
    void draw() const override
    {
        Circle::draw();  // 先画圆(脸)
        for (const auto& eye : eyes)
            eye->draw();  // 画眼睛
        if (mouth)
            mouth->draw();  // 画嘴巴
        cout << "绘制笑脸完成\n";
    }
    void move(Point to) override
    {
        Circle::move(to);  // 移动圆(脸)
        // 眼睛和嘴巴也要跟着移动(简化省略)
    }
    void rotate(int angle) override {}
    // 添加眼睛:接受 Shape* 的所有权(用 unique_ptr 管理)
    void add_eye(unique_ptr<Shape> s)
    {
        eyes.push_back(move(s));  // move 把所有权转移进 vector
    }
    void set_mouth(unique_ptr<Shape> s)
    {
        mouth = move(s);
    }
private:
    // 用 unique_ptr 而非裸指针:析构时自动释放,不会泄漏!
    vector<unique_ptr<Shape>> eyes;  // 眼睛列表(通常 2 个)
    unique_ptr<Shape>         mouth; // 嘴巴
};
// 对所有形状统一操作:不关心具体类型
void rotate_all(vector<unique_ptr<Shape>>& v, int angle)
{
    for (auto& p : v)
        p->rotate(angle);
}
void draw_all(const vector<unique_ptr<Shape>>& v)
{
    for (const auto& p : v)
        p->draw();
}
int main()
{
    // 创建各种形状,存入 vector
    vector<unique_ptr<Shape>> shapes;
    shapes.push_back(make_unique<Circle>(Point{0, 0}, 10));
    auto smiley = make_unique<Smiley>(Point{5, 5}, 20);
    smiley->add_eye(make_unique<Circle>(Point{3, 7}, 2));
    smiley->add_eye(make_unique<Circle>(Point{7, 7}, 2));
    // 嘴巴用一个小圆代替
    smiley->set_mouth(make_unique<Circle>(Point{5, 3}, 3));
    shapes.push_back(move(smiley));
    // draw_all 不知道里面是 Circle 还是 Smiley
    // 通过虚函数,自动调用正确的 draw()
    draw_all(shapes);
    // 离开作用域,unique_ptr 自动 delete 所有形状
    // 不会有内存泄漏!
    return 0;
}
5.5.1 继承的两种好处

类型 含义 例子
接口继承 子类对象可以当父类对象用(多态) use(Container&) 接受任何容器
实现继承 子类复用父类的代码(函数和数据) Smiley 复用 Circle::draw()

5.5.2 层次导航:dynamic_cast

当你只有基类指针,但想访问派生类特有的函数时,用 dynamic_cast

#include <iostream>
#include <memory>
using namespace std;
struct Point { double x, y; };
class Shape {
public:
    virtual void draw() const = 0;
    virtual ~Shape() {}
};
class Circle : public Shape {
public:
    Circle(Point p, int r) : center{p}, radius{r} {}
    void draw() const override
    {
        cout << "Circle at (" << center.x << "," << center.y << ")\n";
    }
private:
    Point center;
    int   radius;
};
class Smiley : public Circle {
public:
    Smiley(Point p, int r) : Circle{p, r} {}
    void draw() const override
    {
        Circle::draw();
        cout << "  (with smiley face)\n";
    }
    // Smiley 特有的函数,Shape 里没有
    void wink(int eye_index)
    {
        cout << "眨眼睛 #" << eye_index << "\n";
    }
};
int main()
{
    // ps 是 Shape*,指向一个 Smiley 对象
    Shape* ps = new Smiley{Point{0,0}, 10};
    // dynamic_cast<Smiley*>(ps):
    // 问:"ps 实际上指向 Smiley 对象吗?"
    // 如果是:返回 Smiley* 指针
    // 如果不是:返回 nullptr
    if (Smiley* p = dynamic_cast<Smiley*>(ps)) {
        p->wink(0);       // 可以调用 Smiley 特有的函数
        cout << "成功转换为 Smiley*\n";
    } else {
        cout << "不是 Smiley\n";
    }
    // 另一种:转换为引用类型
    // 如果失败,抛出 bad_cast 异常(而不是返回 nullptr)
    try {
        Smiley& r = dynamic_cast<Smiley&>(*ps);
        r.wink(1);
    }
    catch (const bad_cast& e) {
        cerr << "类型转换失败:" << e.what() << "\n";
    }
    delete ps;
    return 0;
}

转换方式 失败时 适用场景
dynamic_cast<T*>(ptr) 返回 nullptr 不知道是否成功
dynamic_cast<T&>(*ptr) 抛出 bad_cast 必须成功

5.5.3 用 unique_ptr 避免资源泄漏

裸指针很容易忘记 delete,用 unique_ptr 让编译器帮你管理:

【裸指针版本:容易泄漏】      【unique_ptr 版本:安全】
Shape* p = new Circle{...};   auto p = make_unique<Circle>(...);
if (x < 0) throw Bad{};  // 泄漏!  if (x < 0) throw Bad{};  // 安全,p 自动释放
if (x == 0) return;       // 泄漏!  if (x == 0) return;       // 安全
// ...                               // ...
delete p;  // 很容易忘               // 无需 delete,自动释放

第六章:基本操作

6.1 六个关键特殊成员函数

当一个类管理资源(如内存),这六个函数需要配套定义:

六个关键成员函数

普通构造函数
X(Sometype)

默认构造函数
X()

拷贝构造函数
X(const X&)

移动构造函数
X(X&&)

拷贝赋值
X& operator=(const X&)

移动赋值
X& operator=(X&&)

析构函数
~X()

零法则(Rule of Zero): 要么定义全部,要么一个都不定义(全用默认)。

#include <iostream>
#include <string>
using namespace std;
// 零法则示例:结构体成员都有良好的拷贝/移动语义
// 编译器会自动合成正确的拷贝、移动、析构
struct GoodStruct {
    string name;   // string 已经定义了拷贝和移动
    int    value;
    // 不需要手动定义任何特殊函数!编译器自动处理
};
// 显式指定使用默认实现
class ExplicitDefault {
public:
    ExplicitDefault(int v) : val{v} {}
    ExplicitDefault(const ExplicitDefault&) = default;  // 明确要默认拷贝
    ExplicitDefault(ExplicitDefault&&)      = default;  // 明确要默认移动
    ~ExplicitDefault() = default;
private:
    int val;
};
// 禁止拷贝(用于不应被拷贝的类型,如基类)
class NoCopy {
public:
    NoCopy() = default;
    NoCopy(const NoCopy&)            = delete;  // 禁止拷贝构造
    NoCopy& operator=(const NoCopy&) = delete;  // 禁止拷贝赋值
};
int main()
{
    GoodStruct a{"Alice", 42};
    GoodStruct b = a;   // 自动合成的拷贝构造函数
    b.name = "Bob";
    cout << "a=" << a.name << " b=" << b.name << "\n";  // a=Alice b=Bob
    NoCopy nc;
    // NoCopy nc2 = nc;  // 编译错误:拷贝被禁止
    return 0;
}
6.1.2 explicit 防止隐式转换
#include <iostream>
#include <string>
using namespace std;
class Vector {
public:
    // explicit:只允许显式构造,禁止隐式转换
    // 如果不写 explicit:Vector v = 7 会悄悄成功(通常不是你想要的)
    explicit Vector(int s) : elem{new double[s]}, sz{s}
    {
        for (int i = 0; i < s; ++i) elem[i] = 0;
    }
    ~Vector() { delete[] elem; }
    int size() const { return sz; }
private:
    double* elem;
    int sz;
};
// complex 允许隐式转换:complex z = 3.14 是合理的
class complex {
    double re = 0, im = 0;
public:
    // 没有 explicit:允许隐式转换 double → complex
    complex(double r, double i) : re{r}, im{i} {}
    complex(double r) : re{r}, im{0} {}  // 隐式:complex z = 3.14 OK
    complex() {}
    double real() const { return re; }
    double imag() const { return im; }
};
int main()
{
    Vector v1(7);    // OK:显式构造
    // Vector v2 = 7;  // 编译错误!explicit 禁止隐式转换
    complex z1 = 3.14;   // OK:允许隐式 double → complex
    complex z2 = z1 * 2; // OK:2 隐式转为 complex{2, 0}
    cout << "z1 = " << z1.real() << " + " << z1.imag() << "i\n";
    cout << "z2 = " << z2.real() << " + " << z2.imag() << "i\n";
    return 0;
}
6.1.3 成员默认初始化值
#include <iostream>
using namespace std;
class complex {
    // 数据成员有默认值:即使没有初始化列表,也保证是 0
    double re = 0;
    double im = 0;
public:
    complex(double r, double i) : re{r}, im{i} {}  // 覆盖默认值
    complex(double r) : re{r} {}   // im 用默认值 0
    complex() {}                   // re 和 im 都用默认值 0
    double real() const { return re; }
    double imag() const { return im; }
};
int main()
{
    complex z1{1.5, 2.5};  // re=1.5, im=2.5
    complex z2{3.0};       // re=3.0, im=0(默认值)
    complex z3{};          // re=0, im=0(默认值)
    cout << "z1 = " << z1.real() << " + " << z1.imag() << "i\n";
    cout << "z2 = " << z2.real() << " + " << z2.imag() << "i\n";
    cout << "z3 = " << z3.real() << " + " << z3.imag() << "i\n";
    return 0;
}

6.2 拷贝与移动

6.2.1 拷贝容器

默认的"成员逐一拷贝"对于含指针的类是危险的:

【默认拷贝(危险!)】
Vector v2 = v1;
v1: ┌──────────┬────┐      ┌──────┬──────┬──────┬──────┐
    │ elem ────┼──> ┼──>   │ 1.0  │ 2.0  │ 3.0  │ 4.0  │
    │ sz = 4   │    │      └──────┴──────┴──────┴──────┘
    └──────────┴────┘            ↑
v2: ┌──────────┬────┐            │(指向同一块内存!)
    │ elem ────┼──> ┼────────────┘
    │ sz = 4   │    │
    └──────────┴────┘
问题:v1[0] = 999 → v2[0] 也变成 999!
问题:v1 析构 delete[] elem → v2 的 elem 成了悬空指针!
【正确拷贝(深拷贝)】
正确的拷贝构造函数会分配新内存并复制内容:
v1: ┌──────────┬────┐      ┌──────┬──────┬──────┬──────┐
    │ elem ────┼──> ┼──>   │ 1.0  │ 2.0  │ 3.0  │ 4.0  │
    │ sz = 4   │    │      └──────┴──────┴──────┴──────┘
v2: ┌──────────┬────┐      ┌──────┬──────┬──────┬──────┐
    │ elem ────┼──> ┼──>   │ 1.0  │ 2.0  │ 3.0  │ 4.0  │(独立副本)
    │ sz = 4   │    │      └──────┴──────┴──────┴──────┘

完整的拷贝构造与拷贝赋值:

#include <iostream>
#include <stdexcept>
using namespace std;
class Vector {
public:
    explicit Vector(int s) : elem{new double[s]}, sz{s}
    {
        for (int i = 0; i < s; ++i) elem[i] = 0;
    }
    // 拷贝构造函数:Vector v2 = v1
    // 分配新内存,复制所有元素
    Vector(const Vector& a)
        : elem{new double[a.sz]},  // 分配同样大小的新数组
          sz{a.sz}
    {
        for (int i = 0; i != sz; ++i)
            elem[i] = a.elem[i];   // 逐元素复制
    }
    // 拷贝赋值:v2 = v1(v2 已经存在)
    Vector& operator=(const Vector& a)
    {
        // 先分配新内存(如果中途抛异常,旧数据还在)
        double* p = new double[a.sz];
        for (int i = 0; i != a.sz; ++i)
            p[i] = a.elem[i];
        // 确认新数据没问题后,才释放旧内存
        delete[] elem;
        elem = p;
        sz   = a.sz;
        // 返回 *this 支持链式赋值:v1 = v2 = v3
        return *this;
    }
    ~Vector() { delete[] elem; }
    double& operator[](int i)
    {
        if (!(0 <= i && i < sz)) throw out_of_range{"越界"};
        return elem[i];
    }
    int size() const { return sz; }
private:
    double* elem;
    int sz;
};
int main()
{
    Vector v1(4);
    v1[0] = 1.0; v1[1] = 2.0; v1[2] = 3.0; v1[3] = 4.0;
    Vector v2 = v1;   // 调用拷贝构造函数
    v1[0] = 99.0;     // 修改 v1,v2 不受影响
    cout << "v1[0] = " << v1[0] << "\n";  // 99
    cout << "v2[0] = " << v2[0] << "\n";  // 1(独立副本)
    Vector v3(2);
    v3 = v1;           // 调用拷贝赋值运算符
    cout << "v3[0] = " << v3[0] << "\n";  // 99
    return 0;
}
6.2.2 移动语义

移动的场景: 当一个对象即将"消亡"(如函数返回的临时对象),没必要拷贝,直接"偷走"它的资源更高效。
拷贝代价 = O ( n ) , 移动代价 = O ( 1 ) \text{拷贝代价} = O(n),\quad \text{移动代价} = O(1) 拷贝代价=O(n)移动代价=O(1)

【拷贝 vs 移动】
拷贝:  源对象仍然有效
        ┌──────┐          ┌──────┐
        │ data │ ──复制─> │ data │(新分配内存)
        └──────┘          └──────┘
移动:  源对象被"掏空"(指针设为 nullptr)
        ┌──────┐          ┌──────┐
        │ data │ ──窃取─> │ data │(同一块内存,无需分配)
        │nullptr│          │      │
        └──────┘          └──────┘
#include <iostream>
#include <stdexcept>
#include <utility>   // std::move
using namespace std;
class Vector {
public:
    explicit Vector(int s) : elem{new double[s]}, sz{s}
    {
        for (int i = 0; i < s; ++i) elem[i] = 0;
    }
    // 拷贝构造(深拷贝)
    Vector(const Vector& a) : elem{new double[a.sz]}, sz{a.sz}
    {
        for (int i = 0; i != sz; ++i) elem[i] = a.elem[i];
    }
    // 移动构造函数:X(X&&) 的 && 表示"右值引用"
    // 右值:临时对象、即将消亡的对象,没有名字(不能放在赋值左边)
    // 移动构造:直接"偷走" a 的内存,把 a 设为空
    Vector(Vector&& a) noexcept
        : elem{a.elem},  // 直接接管 a 的指针,不分配新内存
          sz{a.sz}
    {
        a.elem = nullptr;  // a 的指针设为空(不能让 a 析构时 delete 我们的内存)
        a.sz   = 0;
        // 整个操作是 O(1),无论 Vector 有多大!
    }
    // 移动赋值
    Vector& operator=(Vector&& a) noexcept
    {
        delete[] elem;     // 释放自己原有的内存
        elem   = a.elem;   // 接管 a 的内存
        sz     = a.sz;
        a.elem = nullptr;  // 清空 a
        a.sz   = 0;
        return *this;
    }
    ~Vector()
    {
        delete[] elem;  // elem 可能是 nullptr(被移动走后),delete nullptr 是安全的
    }
    double& operator[](int i)
    {
        if (!(0 <= i && i < sz)) throw out_of_range{"越界"};
        return elem[i];
    }
    int size() const { return sz; }
};
// 返回一个 Vector:编译器会用移动构造,而非拷贝构造
// 省去了 O(n) 的拷贝代价
Vector make_vector(int n)
{
    Vector result(n);
    for (int i = 0; i < n; ++i)
        result[i] = i * 1.5;
    return result;  // 移动语义(或编译器直接构造,即 RVO)
}
int main()
{
    // 调用移动构造(或 RVO 优化掉拷贝)
    Vector v = make_vector(5);
    cout << "v 大小: " << v.size() << "\n";
    Vector x(3);
    x[0] = 10.0; x[1] = 20.0; x[2] = 30.0;
    Vector y(4);
    // std::move(x):告诉编译器"x 不再需要了,可以移动"
    // 调用移动赋值,而非拷贝赋值
    y = std::move(x);
    // 注意:x 现在处于"有效但未指定"状态,不应再使用 x 的内容
    cout << "y[0] = " << y[0] << "\n";  // 10
    return 0;
}

std::move 不真正移动任何东西,它只是把参数转换为右值引用,让编译器选择移动版本的操作:
std::move ( x ) ≡ static_cast < Vector&& > ( x ) \text{std::move}(x) \equiv \text{static\_cast}<\text{Vector\&\&}>(x) std::move(x)static_cast<Vector&&>(x)

6.3 资源管理总结

RAII 是 C++ 资源管理的核心哲学:

构造函数
获取资源
new lock open

对象使用阶段
资源安全持有

析构函数
释放资源
delete unlock close

资源不会泄漏
析构一定被调用

标准库中 RAII 无处不在:

资源类型 RAII 包装器
内存 vector, string, unique_ptr
文件 ifstream, ofstream
线程 thread
互斥锁 lock_guard, unique_lock
通用对象 unique_ptr, shared_ptr

6.4 运算符重载

为自定义类型赋予运算符意义,让代码更自然:

#include <iostream>
using namespace std;
class Matrix {
public:
    Matrix(int r, int c) : rows{r}, cols{c}, data{new double[r*c]}
    {
        for (int i = 0; i < r*c; ++i) data[i] = 0;
    }
    Matrix(const Matrix& m) : rows{m.rows}, cols{m.cols}, data{new double[m.rows*m.cols]}
    {
        for (int i = 0; i < rows*cols; ++i) data[i] = m.data[i];
    }
    ~Matrix() { delete[] data; }
    double& operator()(int r, int c) { return data[r*cols + c]; }
    double  operator()(int r, int c) const { return data[r*cols + c]; }
    int get_rows() const { return rows; }
    int get_cols() const { return cols; }
    // 成员函数形式的赋值运算符(= 必须是成员函数)
    Matrix& operator=(const Matrix& a)
    {
        if (this == &a) return *this;  // 防止自赋值
        delete[] data;
        rows = a.rows; cols = a.cols;
        data = new double[rows * cols];
        for (int i = 0; i < rows*cols; ++i) data[i] = a.data[i];
        return *this;
    }
private:
    int rows, cols;
    double* data;
};
// 自由函数形式:+ 两边操作数对称处理
// 依赖移动语义避免拷贝开销
Matrix operator+(const Matrix& a, const Matrix& b)
{
    Matrix result(a.get_rows(), a.get_cols());
    for (int i = 0; i < a.get_rows(); ++i)
        for (int j = 0; j < a.get_cols(); ++j)
            result(i, j) = a(i, j) + b(i, j);
    return result;  // 移动语义:直接移走 result
}
bool operator==(const Matrix& a, const Matrix& b)
{
    if (a.get_rows() != b.get_rows() || a.get_cols() != b.get_cols())
        return false;
    for (int i = 0; i < a.get_rows(); ++i)
        for (int j = 0; j < a.get_cols(); ++j)
            if (a(i, j) != b(i, j)) return false;
    return true;
}
int main()
{
    Matrix a(2, 2), b(2, 2);
    a(0,0) = 1; a(0,1) = 2;
    a(1,0) = 3; a(1,1) = 4;
    b(0,0) = 5; b(0,1) = 6;
    b(1,0) = 7; b(1,1) = 8;
    Matrix c = a + b;  // 调用 operator+,用移动语义返回
    cout << "c(0,0) = " << c(0,0) << "\n";  // 6
    cout << "c(1,1) = " << c(1,1) << "\n";  // 12
    return 0;
}

可重载的运算符列表(节选):

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

不能重载: .(成员访问)、.*(成员指针)、::(作用域)、?:(三元)

6.5 常规操作

6.5.1 比较运算符与飞船运算符

C++20 引入了"飞船运算符" <=>(三路比较),让 < < <, ≤ \leq , > > >, ≥ \geq , = = == ==, ! = != != 都能一次定义:
a ⇔ b = { 负数 a < b 0 a = b 正数 a > b \text{a} \Leftrightarrow \text{b} = \begin{cases} \text{负数} & a < b \\ 0 & a = b \\ \text{正数} & a > b \end{cases} ab= 负数0正数a<ba=ba>b

#include <iostream>
#include <compare>   // C++20 飞船运算符
using namespace std;
class Point {
public:
    Point(int x, int y) : x{x}, y{y} {}
    // 默认飞船运算符:编译器自动按成员顺序比较
    // 定义 <=> 后,< <= > >= 都自动可用
    auto operator<=>(const Point&) const = default;
    // 注意:= default 的 <=> 同时也隐式定义了 ==
    // 所以下面这行可以省略
    // bool operator==(const Point&) const = default;
    int get_x() const { return x; }
    int get_y() const { return y; }
private:
    int x, y;
};
// 对于复杂类型,自定义 <=> 后需要单独定义 ==
struct R2 {
    int m;
    // 自定义三路比较(不用 default)
    auto operator<=>(const R2& a) const
    {
        // 三元表达式:条件 ? 真值 : 假值
        return m == a.m ? 0 : m < a.m ? -1 : 1;
    }
    // 自定义 <=> 后,== 不会自动生成,需要手动写
    bool operator==(const R2& a) const { return m == a.m; }
};
int main()
{
    Point p1{1, 2}, p2{3, 4}, p3{1, 2};
    cout << boolalpha;
    cout << "p1 < p2: "  << (p1 < p2)  << "\n";   // true(先比 x,1 < 3)
    cout << "p1 == p3: " << (p1 == p3) << "\n";   // true
    cout << "p2 > p1: "  << (p2 > p1)  << "\n";   // true
    // 飞船运算符的直接结果
    auto result = (p1 <=> p2);
    cout << "p1 <=> p2 是负数(p1 < p2): " << (result < 0) << "\n";
    R2 r1{5}, r2{3};
    cout << "r1 > r2: "  << (r1 > r2)  << "\n";   // true
    cout << "r1 == r2: " << (r1 == r2) << "\n";   // false
    return 0;
}
6.5.2 容器操作:begin/end 与迭代器
#include <iostream>
#include <vector>
#include <algorithm>   // sort
using namespace std;
int main()
{
    vector<int> v = {5, 3, 1, 4, 2};
    // 传统下标循环
    for (size_t i = 0; i != v.size(); ++i)
        cout << v[i] << " ";
    cout << "\n";
    // 迭代器循环:p 类似指针
    // begin() 指向第一个元素,end() 指向最后一个元素的下一位
    for (auto p = v.begin(); p != v.end(); ++p)
        *p *= 2;   // *p 取迭代器指向的值
    // 范围 for(最简洁,底层用 begin/end 实现)
    for (auto& x : v)
        cout << x << " ";
    cout << "\n";
    // 标准算法:sort 需要 begin 和 end 迭代器
    sort(v.begin(), v.end());
    for (auto x : v)
        cout << x << " ";
    cout << "\n";
    return 0;
}

迭代器的概念(就像指针一样操作):

vector<int> v = {10, 20, 30, 40, 50}
begin()                         end()
  ↓                               ↓
┌────┬────┬────┬────┬────┐   ┌────┐
│ 10 │ 20 │ 30 │ 40 │ 50 │   │(过)│
└────┴────┴────┴────┴────┘   └────┘
  p    p++  p++  p++  p++
*p 取值,++p 移到下一个,p != end() 判断是否结束
6.6 用户自定义字面量

让自定义类型也能像内置类型一样有字面量后缀:

#include <iostream>
#include <complex>
#include <chrono>   // std::chrono 时间库
using namespace std;
// 自定义虚数字面量
// operator"" 后面的 i 是后缀名
// long double 是参数类型(浮点数字面量用 long double)
// 返回 complex<double>
constexpr complex<double> operator""_i(long double arg)
{
    return {0, static_cast<double>(arg)};
}
// 自定义角度字面量(度转弧度)
// $\text{弧度} = \text{度} \times \frac{\pi}{180}$
constexpr double operator""_deg(long double deg)
{
    return deg * 3.14159265358979 / 180.0;
}
int main()
{
    // 使用自定义字面量
    auto z = 2.7182818 + 6.283185_i;  // 复数 2.718 + 6.283i
    cout << "实部: " << z.real() << " 虚部: " << z.imag() << "\n";
    double angle = 90.0_deg;  // 90 度 = π/2 弧度
    cout << "90度 = " << angle << " 弧度\n";
    // 标准库自带的字面量(需要 using namespace)
    using namespace std::literals::chrono_literals;
    auto duration = 5s + 300ms;  // 5秒 + 300毫秒
    cout << "时长(毫秒): "
         << chrono::duration_cast<chrono::milliseconds>(duration).count()
         << " ms\n";
    using namespace std::literals::string_literals;
    auto s = "Hello"s;  // std::string,不是 const char*
    cout << "字符串类型: " << s << "\n";
    return 0;
}

标准库内置的字面量后缀:

后缀 含义 例子
s std::string "Hello"s
sv string_view "Hello"sv
s 秒(chrono) 5s
ms 毫秒 300ms
us 微秒 100us
ns 纳秒 50ns
i 虚数 3.14i

总结:第 5-6 章知识图谱

第5章 类

具体类
像内置类型一样使用

抽象类
只定义接口 纯虚函数

类层次结构
继承 多态

complex 算术类型

Vector 容器
RAII 管理内存

初始化列表构造
push_back

纯虚函数 =0

虚析构函数

通过指针或引用使用

virtual 虚函数

override 明确重写

vtbl 虚函数表

dynamic_cast 运行时类型转换

unique_ptr 避免泄漏

第6章 基本操作

六大特殊成员函数

拷贝与移动

RAII 资源管理

运算符重载

常规操作约定

= default 使用默认

= delete 禁止操作

explicit 禁止隐式转换

成员默认初始化值

深拷贝构造函数

拷贝赋值运算符

移动构造 X&&
O1 代价

std::move 触发移动

构造获取 析构释放

unique_ptr shared_ptr

成员函数形式
修改第一操作数

自由函数形式
对称操作符

飞船运算符 <=>

begin end 迭代器

用户自定义字面量

核心建议速查


建议 原因
定义析构函数时,同时定义拷贝和移动 有析构说明管理资源,默认拷贝很可能是错的
单参数构造函数加 explicit 防止意外的隐式类型转换
用成员默认初始化值代替繁琐的构造函数 更简洁,防止遗漏初始化
用 unique_ptr 代替裸 new/delete 自动释放,不会泄漏
虚基类必须有虚析构函数 通过基类指针 delete 时才能调用正确的析构函数
用 override 标记重写的虚函数 编译器能检查拼写错误和签名不匹配
用移动语义返回大对象,不用返回指针 更安全,效率同样高( O ( 1 ) O(1) O(1) 移动)
为对称运算符定义自由函数 两个操作数被平等对待
定义 <=> 后还需单独定义 == 自定义的 <=> 不会自动生成 ==
避免裸 new 和裸 delete 用资源句柄(RAII)代替,代码更安全、更易读

C++ 模板与泛型编程 — 从零理解

本文对应《A Tour of C++》第7、8章,目标是让没有模板基础的读者也能完全看懂。

目录

  1. 什么是模板?
  2. 参数化类型(类模板)
  3. 约束模板参数(Concepts 初探)
  4. 值作为模板参数
  5. 模板参数自动推导(CTAD)
  6. 参数化操作
  7. 模板机制工具箱
  8. Concepts 深入
  9. 泛型编程思想
  10. 可变参数模板
  11. 模板编译模型

1. 什么是模板?

核心思想

想象你要写一个"存放东西的盒子":

  • 存放 double 的盒子 → 写一个 BoxOfDouble
  • 存放 string 的盒子 → 再写一个 BoxOfString
  • 存放 int 的盒子 → 再再写一个 BoxOfInt
    这三个类几乎完全一样,只是元素类型不同。重复劳动!
    模板的作用就是:把类型本身作为参数,写一次代码,适用于所有类型。
"对于所有类型 T,我有一个 Box<T>"

数学上类似于: ∀ T ,  Box ( T ) \forall T,\ \text{Box}(T) T, Box(T) 成立。

2. 参数化类型(类模板)

2.1 基本语法

#include <iostream>
#include <stdexcept>  // std::out_of_range, std::length_error
using namespace std;
// template<typename T> 告诉编译器:T 是一个类型参数
// 就像函数参数一样,只不过这里传入的是"类型"而不是"值"
template<typename T>
class Vector {
private:
    T* elem;  // 指向 T 类型数组的指针
    int sz;   // 数组大小
public:
    // 构造函数:分配 s 个 T 类型的元素
    explicit Vector(int s) {
        if (s < 0)
            throw length_error{"Vector: 负数大小"};
        elem = new T[s];  // 用 new 分配堆内存
        sz = s;
    }
    // 析构函数:释放内存(RAII 原则)
    ~Vector() {
        delete[] elem;
    }
    // 下标运算符(非 const 版本,可修改元素)
    T& operator[](int i) {
        if (i < 0 || sz <= i)
            throw out_of_range{"Vector::operator[]"};
        return elem[i];
    }
    // 下标运算符(const 版本,只读)
    const T& operator[](int i) const {
        if (i < 0 || sz <= i)
            throw out_of_range{"Vector::operator[]"};
        return elem[i];
    }
    int size() const { return sz; }
};
// 为了支持 range-for 循环(for (auto x : v)),需要 begin/end
template<typename T>
T* begin(Vector<T>& x) {
    return &x[0];           // 指向第一个元素
}
template<typename T>
T* end(Vector<T>& x) {
    return &x[0] + x.size(); // 指向最后一个元素的下一位
}
int main() {
    Vector<int>    vi(5);    // 存放 int 的 Vector
    Vector<double> vd(3);    // 存放 double 的 Vector
    // 赋值
    for (int i = 0; i < vi.size(); ++i)
        vi[i] = i * 10;
    // range-for 遍历(依赖 begin/end)
    for (auto x : vi)
        cout << x << " ";   // 输出: 0 10 20 30 40
    cout << "\n";
    return 0;
}

2.2 实例化过程

当你写 Vector<int> 时,编译器会把模板里所有的 T 替换成 int生成真实代码

模板定义(抽象)
     |
     | 用 T=int 实例化
     v
Vector<int> 的具体代码(编译期生成)

这个过程叫模板实例化(Template Instantiation),发生在编译期,运行时零开销。

3. 约束模板参数(Concepts 初探)

3.1 问题:无约束模板太宽松

// 没有约束:T 可以是任何类型,包括"不能复制"的类型
template<typename T>
class Vector { ... };
// 编译期报错,但错误信息很难懂:
// Vector<thread> v;  // thread 不能被复制

3.2 用 Concept 加约束

Concept 就像一份合同:“只接受满足某些条件的类型”。

"对于所有满足 Element(T) 条件的类型 T"
数学表达:$\forall T \text{ s.t. Element}(T),\ \text{Vector}(T)$ 合法
// template<Element T> 表示 T 必须满足 Element 这个概念
// 这是 C++20 的写法
template<typename T>
concept Element = std::copyable<T>;  // 简单起见:T 必须可复制
template<Element T>
class SafeVector {
    // ...
};

写法 含义 错误时机
template<typename T> T 可以是任何类型 实例化时(很晚,报错难懂)
template<Element T> T 必须满足 Element 调用点(立刻,报错清晰)

4. 值作为模板参数

模板不仅能接受类型参数,还能接受参数(必须是编译期常量):

#include <iostream>
using namespace std;
// T 是类型参数,N 是值参数(整数)
template<typename T, int N>
struct Buffer {
    T elem[N];              // 固定大小数组,存放在栈上!
    constexpr int size() const { return N; }
};
int main() {
    // 1024 个 char,分配在全局区(静态存储)
    Buffer<char, 1024> glob;
    // 10 个 int,分配在栈上(无堆分配!)
    Buffer<int, 10> buf;
    buf.elem[0] = 42;
    cout << "Buffer 大小: " << buf.size() << "\n";  // 10
    return 0;
}

优点:大小在编译期确定,不用动态内存,性能极高。
类比: Buffer ( T , N ) \text{Buffer}(T, N) Buffer(T,N) 就像数学里的 R N \mathbb{R}^N RN N N N 是维度,写死在类型里。

5. 模板参数自动推导(CTAD)

C++17 引入类模板参数推导(Class Template Argument Deduction,CTAD)

#include <iostream>
#include <utility>   // std::pair
#include <vector>
using namespace std;
int main() {
    // 旧写法(C++17 之前必须显式指定)
    pair<int, double> p1 = {1, 5.2};
    // 新写法(C++17+):编译器从初始化值推导出 pair<int, double>
    pair p2 = {1, 5.2};
    // vector 也一样
    vector v1 = {1, 2, 3};        // 推导为 vector<int>
    vector v2 = {1.1, 2.2, 3.3};  // 推导为 vector<double>
    cout << p2.first << ", " << p2.second << "\n";  // 1, 5.2
    return 0;
}

推导陷阱

#include <vector>
#include <string>
using namespace std;
int main() {
    // "Hello" 的类型是 const char[6],不是 string!
    // vs1 被推导为 vector<const char*>,可能出乎意料
    vector vs1 = {"Hello", "World"};
    // 加 s 后缀变成 std::string,推导为 vector<string>
    vector vs2 = {"Hello"s, "World"s};  // OK
    // 混合类型 → 编译错误(无法推导唯一类型)
    // vector vs3 = {"Hello"s, "World"};  // 错误!
    return 0;
}

6. 参数化操作

有三种方式表达"参数化的操作":

三种方式
├── 函数模板(Function Template)
├── 函数对象(Function Object / Functor)
└── Lambda 表达式

6.1 函数模板

#include <iostream>
#include <vector>
#include <list>
#include <complex>
using namespace std;
// Sequence: 任何支持 range-for 的序列
// Value:    任何支持 += 的数值类型
template<typename Sequence, typename Value>
Value sum(const Sequence& s, Value v) {
    for (const auto& x : s)
        v += x;   // 把每个元素加到 v 上
    return v;
}
int main() {
    vector<int>    vi = {1, 2, 3, 4, 5};
    list<double>   ld = {1.1, 2.2, 3.3};
    int    s1 = sum(vi, 0);          // 对 int 求和
    double s2 = sum(vi, 0.0);        // 对 int 序列用 double 累加(防溢出)
    double s3 = sum(ld, 0.0);        // 对 double 列表求和
    cout << s1 << "\n";  // 15
    cout << s2 << "\n";  // 15.0
    cout << s3 << "\n";  // 6.6
    return 0;
}

为什么需要 Value v 参数? 因为初始值决定了累加类型:

  • sum(vi, 0) → 用 int 累加
  • sum(vi, 0.0) → 用 double 累加,避免 int 溢出

6.2 函数对象(仿函数)

函数对象是可以像函数一样被调用的对象,通过重载 operator() 实现:

#include <iostream>
#include <vector>
#include <string>
using namespace std;
// Less_than<T>:创建一个"小于某值"的判断器
template<typename T>
class Less_than {
    const T val;  // 保存比较基准值
public:
    Less_than(const T& v) : val{v} {}
    // operator() 让对象可以像函数一样被调用
    // 返回 x < val 的结果
    bool operator()(const T& x) const {
        return x < val;
    }
};
// 通用计数函数:统计容器中满足谓词 pred 的元素个数
template<typename C, typename P>
int count(const C& c, P pred) {
    int cnt = 0;
    for (const auto& x : c)
        if (pred(x))  // 调用谓词(函数对象或 lambda)
            ++cnt;
    return cnt;
}
int main() {
    vector<int>    vi = {1, 5, 3, 8, 2, 9, 4};
    vector<string> vs = {"apple", "banana", "cherry", "date"};
    Less_than<int>    lti{5};        // 小于 5
    Less_than<string> lts{"cherry"}; // 字典序小于 "cherry"
    cout << count(vi, lti)   << "\n"; // 输出 3(1,3,2 小于 5)
    cout << count(vs, lts)   << "\n"; // 输出 2(apple, banana)
    // 也可以直接在调用处构造
    cout << count(vi, Less_than<int>{10}) << "\n"; // 全部 < 10,输出 7
    return 0;
}

函数对象的优势:

  • 携带状态(val 字段)
  • 编译器容易内联,效率高
  • 可复用、可命名

6.3 Lambda 表达式

Lambda 是就地定义函数对象的简写语法

#include <iostream>
#include <vector>
#include <string>
using namespace std;
template<typename C, typename P>
int count(const C& c, P pred) {
    int cnt = 0;
    for (const auto& x : c)
        if (pred(x)) ++cnt;
    return cnt;
}
int main() {
    vector<int> vi = {1, 5, 3, 8, 2, 9, 4};
    int x = 5;
    // Lambda 写法:[&] 表示捕获外部变量(按引用)
    // (int a) 是参数列表
    // { return a < x; } 是函数体
    int n = count(vi, [&](int a){ return a < x; });
    cout << n << "\n";  // 3
    // 对比:等价的函数对象写法(Less_than<int>{5})
    // Lambda 更简洁,适合"用一次就扔"的场景
    return 0;
}

Lambda 捕获方式总结:

[&]      捕获所有局部变量(按引用)
[=]      捕获所有局部变量(按值/复制)
[&x]     只捕获 x(按引用)
[x]      只捕获 x(按值)
[this]   捕获当前对象(按引用)
[*this]  捕获当前对象(按值,复制)
[]       不捕获任何东西

6.4 Lambda 用于初始化

Lambda 可以把复杂的初始化逻辑变成一个表达式,避免"先声明再赋值"的坏习惯:

#include <iostream>
#include <vector>
using namespace std;
enum class InitMode { zero, fill, range };
int main() {
    InitMode m = InitMode::fill;
    int n = 5;
    int fill_val = 7;
    // 不好的写法:变量先声明,后赋值,中间可能被误用
    // vector<int> v;
    // switch (m) { case zero: v = vector<int>(n); break; ... }
    // 好的写法:用 Lambda 立即初始化
    // [&] 捕获外部变量,() 立即调用
    vector<int> v = [&]() -> vector<int> {
        switch (m) {
            case InitMode::zero:  return vector<int>(n, 0);
            case InitMode::fill:  return vector<int>(n, fill_val);
            case InitMode::range: return {1, 2, 3, 4, 5};
            default:              return {};
        }
    }();  // 注意最后的 () 表示立即调用
    for (auto x : v)
        cout << x << " ";  // 7 7 7 7 7
    cout << "\n";
    return 0;
}

6.5 finally() — 用 Lambda 实现 RAII 清理

对于没有析构函数的 C 风格资源,可以用 finally() 保证清理:

#include <iostream>
#include <cstdlib>  // malloc, free
using namespace std;
// Final_action:在析构时执行传入的动作
template<class F>
struct Final_action {
    explicit Final_action(F f) : act(f) {}
    ~Final_action() { act(); }  // 离开作用域时自动调用
    F act;
};
// 工厂函数:[[nodiscard]] 警告你不能忽略返回值
template<class F>
[[nodiscard]] auto finally(F f) {
    return Final_action{f};
}
void old_style(int n) {
    void* p = malloc(n * sizeof(int));  // C 风格分配
    // 绑定清理动作:离开函数时自动 free(p)
    auto act = finally([&]{ free(p); });
    // ... 做一些操作,即使抛异常也会 free(p) ...
    cout << "使用内存中...\n";
}   // act 析构 → 调用 free(p)
int main() {
    old_style(10);
    return 0;
}

7. 模板机制工具箱

7.1 变量模板(Variable Templates)

当某个值依赖于类型 T T T 时,可以用变量模板:

#include <iostream>
#include <numbers>  // C++20 数学常数
using namespace std;
// 对不同类型的精度,提供对应精度的 pi
template<class T>
constexpr T pi = T(3.14159265358979323846);
// 也可以定义依赖类型的常量
template<typename T>
constexpr bool is_integer_v = false;
template<>
constexpr bool is_integer_v<int> = true;
template<>
constexpr bool is_integer_v<long> = true;
int main() {
    float  pf = pi<float>;   // 单精度 pi
    double pd = pi<double>;  // 双精度 pi
    cout << pf << "\n";  // 3.14159
    cout << pd << "\n";  // 3.14159265358979
    cout << is_integer_v<int>    << "\n";  // 1 (true)
    cout << is_integer_v<double> << "\n";  // 0 (false)
    return 0;
}

7.2 别名(Aliases)

using 为类型或模板创建简洁的别名:

#include <iostream>
#include <vector>
#include <map>
#include <string>
using namespace std;
// 为模板实例起别名
template<typename T>
class MyMap {
    // 简化版,仅作演示
};
// 绑定第一个参数,创建新模板
template<typename Value>
using StringMap = MyMap<Value>;
// 现在 StringMap<int> 等价于 MyMap<int>
// 提取容器元素类型的别名
template<typename C>
using ValueType = typename C::value_type;
int main() {
    // size_t 就是 using size_t = unsigned long(平台相关)
    size_t n = 42;
    // ValueType 帮助我们以统一方式获取容器的元素类型
    ValueType<vector<int>>    x = 1;     // x 是 int
    ValueType<vector<double>> y = 3.14;  // y 是 double
    cout << x << ", " << y << "\n";
    return 0;
}

7.3 编译期 if(if constexpr

根据类型属性,在编译期选择不同的代码路径,无运行时开销:

#include <iostream>
#include <type_traits>  // is_trivially_copyable_v
#include <string>
using namespace std;
// 对"普通旧数据"(POD)用快速复制,否则用安全复制
template<typename T>
void copy_value(T& target, const T& source) {
    if constexpr (is_trivially_copyable_v<T>) {
        // 编译期选择:T 是简单可复制类型(如 int、double)
        // 可以直接内存复制
        target = source;
        cout << "快速复制(平凡可复制类型)\n";
    } else {
        // 编译期选择:T 有复杂的复制逻辑(如 string)
        target = source;  // 调用复制赋值运算符
        cout << "安全复制(非平凡类型)\n";
    }
}
int main() {
    int a = 0, b = 42;
    copy_value(a, b);      // 输出: 快速复制
    string s1, s2 = "hello";
    copy_value(s1, s2);    // 输出: 安全复制
    return 0;
}

注意if constexpr 不是文本替换,必须遵守语法和类型规则。两个分支都要语法正确,但只有选中的分支会被编译。

8. Concepts 深入

8.1 Concept 的本质

Concept 是一个编译期谓词(返回 true/false 的布尔表达式),用来描述类型必须满足的条件。
concept  C = P ( T ) ∈ { true , false } \text{concept } C = P(T) \in \{\text{true}, \text{false}\} concept C=P(T){true,false}

8.2 定义自己的 Concept

#include <iostream>
#include <concepts>  // C++20
using namespace std;
// 定义"可相等比较"的 Concept
// requires 表达式:检查这些操作是否合法
template<typename T>
concept EqualityComparable =
    requires (T a, T b) {
        { a == b } -> convertible_to<bool>;  // == 必须返回可转 bool 的值
        { a != b } -> convertible_to<bool>;  // != 同理
    };
// 定义"数值"Concept:支持基本算术运算
template<typename T, typename U = T>
concept Number =
    requires(T x, U y) {
        x + y;   // 支持加法
        x - y;   // 支持减法
        x * y;   // 支持乘法
        x / y;   // 支持除法
        x += y;
        x -= y;
        x *= y;
        x /= y;
        x = x;   // 可赋值
        x = 0;   // 可用 0 初始化
    };
// 使用 Concept 约束函数
template<EqualityComparable T>
bool are_equal(T a, T b) {
    return a == b;
}
// 用 Concept 约束:只接受数值类型
template<Number T>
T double_it(T x) {
    return x + x;
}
int main() {
    cout << are_equal(1, 1)     << "\n";  // 1 (true)
    cout << are_equal(1, 2)     << "\n";  // 0 (false)
    cout << double_it(5)        << "\n";  // 10
    cout << double_it(3.14)     << "\n";  // 6.28
    // are_equal("a", "b") 可能报错,取决于是否支持 ==
    return 0;
}

8.3 基于 Concept 的重载

不同约束的模板可以重载,编译器选择约束最严格(最精确)的版本:

#include <iostream>
#include <iterator>
#include <vector>
#include <list>
using namespace std;
// 慢版本:只要求前向迭代器(只能 ++)
template<forward_iterator Iter>
void advance_iter(Iter& p, int n) {
    while (n--) ++p;
    cout << "[慢速] 逐步前进 " << n + 1 << " 步\n";
}
// 快版本:要求随机访问迭代器(支持 +=)
template<random_access_iterator Iter>
void advance_iter(Iter& p, int n) {
    p += n;
    cout << "[快速] 直接跳跃\n";
}
int main() {
    vector<int> v = {1, 2, 3, 4, 5};
    list<int>   l = {1, 2, 3, 4, 5};
    auto vi = v.begin();
    auto li = l.begin();
    // vector 迭代器是随机访问迭代器 → 选快版本
    advance_iter(vi, 3);
    // list 迭代器只是前向迭代器 → 选慢版本
    advance_iter(li, 3);
    return 0;
}

选择规则(越严格越优先):

约束强度:random_access_iterator > bidirectional_iterator > forward_iterator > input_iterator
匹配时选最强约束的版本

8.4 Concept 与 auto

Concept 可以和 auto 结合,限制自动推导的类型范围:

#include <iostream>
#include <concepts>
using namespace std;
// auto 前加 Concept:限制参数类型
// 这实际上是函数模板的简写形式
auto twice(arithmetic auto x) {
    return x + x;   // 只接受算术类型(int, double 等)
}
auto thrice(auto x) {
    return x + x + x;  // 接受任何支持 + 的类型
}
int main() {
    cout << twice(7)       << "\n";  // 14
    cout << twice(3.14)    << "\n";  // 6.28
    // twice("hello") 会报错:string 不是 arithmetic
    cout << thrice(4)      << "\n";  // 12
    // string 支持 +,所以可以用
    string s = "ab";
    cout << thrice(s)      << "\n";  // "ababab"
    return 0;
}

9. 泛型编程思想

9.1 “提升”(Lifting)过程

泛型编程的核心方法是从具体到抽象,步骤如下:

第一步:写具体版本
double sum_doubles(const vector<int>& v) {
    double res = 0;
    for (auto x : v) res += x;
    return res;
}
第二步:问"哪些地方不必要地具体了?"
  - 为什么只能是 int?
  - 为什么只能是 vector?
  - 为什么累加器是 double?
  - 为什么初始值是 0?
第三步:把具体类型替换为模板参数
#include <iostream>
#include <vector>
#include <list>
#include <iterator>  // begin, end
using namespace std;
// 经过"提升"后的通用 accumulate
// Iter: 前向迭代器
// Val:  支持 += 的数值类型
template<typename Iter, typename Val>
Val accumulate(Iter first, Iter last, Val res) {
    for (auto p = first; p != last; ++p)
        res += *p;  // *p 解引用迭代器,获取元素值
    return res;
}
int main() {
    vector<int>    vi = {1, 2, 3, 4, 5};
    list<double>   ld = {1.1, 2.2, 3.3};
    // 对 vector<int> 求和,累加器类型 double
    double s1 = accumulate(begin(vi), end(vi), 0.0);
    // 对 list<double> 求和
    double s2 = accumulate(begin(ld), end(ld), 0.0);
    cout << s1 << "\n";  // 15
    cout << s2 << "\n";  // 6.6
    return 0;
}

9.2 迭代器是"概念黏合剂"

数据结构                  算法
(vector, list, map)  ←→  (sort, find, accumulate)
         通过迭代器接口连接
每种容器提供迭代器 → 算法只依赖迭代器 → 算法对所有容器通用
迭代器层次(概念层次)
input_iterator
    |
forward_iterator       (可多次遍历)
    |
bidirectional_iterator (可向前向后)
    |
random_access_iterator (可跳跃,支持 p+n)

10. 可变参数模板

10.1 接受任意数量、任意类型的参数

#include <iostream>
#include <concepts>
using namespace std;
// Printable: 可以用 << 输出的类型
template<typename T>
concept Printable = requires(T t) { cout << t; };
// 递归基础:零个参数时什么都不做
void print() {}
// 递归情形:至少一个参数
// T head:      第一个参数
// Tail... tail: 剩余参数(数量不定)
template<Printable T, Printable... Tail>
void print(T head, Tail... tail) {
    cout << head << ' ';  // 打印第一个
    print(tail...);       // 递归打印剩余(... 展开参数包)
}
int main() {
    print("hello", 42, 3.14, 'A');
    // 输出: hello 42 3.14 A
    cout << "\n";
    return 0;
}

递归展开过程:

print("hello", 42, 3.14, 'A')
  打印 "hello"
  print(42, 3.14, 'A')
    打印 42
    print(3.14, 'A')
      打印 3.14
      print('A')
        打印 'A'
        print()   ← 基础情形,什么都不做

if constexpr 可以省掉基础情形:

template<Printable T, Printable... Tail>
void print2(T head, Tail... tail) {
    cout << head << ' ';
    if constexpr (sizeof...(tail) > 0)  // 编译期判断是否还有参数
        print2(tail...);
}

10.2 折叠表达式(Fold Expressions)

C++17 提供更简洁的写法,不用手写递归:

#include <iostream>
#include <concepts>
using namespace std;
// 右折叠:(v[0] + (v[1] + (v[2] + ... (v[n] + 0))))
// 公式:$(v_0 + (v_1 + (v_2 + \cdots + 0)))$
template<typename... T>
auto sum_right(T... v) {
    return (v + ... + 0);  // 右折叠,初始值 0
}
// 左折叠:(((0 + v[0]) + v[1]) + ... + v[n])
// 公式:$((((0 + v_0) + v_1) + v_2) + \cdots)$
template<typename... T>
auto sum_left(T... v) {
    return (0 + ... + v);  // 左折叠,初始值 0
}
// 折叠输出(非数值用法)
template<typename... T>
void print_all(T&&... args) {
    (cout << ... << args) << '\n';
    // 展开为: (((cout << args[0]) << args[1]) << args[2]) << '\n'
}
int main() {
    cout << sum_right(1, 2, 3, 4, 5) << "\n";  // 15
    cout << sum_left(1, 2, 3, 4, 5)  << "\n";  // 15(结合律下相同)
    print_all("Hello", ' ', "World", ' ', 2024);
    // 输出: Hello World 2024
    return 0;
}

折叠表达式语法:

写法 含义 展开形式
(pack op ... op init) 右折叠 v 0 ⊕ ( v 1 ⊕ ( ⋯ ⊕ ( v n ⊕ init ) ) ) v_0 \oplus (v_1 \oplus (\cdots \oplus (v_n \oplus \text{init}))) v0(v1((vninit)))
(init op ... op pack) 左折叠 ( ⋯ ( ( i n i t ⊕ v 0 ) ⊕ v 1 ) ⊕ ⋯   ) ⊕ v n (\cdots((init \oplus v_0) \oplus v_1) \oplus \cdots) \oplus v_n (((initv0)v1))vn
(pack op ...) 右折叠(无初始值) v 0 ⊕ ( v 1 ⊕ ⋯   ) v_0 \oplus (v_1 \oplus \cdots) v0(v1)
(... op pack) 左折叠(无初始值) ( ⋯ ⊕ v n ) (\cdots \oplus v_n) (vn)

10.3 完美转发(Perfect Forwarding)

#include <iostream>
#include <utility>  // std::forward
using namespace std;
// 模拟一个传输类
struct TcpTransport {
    string host;
    int port;
    TcpTransport(string h, int p) : host(h), port(p) {
        cout << "TCP 连接到 " << h << ":" << p << "\n";
    }
};
// InputChannel 不知道 Transport 需要什么参数
// 用可变参数模板 + forward 透明地传递参数
template<typename Transport>
class InputChannel {
public:
    template<typename... Args>
    InputChannel(Args&&... args)
        : transport_(forward<Args>(args)...)
        // forward 保持参数的"左值/右值"属性不变
    {}
    Transport transport_;
};
int main() {
    // InputChannel 把 "localhost", 8080 直接转给 TcpTransport 构造函数
    InputChannel<TcpTransport> ch("localhost", 8080);
    return 0;
}

11. 模板编译模型

11.1 检查时机

用 Concept 约束的参数
    检查时机:调用点(早,报错清晰)
未约束的参数
    检查时机:实例化时(晚,报错难看)

具体例子:

template<typename T>
concept EqComparable = requires(T a, T b) { { a == b } -> convertible_to<bool>; };
// Concept 约束:调用时立即检查 ==
template<EqComparable T>
bool check_eq(T a, T b) {
    return a < b;  // 注意:只保证了 ==,没保证 <
}
// 对 int:int 支持 ==(通过概念检查),也支持 <(实例化时通过)→ OK
// 对 complex:complex 支持 ==(通过),但不支持 <(实例化时失败)→ 报错

11.2 头文件规则

由于模板需要在实例化时生成代码,编译器需要看到完整的定义:

// good_template.h
template<typename T>
class MyClass {
    // 定义必须在头文件里!
    void method() { ... }  // 不能只声明,要写完整
};
// main.cpp
#include "good_template.h"  // 编译器看到完整定义,才能实例化
MyClass<int> obj;           // 实例化时编译器知道怎么生成代码

使用 C++20 模块(modules)可以改变这一规则,让模板也可以放在 .cpp 文件里。

综合演示

下面是一个把本章主要概念融合在一起的完整例子:

#include <iostream>
#include <vector>
#include <list>
#include <string>
#include <concepts>
#include <algorithm>
using namespace std;
// ========== Concept 定义 ==========
// 可打印:支持 << 运算符
template<typename T>
concept Printable = requires(T t) { cout << t; };
// 可比较:支持 <
template<typename T>
concept Comparable = requires(T a, T b) {
    { a < b } -> convertible_to<bool>;
};
// ========== 函数模板 ==========
// 找出序列中的最大值(要求元素可比较)
template<typename Iter>
    requires Comparable<typename iterator_traits<Iter>::value_type>
auto find_max(Iter first, Iter last) {
    auto max_it = first;
    for (auto it = first; it != last; ++it)
        if (*max_it < *it)
            max_it = it;
    return *max_it;
}
// ========== 函数对象 ==========
// 乘以某个因子的变换器
template<typename T>
struct Multiplier {
    T factor;
    Multiplier(T f) : factor(f) {}
    T operator()(T x) const { return x * factor; }
};
// ========== 泛型打印函数 ==========
template<Printable T, Printable... Rest>
void println(T first, Rest... rest) {
    cout << first;
    if constexpr (sizeof...(rest) > 0) {
        cout << ", ";
        println(rest...);
    } else {
        cout << "\n";
    }
}
int main() {
    vector<int> vi = {3, 1, 4, 1, 5, 9, 2, 6};
    list<double> ld = {1.5, 2.7, 0.3, 4.1};
    // 找最大值(泛型,对 vector 和 list 都适用)
    cout << "vector 最大值: " << find_max(vi.begin(), vi.end()) << "\n";
    cout << "list 最大值:   " << find_max(ld.begin(), ld.end()) << "\n";
    // 用函数对象变换每个元素
    Multiplier<int> times3{3};
    for (auto& x : vi)
        x = times3(x);
    // 用 Lambda 打印
    cout << "乘以 3 后: ";
    for (auto x : vi)
        cout << x << " ";
    cout << "\n";
    // 可变参数打印
    println("混合类型", 42, 3.14, 'Z');
    return 0;
}

总结速查


特性 语法 用途
类模板 template<typename T> class C {...} 参数化数据结构
函数模板 template<typename T> T f(T x) 参数化算法
值参数 template<typename T, int N> 编译期常量嵌入类型
Concept template<MyConcept T> 约束模板参数
CTAD vector v = {1,2,3} 自动推导模板参数
if constexpr if constexpr(cond) 编译期分支
变量模板 template<class T> constexpr T pi 依赖类型的常量
别名模板 template<typename T> using X = ... 简化复杂类型名
可变参数 template<typename... T> 任意数量参数
折叠表达式 (v + ... + 0) 简化可变参数运算
完美转发 forward<Args>(args)... 透明传递参数

关键原则

  1. 模板是编译期机制,零运行时开销
  2. 用 Concept 约束参数,让错误更早出现、更易理解
  3. 先写具体版本,再"提升"为模板(不要过早抽象)
  4. 函数对象和 Lambda 是传递"操作"的首选方式
  5. 模板定义必须在头文件(或用模块),编译器实例化时需要完整定义

C++ 标准库、字符串与正则表达式 — 从零理解

对应《A Tour of C++》第9、10章。目标:让没有基础的读者也能完全看懂。

目录

  1. 为什么需要标准库?
  2. 标准库的组成
  3. 标准库的组织方式
  4. 字符串 string
  5. 字符串视图 string_view
  6. 正则表达式
  7. 总结速查

1. 为什么需要标准库?

没有任何重要程序是只用"裸语言"写成的。就像盖房子不会从烧砖开始,写程序也不会从零实现所有基础功能。
标准库的价值:

裸 C++ 语言
    +
标准库(string, vector, map, thread, regex ...)
    =
可以快速完成几乎任何任务的工具箱

标准库规范占 ISO C++ 标准的三分之二以上,经过大量专家设计、实现和维护,优先使用它而不是自己造轮子。

2. 标准库的组成

标准库提供的设施可以分为以下几大类:

标准库组成
├── 运行时支持        内存分配、异常、运行时类型信息
├── C 标准库          printf、malloc 等(略作改进)
├── 字符串            string、string_view、正则表达式
├── I/O 流            cin、cout、文件流、格式化输出
├── 文件系统          path、目录操作
├── 容器与算法        vector、map、sort、find(STL)
├── 范围(Ranges)    views、generators、pipes
├── 数值计算          数学函数、complex、随机数
├── 并发支持          thread、mutex、future
├── 工具类            pair、tuple、variant、optional
├── 智能指针          unique_ptr、shared_ptr
└── 时间与日历        time_point、duration、time_zone

库的入选标准:

  • 对几乎所有 C++ 程序员都有帮助(新手和专家)
  • 通用形式不会带来明显开销
  • 简单用法应易于学习

3. 标准库的组织方式

3.1 命名空间 std

所有标准库设施都放在 std 命名空间中,通过头文件或模块引入:

#include <string>
#include <vector>
#include <iostream>
using namespace std;  // 省略 std:: 前缀(示例代码中常用)

在正式项目中,建议写 std::string 而不是全局 using namespace std,避免命名污染。

3.2 子命名空间

标准库还提供一些子命名空间,需要显式引入才能使用:

#include <complex>
#include <string>
#include <chrono>
using namespace std;
int main() {
    // 使用复数字面量后缀 i,需引入子命名空间
    using namespace std::literals::complex_literals;
    auto z = 2 + 3i;   // z 是 complex<double>
    // 使用字符串字面量后缀 s
    using namespace std::literals::string_literals;
    auto s = "hello"s; // s 是 std::string,不是 const char*
    return 0;
}

3.3 ranges 命名空间

标准库中很多算法有两个版本:

#include <algorithm>
#include <vector>
#include <ranges>
using namespace std;
int main() {
    vector<int> v = {3, 1, 4, 1, 5};
    // 传统版本:传入一对迭代器
    sort(v.begin(), v.end());
    // ranges 版本:直接传容器,需要显式限定
    ranges::sort(v);
    // 或者引入后直接用
    using ranges::sort;
    sort(v);   // OK
    return 0;
}

为什么不能直接 using namespace ranges 因为和 std 里的同名函数会产生二义性(ambiguous)。

3.4 常用头文件速查


头文件 提供的主要内容
<algorithm> sort(), find(), copy()
<vector> vector
<map> map, multimap
<string> string, basic_string
<string_view> string_view
<regex> regex, smatch, regex_search()
<iostream> cin, cout, istream, ostream
<fstream> ifstream, ofstream
<memory> unique_ptr, shared_ptr
<thread> thread
<chrono> duration, time_point
<concepts> copyable, invocable 等概念
<ranges> sized_range, take(), split()
<variant> variant
<stdexcept> out_of_range, runtime_error

4. 字符串 string

4.1 为什么用 string 而不用 char 数组?

C 语言风格:char name[50] = "Alice"; — 固定大小,容易溢出,操作繁琐。
C++ 风格:string name = "Alice"; — 自动管理内存,操作简单安全。

4.2 基本操作

#include <iostream>
#include <string>
using namespace std;
// 拼接两个字符串,生成邮箱地址
// 演示 string 的 + 运算符(拼接)
string compose(const string& name, const string& domain) {
    return name + "@" + domain;  // + 运算符:字符串拼接
}
int main() {
    // 基本创建
    string s1 = "Hello";
    string s2 {"World"};
    string s3 (5, 'A');       // "AAAAA":用 5 个 'A' 填充
    // 拼接
    string s4 = s1 + " " + s2; // "Hello World"
    // += 追加(推荐:比 s = s + x 更高效)
    s1 += " C++";               // s1 变为 "Hello C++"
    cout << s4 << "\n";         // Hello World
    cout << s1 << "\n";         // Hello C++
    // 生成邮箱地址
    auto addr = compose("dmr", "bell-labs.com");
    cout << addr << "\n";       // dmr@bell-labs.com
    return 0;
}

4.3 子串与替换

#include <iostream>
#include <string>
#include <cctype>   // toupper
using namespace std;
int main() {
    string name = "Niels Stroustrup";
    // substr(起始位置, 长度):提取子串(返回新 string,原串不变)
    // 索引从 0 开始:N(0)i(1)e(2)l(3)s(4) (5)S(6)t(7)r(8)o(9)u(10)...
    string s = name.substr(6, 10);  // 从第6位取10个字符 → "Stroustrup"
    cout << s << "\n";              // Stroustrup
    // replace(起始位置, 替换长度, 新内容):替换子串
    // 把位置0开始的5个字符("Niels")替换成 "nicholas"
    name.replace(0, 5, "nicholas");
    cout << name << "\n";           // nicholas Stroustrup
    // 修改单个字符:把首字母改成大写
    name[0] = toupper(name[0]);     // 'n' → 'N'
    cout << name << "\n";           // Nicholas Stroustrup
    return 0;
}

substr 索引示意:

N  i  e  l  s     S  t  r  o  u  s  t  r  u  p
0  1  2  3  4  5  6  7  8  9  10 11 12 13 14 15
substr(6, 10) → 从位置6开始,取10个字符 → "Stroustrup"

4.4 比较、搜索与 C 字符串互转

#include <iostream>
#include <string>
#include <cstdio>   // printf
using namespace std;
int main() {
    string a = "apple";
    string b = "banana";
    // == != 比较内容(不是地址!)
    cout << (a == b) << "\n";   // 0 (false)
    cout << (a != b) << "\n";   // 1 (true)
    // 字典序比较(按字母顺序)
    cout << (a < b)  << "\n";   // 1 (true,'a' < 'b')
    // find:查找子串,返回起始位置;找不到返回 string::npos
    string sentence = "The quick brown fox";
    size_t pos = sentence.find("quick");
    if (pos != string::npos)
        cout << "找到 'quick' 在位置: " << pos << "\n"; // 4
    // c_str():转成 C 风格字符串(const char*),只读
    // 需要调用 printf 或某些 C 接口时使用
    printf("C 风格输出: %s\n", a.c_str());
    // 字面量后缀 s:明确创建 std::string(而非 const char*)
    using namespace std::literals::string_literals;
    auto cat = "Cat"s;   // cat 是 std::string
    auto dog = "Dog";    // dog 是 const char*(C 风格)
    cout << cat.size() << "\n"; // 3(string 有 size() 方法)
    return 0;
}

4.5 string 的内存实现(短字符串优化)

标准库的 string 使用短字符串优化(Short String Optimization,SSO)

短字符串(约 ≤ 14 个字符):
┌────────────────────────────────┐
│  string 对象本身               │
│  ┌──────────────────────────┐  │
│  │ 字符直接存在对象内部      │  │
│  │ "Hello"                  │  │
│  └──────────────────────────┘  │
│  size=5  capacity=14           │
└────────────────────────────────┘
不需要堆分配,访问速度快!
长字符串(> 14 个字符):
┌────────────────────────────────┐
│  string 对象本身               │
│  ptr ──────────────────────────┼──→ 堆上的字符数组
│  size=20  capacity=32          │    "Annemarie Stroustrup"
└────────────────────────────────┘

这是为什么 string 对短文本非常高效的原因。

4.6 支持多字节字符(basic_string 模板)

string 本质上是 basic_string<char> 的别名:

// 标准库内部定义:
template<typename Char>
class basic_string { /* ... */ };
using string  = basic_string<char>;    // 普通字符串
using wstring = basic_string<wchar_t>; // 宽字符串
using u8string  = basic_string<char8_t>;  // UTF-8(C++20)
using u16string = basic_string<char16_t>; // UTF-16
using u32string = basic_string<char32_t>; // UTF-32

5. 字符串视图 string_view

5.1 为什么需要 string_view?

考虑一个函数,需要读取字符串的一部分:

传统做法的问题:
- 传 const string&:如果调用者有的是 const char*,需要先构造 string(额外开销)
- 传 const char*:如果调用者有的是 string,需要调用 c_str()(丢失长度信息)
- 传子串:需要调用 substr() 复制出新 string(额外内存分配)
理想做法:
一个轻量级"视图",能统一接受 string、const char*、子串,且不复制数据

string_view 就是这个答案。它本质上是一个 (指针 + 长度) 的对:
string_view = ( const char* ptr ,  size_t len ) \text{string\_view} = (\text{const char*}\ \text{ptr},\ \text{size\_t}\ \text{len}) string_view=(const char* ptr, size_t len)

string_view 不拥有字符,只是"观察"它们
就像一扇窗户,可以看到窗外的景色,但不拥有景色
string s = "Hello World";
string_view sv = s;       // sv 观察 s 的内容,不复制
sv:  ptr ──→ H e l l o   W o r l d
             ↑___________________________↑
             len = 11

5.2 string_view 的使用

#include <iostream>
#include <string>
#include <string_view>
using namespace std;
// 接受 string_view:可以接受 string、const char*、子串,无需复制
string cat(string_view sv1, string_view sv2) {
    string res {sv1};  // 从 sv1 初始化(这里才发生复制)
    return res += sv2; // 追加 sv2 并返回
}
// 遍历 string_view(只读)
void print_lower(string_view sv) {
    for (char ch : sv)
        cout << (char)tolower(ch);
    cout << "\n";
}
int main() {
    string king = "Harold";
    // 以下调用都合法!string_view 统一了接口
    auto s1 = cat(king, "William");      // string + const char*
    auto s2 = cat(king, king);           // string + string
    auto s3 = cat("Edward", "Stephen");  // const char* + const char*
    // sv 后缀:在编译期计算长度(比运行时计算更快)
    using namespace std::literals::string_view_literals;
    auto s4 = cat("Canute"sv, king);     // string_view + string
    // 传入子串:{指针, 长度},不需要 substr() 复制
    // &king[0] 是指向 'H' 的指针,2 是长度 → 取 "Ha"
    auto s5 = cat({&king[0], 2}, "Henry"sv);  // "Ha" + "Henry" = "HaHenry"
    cout << s1 << "\n";  // HaroldWilliam
    cout << s5 << "\n";  // HaHenry
    // 遍历(只读)
    print_lower("HELLO WORLD");  // hello world
    return 0;
}

5.3 string_view 的限制与陷阱

#include <iostream>
#include <string>
#include <string_view>
using namespace std;
// 危险!返回指向局部变量的 string_view
// 函数返回后,局部 string s 被销毁,string_view 指向无效内存
string_view bad() {
    string s = "Once upon a time";
    return {&s[5], 4};  // 错误:s 离开作用域后被销毁!
    // 返回的 string_view 是悬空视图(dangling view)
}
int main() {
    // string_view 是只读的,不能修改字符
    string s = "Hello";
    string_view sv = s;
    // sv[0] = 'h';  // 编译错误:string_view 是只读的
    // 正确用法:string_view 必须指向有效的、生命周期更长的字符串
    string data = "Hello World";
    string_view view = data;      // OK:data 在 view 使用期间有效
    cout << view.substr(6) << "\n"; // "World"(string_view 的 substr 不复制!)
    return 0;
}

string_view 使用守则:

可以用 string_view 的场景:
  函数参数(只读读取字符串内容)
  在已知字符串生命周期更长时观察子串
不应该用 string_view 的场景:
  返回值(容易返回悬空视图)
  存储为成员变量(生命周期难以管理)
  需要修改字符的场景(改用 span<char>)

6. 正则表达式

6.1 什么是正则表达式?

正则表达式(Regular Expression,regex)是描述文本模式的一种语言。
举例:你想在一堆文本里找所有美国邮政编码(格式:两个字母 + 五位数字,可选 - + 四位数字):

TX 77845          ← 匹配
DC 20500-0001     ← 匹配
Hello World       ← 不匹配
AB12345           ← 匹配(字母和数字之间可以没有空格)

对应的正则表达式模式:\w{2}\s*\d{5}(-\d{4})?
拆解理解:

\w{2}      两个"单词字符"(字母或数字)
\s*        零个或多个空白字符(空格、制表符等)
\d{5}      恰好五个数字
(-\d{4})?  可选部分:一个连字符后跟四个数字

6.2 正则表达式特殊字符


字符 含义
. 任意单个字符(通配符)
* 前面的模式零次或多次(贪婪)
+ 前面的模式一次或多次
? 前面的模式零次或一次(可选)
\ 转义:下一个字符有特殊含义
^ 行首 / 取反(在 [] 内)
$ 行尾
[...] 字符类:匹配括号内的任意一个字符
(...) 分组(捕获子模式)
(?:...) 分组(不捕获)
| 或:匹配左边或右边
{n,m} 重复 n 到 m 次


缩写 等价 含义
\d [[:digit:]] 十进制数字 0-9
\s [[:space:]] 空白字符(空格、制表符等)
\w [_[:alnum:]] 单词字符(字母、数字、下划线)
\D [^[:digit:]] 非数字
\S [^[:space:]] 非空白
\W [^_[:alnum:]] 非单词字符

6.3 量词(重复次数)


量词 含义
{n} 恰好 n 次
{n,} 至少 n 次
{n,m} n 到 m 次
* 等价于 {0,}
+ 等价于 {1,}
? 等价于 {0,1}
*? +? ?? 非贪婪(懒惰)模式:尽可能少匹配

贪婪 vs 非贪婪:

输入文本:<b>bold</b> and <i>italic</i>

模式 <.*>  (贪婪):匹配整个 <b>bold</b> and <i>italic</i>(从第一个 < 到最后一个 >)
模式 <.*?> (非贪婪):分别匹配 <b>、</b>、<i>、</i>(每次尽量少匹配)

6.4 搜索(regex_search)

#include <iostream>
#include <string>
#include <regex>
#include <fstream>
using namespace std;
int main() {
    // R"(...)" 是原始字符串字面量,反斜杠不需要转义
    // 等价的普通字符串写法:\\w{2}\\s*\\d{5}(-\\d{4})?
    regex pat {R"(\w{2}\s*\d{5}(-\d{4})?)"};
    // smatch:存储匹配结果的容器(s = string)
    // matches[0]:完整匹配
    // matches[1]:第一个捕获组 (-\d{4})? 的匹配结果
    smatch matches;
    vector<string> lines = {
        "Office: TX 77845",
        "White House: DC 20500-0001",
        "No postal code here",
        "AB12345 and XY 99999-1234"
    };
    int lineno = 0;
    for (const auto& line : lines) {
        ++lineno;
        // regex_search:在 line 中搜索 pat,找到第一个匹配
        // 返回 true/false,并把结果存入 matches
        if (regex_search(line, matches, pat)) {
            cout << "行 " << lineno << ": " << matches[0] << "\n";
            // matches[1].matched:检查可选子模式是否确实匹配到了
            if (matches.size() > 1 && matches[1].matched)
                cout << "  └─ 附加码: " << matches[1] << "\n";
        }
    }
    return 0;
}
// 输出:
// 行 1: TX 77845
// 行 2: DC 20500-0001
//   └─ 附加码: -0001
// 行 4: AB12345
// 行 4: XY 99999-1234
//   └─ 附加码: -1234

6.5 匹配(regex_match)

regex_match 要求整个字符串完全匹配模式(而不是查找其中的一段):

#include <iostream>
#include <string>
#include <regex>
using namespace std;
// 判断字符串是否是合法的 C++ 标识符
// 规则:下划线或字母开头,后跟零个或多个下划线/字母/数字
bool is_identifier(const string& s) {
    // [_[:alpha:]]  :第一个字符:下划线或字母
    // \w*           :后续:零个或多个单词字符(字母/数字/下划线)
    regex pat {R"([_[:alpha:]]\w*)"};
    return regex_match(s, pat);  // 整个 s 都要匹配
}
// 判断是否是合法的整数字符串
bool is_integer(const string& s) {
    // -?    :可选的负号
    // \d+   :一个或多个数字
    regex pat {R"(-?\d+)"};
    return regex_match(s, pat);
}
// 判断是否是合法的电子邮件(简化版)
bool is_email(const string& s) {
    // [[:alnum:]_.+]+  :用户名(字母数字和 _.+)
    // @                :at 符号
    // [[:alnum:]_.-]+  :域名
    // \.               :点(需要转义)
    // [[:alpha:]]{2,4} :顶级域名(2-4个字母)
    regex pat {R"([[:alnum:]_.+]+@[[:alnum:]_.-]+\.[[:alpha:]]{2,4})"};
    return regex_match(s, pat);
}
int main() {
    // 标识符测试
    cout << "=== 标识符测试 ===\n";
    for (const string& s : {"hello", "_var", "123abc", "my_var_2", ""}) {
        cout << "\"" << s << "\": "
             << (is_identifier(s) ? "合法" : "不合法") << "\n";
    }
    // 整数测试
    cout << "\n=== 整数测试 ===\n";
    for (const string& s : {"42", "-7", "3.14", "0", "abc"}) {
        cout << "\"" << s << "\": "
             << (is_integer(s) ? "是整数" : "不是整数") << "\n";
    }
    // 邮件测试
    cout << "\n=== 邮件测试 ===\n";
    for (const string& s : {"user@example.com", "bad@", "a@b.c", "test.user+tag@domain.org"}) {
        cout << "\"" << s << "\": "
             << (is_email(s) ? "合法" : "不合法") << "\n";
    }
    return 0;
}

6.6 替换(regex_replace)

#include <iostream>
#include <string>
#include <regex>
using namespace std;
int main() {
    string text = "The date is 2024-01-15 and also 2023-12-25.";
    // 把 YYYY-MM-DD 格式改为 DD/MM/YYYY 格式
    // 捕获组:(\d{4}) 年,(\d{2}) 月,(\d{2}) 日
    regex date_pat {R"((\d{4})-(\d{2})-(\d{2}))"};
    // $2/$3/$1 表示:第2组/第3组/第1组(重新排列)
    string result = regex_replace(text, date_pat, "$3/$2/$1");
    cout << "原文: " << text   << "\n";
    cout << "替换: " << result << "\n";
    // 原文: The date is 2024-01-15 and also 2023-12-25.
    // 替换: The date is 15/01/2024 and also 25/12/2023.
    // 把所有空白字符序列替换成单个空格(规范化空格)
    string messy = "Hello    World  \t  C++";
    regex spaces {R"(\s+)"};
    string clean = regex_replace(messy, spaces, " ");
    cout << "规范化: " << clean << "\n";  // Hello World C++
    return 0;
}

6.7 迭代器(regex_iterator)

当需要找出文本中所有的匹配项时,使用 sregex_iterator

#include <iostream>
#include <string>
#include <regex>
using namespace std;
int main() {
    string input = "aa as; asd ++e^asdf asdfg";
    // 模式:空白后跟单词(捕获组 (\w+) 是我们想要的单词)
    regex pat {R"(\s+(\w+))"};
    // sregex_iterator:遍历 input 中所有匹配 pat 的位置
    // 构造:起始迭代器、结束迭代器、模式
    // 终止条件:和默认构造的 sregex_iterator{} 比较
    for (sregex_iterator p(input.begin(), input.end(), pat);
         p != sregex_iterator{};
         ++p) {
        // (*p)[0]:完整匹配(空白+单词)
        // (*p)[1]:第一个捕获组(只有单词部分)
        cout << (*p)[1] << "\n";
    }
    // 输出:as、asd、asdfg(注意:第一个单词 aa 前没有空格,被跳过)
    cout << "---\n";
    // 改用更简单的模式,捕获所有单词
    regex word_pat {R"((\w+))"};
    for (sregex_iterator p(input.begin(), input.end(), word_pat);
         p != sregex_iterator{};
         ++p) {
        cout << (*p)[1] << "\n";
    }
    // 输出:aa、as、asd、e、asdf、asdfg(全部单词)
    return 0;
}

6.8 正则表达式模式示例集

以下是一些常用模式,帮助建立直觉:

模式 含义 匹配示例
Ax* A 后跟零个或多个 x A, Ax, Axxx
Ax+ A 后跟一个或多个 x Ax, Axxx(不匹配 A
\d-?\d 数字、可选连字符、数字 1-2, 12
\w{2}-\d{4,5} 两个单词字符、连字符、4-5位数字 Ab-1234, XX-54321
(\d*:)?(\d+) 可选"数字:"前缀 + 数字 12:3, 1:23, 123
(bs|BS) bs 或 BS bs, BS
[aeiouy] 英文元音 a, e, u
[^aeiouy] 非英文元音 x, k, 3
[_[:alpha:]]\w* C++ 标识符 hello, _var, my2
\d{4}-\d{2}-\d{2} ISO 日期格式 2024-01-15

6.9 XML/HTML 标签解析示例

#include <iostream>
#include <string>
#include <regex>
using namespace std;
int main() {
    string html = "Visit <a href='url'>our site</a> for <b>more info</b>.";
    // <(.*?)>  第1组:标签名(非贪婪,只匹配到第一个 >)
    // (.*?)    第2组:标签内容(非贪婪)
    // </\1>    结束标签:\1 引用第1组的内容(反向引用)
    regex tag_pat {R"(<(.*?)>(.*?)</\1>)"};
    smatch m;
    string s = html;
    // 循环查找所有标签
    while (regex_search(s, m, tag_pat)) {
        cout << "标签: <" << m[1] << ">  内容: " << m[2] << "\n";
        s = m.suffix(); // 从匹配结束处继续搜索
    }
    // 输出:
    // 标签: <a href='url'>  内容: our site
    // 标签: <b>  内容: more info
    return 0;
}

7. 总结速查

string vs string_view vs const char*

字符串相关类型对比:
const char*
  用途:C 接口、字面量
  拥有数据:否
  可修改:否(const)
  空终止符:是
  长度获取:O(n),需要 strlen
std::string
  用途:拥有并管理字符串
  拥有数据:是(堆分配或 SSO)
  可修改:是
  空终止符:内部有
  长度获取:O(1)
std::string_view
  用途:只读观察,统一接口
  拥有数据:否(只是指针+长度)
  可修改:否
  空终止符:不保证
  长度获取:O(1)

正则表达式三种操作

regex_match(s, m, pat)
  → 整个字符串 s 必须完全匹配 pat
  → 用于验证格式(是否是合法邮箱?是否是整数?)
regex_search(s, m, pat)
  → 在 s 中查找第一个匹配 pat 的位置
  → 用于提取(在文章中找日期、邮政编码)
regex_replace(s, pat, fmt)
  → 把 s 中所有匹配 pat 的部分替换成 fmt
  → 用于变换(日期格式转换、敏感词替换)
sregex_iterator
  → 遍历 s 中所有匹配 pat 的结果
  → 用于批量提取(找出所有单词、所有链接)

需求 推荐类型/函数
拥有并操作字符串 std::string
只读接收任意字符串 std::string_view
验证字符串格式 regex_match
在文本中搜索模式 regex_search
批量查找所有匹配 sregex_iterator
搜索并替换 regex_replace
需要 C 接口 string::c_str()
国际化字符 basic_string<CharType>

关键原则

  1. 优先使用 string 而不是 char[],优先使用标准库而不是自己实现
  2. 函数参数只读字符串时,用 string_view 而不是 const string&(更通用,开销更小)
  3. string_view 不拥有数据,不能用于返回值或长期存储
  4. 正则表达式用原始字符串字面量 R"(...)" 避免双重转义
  5. 默认使用贪婪匹配,需要精确匹配时加 ? 改为非贪婪
  6. 正则表达式编译(构造 regex 对象)有开销,循环中使用时应只构造一次
Logo

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

更多推荐