简单来说,不要手动去 delete 指针或释放资源,而是把资源放进一个对象里,依靠对象的析构函数(Destructor)自动释放资源。
以下是该条款的详细深度解析,结合了原书内容与现代 C++(C++11 及以后)的最佳实践。
1. 为什么我们需要“以对象管理资源”?
传统做法的缺陷
假设我们有一个工厂函数,用于创建一个 Investment(投资)对象:
Investment* createInvestment(); // 返回指针,指向动态分配的对象
传统的调用方式如下:
void f() {Investment* pInv = createInvestment(); // 1. 获取资源// ... 执行一些操作 ...delete pInv; // 2. 显式释放资源
}
潜在的风险: 如果在“执行一些操作”的过程中发生了以下情况,delete pInv 永远不会被执行,从而导致内存泄漏:
- 提前返回(Early return):代码逻辑中有一个
return语句。 - 抛出异常(Exception):中间的代码抛出了异常,栈展开(Stack unwinding)跳过了 delete。
解决方案:RAII
C++ 保证:当一个对象离开作用域(Scope)时,其析构函数会被自动调用。
因此,我们将资源(pInv)包装在一个局部对象中。当函数 f() 结束时,局部对象自动销毁,其析构函数负责调用 delete。
2. 核心原则 (RAII 的两大铁律)
Scott Meyers 在书中总结了两个关键点:
- 获得资源后立刻放进管理对象 (Resource Acquisition Is Initialization)
- 当我们调用
createInvestment()拿到指针的那一刻,应该立刻把它传递给智能指针的构造函数。 - 不要让裸指针在外面“裸奔”。
- 当我们调用
- 管理对象运用析构函数确保资源被释放
- 无论控制流是如何离开作用域的(正常结束、break、return、throw),析构函数都会被调用,从而保证资源不泄漏。
3. 书中介绍的工具与现代演变
《Effective C++》成书较早,书中主要介绍了 std::auto_ptr 和 tr1::shared_ptr。在现代 C++ (C++11+) 中,情况发生了变化。
A. std::auto_ptr (已废弃/移除)
- 书中的描述:它是早期的智能指针,当它被复制时(通过 copy 构造或 copy assignment),它会转移所有权,原来的指针变成
null。 - 现代观点:绝对不要再使用
auto_ptr。它在 C++11 中被标记为废弃,在 C++17 中已被彻底移除。它的“复制即转移”语义非常容易导致 Bug(例如在 STL 容器中使用会导致未定义行为)。 - 替代者:
std::unique_ptr。
B. std::unique_ptr (现代首选)
这是 auto_ptr 的完美继任者。
- 语义:专属所有权(Exclusive Ownership)。同一时间只能有一个
unique_ptr指向该对象。 - 特点:禁止复制(Copy),只允许移动(Move)。这完美契合了“独占”的逻辑,且性能几乎等同于裸指针。
代码示例:
#include <memory>void f() {// 使用 unique_ptr 管理资源std::unique_ptr<Investment> pInv(createInvestment()); // ... 做任何操作 ...// ... 哪怕抛出异常 ...} // 函数结束,pInv 离开作用域,自动调用 delete
C. std::shared_ptr (引用计数)
书中提到了 RCSP (Reference-counting smart pointer)。
- 语义:共享所有权。多个指针可以指向同一个对象。
- 原理:内部维护一个引用计数器。
- 复制时,计数 +1。
- 析构时,计数 -1。
- 当计数变为 0 时,真正的
delete被调用。
- 适用场景:当多个对象需要共享底层资源,且无法确定谁最后使用完该资源时。
4. 这里的“资源”不仅仅是内存
虽然本条款主要用内存(指针)举例,但 RAII 适用于所有必须释放的资源:
- 文件句柄 (File descriptors)
- 互斥锁 (Mutex locks)
- 数据库连接 (Database connections)
- 网络套接字 (Network sockets)
例子:管理互斥锁 不要手动 lock() 和 unlock():
void strictCode() {std::mutex m;std::lock_guard<std::mutex> lock(m); // 构造时 lock// ... 操作共享数据 ...
} // 作用域结束,lock 析构,自动 unlock
5. 特别警示:关于数组
条款 13 特别提到一点:智能指针默认的删除器是 delete,而不是 delete[]。
如果你这样做(在老式 C++ 中):
std::auto_ptr<std::string> aps(new std::string[10]); // 错误!
当 aps 析构时,它会执行 delete 而不是 delete[],导致未定义行为(通常是内存泄漏或崩溃)。
现代 C++ 的建议:
-
优先使用
std::vector或std::string:它们内部已经封装了数组管理的逻辑,几乎不需要手动new数组。 -
如果非要用智能指针管理数组,C++11 后的
unique_ptr支持数组特化:std::unique_ptr<int[]> up(new int[10]); // 正确,会自动调用 delete[]
总结
条款 13 的核心教义是:为了防止资源泄漏,请使用 RAII 对象。
对于现代 C++ 开发者 (你) 的行动指南:
- 默认使用
std::unique_ptr:如果资源是独占的。 - 需要共享时使用
std::shared_ptr:如果资源需要在多个拥有者之间共享。 - 永远不要使用
std::auto_ptr。 - 优先使用标准容器 (
vector,string) 代替动态分配的数组 (new T[])。