A Tour of C++ Third Edition学习:C++ 基础:从零开始理解
本文对应《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 表示:
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(仅整数)
比较运算符结果为 true 或 false:
相等: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 作用域与生命周期
变量"活"在哪个范围内?这就是作用域的概念。
#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;
}
控制流图:
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 等)太底层,不够表达现实世界的概念。自定义类型让我们可以创建更有意义的抽象。
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(外部不能直接访问)
访问控制示意:
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;
}
交通灯状态转换:
枚举的类型安全:
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 章知识图谱
核心建议速查
| 建议 | 原因 |
|---|---|
用 {} 初始化 |
防止窄化转换,更安全 |
用 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++ 提供两种方式实现分离编译:
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;
}
传参方式选择规则:
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++ 提供了一套机制来优雅地处理这些情况,而不是让程序直接崩溃。
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;
}
不变量的概念图:
4.4 错误处理策略选择
什么时候用异常,什么时候用错误码,什么时候直接终止?
完整的对比示例:
#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 章知识图谱
核心建议速查
| 建议 | 原因 |
|---|---|
| 优先使用模块,而非头文件 | 编译更快,没有传递性污染 |
| 用 const 引用传大型参数 | 避免拷贝,同时防止误修改 |
| 不要返回局部变量的引用或指针 | 局部变量函数返回后消亡,引用变悬空 |
| 直接返回大对象,不用返回指针 | 编译器自动优化(移动语义/RVO) |
| 用异常处理"不应该发生"的错误 | 调用者不会忘记检查;可以跨层传播 |
| 用错误码处理"预期中"的失败 | 打开文件失败很正常,用 bool 返回即可 |
| 构造函数中用 throw 建立不变量 | 保证对象被创建后一定处于有效状态 |
| 析构函数标记 noexcept | 析构中抛异常是大忌 |
| 不要滥用 noexcept | 标错了反而更危险 |
| 能在编译期检查的就用 static_assert | 越早发现错误越好 |
C++ 类与基本操作:从零开始理解
本文对应《A Tour of C++》第 5、6 章,用最通俗的语言解释每个概念。
第五章:类
5.1 三种核心类
C++ 的类分三大类型,就像建房子有三种结构方式:
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"(是一种)关系:
完整的形状层次结构示例:
#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 六个关键特殊成员函数
当一个类管理资源(如内存),这六个函数需要配套定义:
零法则(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++ 资源管理的核心哲学:
标准库中 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} a⇔b=⎩
⎨
⎧负数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 章知识图谱
核心建议速查
| 建议 | 原因 |
|---|---|
| 定义析构函数时,同时定义拷贝和移动 | 有析构说明管理资源,默认拷贝很可能是错的 |
| 单参数构造函数加 explicit | 防止意外的隐式类型转换 |
| 用成员默认初始化值代替繁琐的构造函数 | 更简洁,防止遗漏初始化 |
| 用 unique_ptr 代替裸 new/delete | 自动释放,不会泄漏 |
| 虚基类必须有虚析构函数 | 通过基类指针 delete 时才能调用正确的析构函数 |
| 用 override 标记重写的虚函数 | 编译器能检查拼写错误和签名不匹配 |
| 用移动语义返回大对象,不用返回指针 | 更安全,效率同样高( O ( 1 ) O(1) O(1) 移动) |
| 为对称运算符定义自由函数 | 两个操作数被平等对待 |
| 定义 <=> 后还需单独定义 == | 自定义的 <=> 不会自动生成 == |
| 避免裸 new 和裸 delete | 用资源句柄(RAII)代替,代码更安全、更易读 |
C++ 模板与泛型编程 — 从零理解
本文对应《A Tour of C++》第7、8章,目标是让没有模板基础的读者也能完全看懂。
目录
- 什么是模板?
- 参数化类型(类模板)
- 约束模板参数(Concepts 初探)
- 值作为模板参数
- 模板参数自动推导(CTAD)
- 参数化操作
- 模板机制工具箱
- Concepts 深入
- 泛型编程思想
- 可变参数模板
- 模板编译模型
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⊕(⋯⊕(vn⊕init))) |
(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 (⋯((init⊕v0)⊕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)... |
透明传递参数 |
关键原则
- 模板是编译期机制,零运行时开销
- 用 Concept 约束参数,让错误更早出现、更易理解
- 先写具体版本,再"提升"为模板(不要过早抽象)
- 函数对象和 Lambda 是传递"操作"的首选方式
- 模板定义必须在头文件(或用模块),编译器实例化时需要完整定义
C++ 标准库、字符串与正则表达式 — 从零理解
对应《A Tour of C++》第9、10章。目标:让没有基础的读者也能完全看懂。
目录
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> |
关键原则
- 优先使用
string而不是char[],优先使用标准库而不是自己实现 - 函数参数只读字符串时,用
string_view而不是const string&(更通用,开销更小) string_view不拥有数据,不能用于返回值或长期存储- 正则表达式用原始字符串字面量
R"(...)"避免双重转义 - 默认使用贪婪匹配,需要精确匹配时加
?改为非贪婪 - 正则表达式编译(构造
regex对象)有开销,循环中使用时应只构造一次
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)