本页目录

C++ I · 抽象、RAII 与值/移动语义

对标A Tour of C++(Stroustrup)/ Effective Modern C++(Meyers)| 前置:csapp-01/04(内存、指针)、pl-02(三条内存路线) C++ 是一门矛盾又强大的语言:贴近硬件到能写操作系统和游戏引擎,又提供高级抽象且承诺"零成本"。它是理解"如何在不牺牲性能的前提下抽象"的最佳教材,也是通往 Rust(rust 线)的桥——Rust 的所有权正是把 C++ 的 RAII 和移动语义变成编译器强制。这一页讲 C++ 的世界观:值语义、RAII(资源管理的核心创见)、移动语义、以及"零成本抽象"到底什么意思。

1. C++ 的设计哲学:零成本抽象

C++ 的核心承诺——零成本抽象(zero-overhead abstraction):"你不用的东西不付代价,你用的东西手写也不会更快"。你能用高级抽象(类、模板、STL),编译后和手写底层 C 一样快(🔗 rust 也继承这个承诺)。这和 Python(py 线,抽象有解释开销)截然相反——C++ 的抽象在编译期展开、运行期不留痕迹

代价是复杂:C++ 把内存、生命周期、拷贝的控制权全交给你——威力大、脚也容易打中。理解它的几个核心机制(值语义、RAII、移动)就能驾驭大半。

2. 值语义:对象就是值,不是引用

值语义(b 是 a 的拷贝) vs 引用/指针,栈上对象。

图 cpp-01.3值语义(b 是 a 的拷贝) vs 引用/指针,栈上对象。

和 Python(py-01 变量是引用)、Java(对象是引用)不同,C++ 默认是值语义——变量就是对象本身,不是指向对象的引用:

后果:C++ 让你精确控制"拷贝还是共享"——这是性能的来源(无隐藏的堆分配、无 GC),也是复杂度的来源(要管拷贝成本、要懂什么时候发生拷贝)。"默认值语义 + 显式引用"是 C++ 内存模型的基石,理解它才能读懂 C++ 代码在做什么。

3. RAII:C++ 最伟大的创见

RAII:构造获取资源、离作用域析构自动释放(含异常路径),对照 C 手动易漏。

图 cpp-01.2RAII:构造获取资源、离作用域析构自动释放(含异常路径),对照 C 手动易漏。

RAII(Resource Acquisition Is Initialization,资源获取即初始化)——把资源的生命周期绑定到对象的生命周期:构造函数获取资源(内存、文件、锁、连接),析构函数释放它;对象离开作用域时析构函数自动调用(🔗 csapp-03 栈展开)。

{
    std::lock_guard<std::mutex> lock(mtx);  // 构造:加锁
    // ... 临界区 ...
}  // 离开作用域,析构:自动解锁——哪怕中途抛异常也保证解锁

这解决了 C 的核心痛点(csapp-04)——手动 malloc/free、手动加锁解锁、手动关文件,极易忘记或在异常路径漏掉。RAII 让"作用域结束"自动触发清理,无论正常返回还是异常。智能指针是 RAII 管理内存的落地:

深远影响:RAII 是 C++ 对编程的重大贡献——"资源生命周期 = 对象生命周期 = 作用域"这个思想,Rust 的所有权(rust-01 的 drop)、Python 的 with(py-01)都是它的后裔。理解 RAII,你就理解了"确定性资源管理"这条区别于 GC 的路线(pl-02 的三条路线里,C++/Rust 走的正是这条)。

4. 移动语义:避免昂贵的拷贝

移动(偷内部缓冲, O(1)) vs 拷贝(复制全部, O(n)),通往 Rust 所有权。

图 cpp-01.1移动(偷内部缓冲, O(1)) vs 拷贝(复制全部, O(n)),通往 Rust 所有权。

值语义(第 2 节)意味着拷贝——但拷贝一个持有大堆内存的对象(如装了百万元素的 vector)很贵。移动语义(C++11)解决之:当源对象是临时的/即将销毁的,不拷贝它的资源,而是"偷"过来(把内部指针转移、把源置空)——\(O(1)\) 而非 \(O(n)\)

std::vector<int> make() { std::vector<int> v(1000000); return v; }
auto data = make();   // 不拷贝一百万元素,移动:偷走 v 的内部缓冲区

这正是通往 Rust 的桥(🔗 rust-01):C++ 的移动是约定std::move 后你不该再用源对象,但编译器不拦你——用了是 bug);Rust 把移动变成默认 + 编译器强制(move 后编译器禁止你再用源对象)。C++ 教你移动语义为什么重要,Rust 把它做成不会出错的样子——两页对照读,所有权的价值就立体了。

5. C++ 的适用与代价

6. 练习与要点

例 1(RAII 对比手动) 写一段"打开文件—处理—中途可能抛异常"的代码,对比手动 fopen/fclose(异常路径漏关)vs RAII 的 std::ifstream(自动关)——理解 RAII 为什么让资源管理"异常安全",C 的痛在这里被治好。

例 2(拷贝 vs 移动) 给一个持有大 vector 的类,观察"返回临时对象"触发移动(快)而"拷贝具名对象"触发拷贝(慢)——用计数器或计时验证。移动语义省的那次 \(O(n)\) 拷贝亲眼可见

例 3(对照 Rust) 把本页的移动语义和 rust-01 的所有权并读:C++ std::move 后用源对象是"合法但错",Rust move 后用源对象是"编译错误"——理解"约定 vs 强制"的区别,这就是 Rust 存在的理由。\(\blacksquare\)


下一页:C++ II——现代 C++、STL、模板与并发:标准库的容器与算法、模板泛型编程、以及 C++ 的并发工具与陷阱。