【C++】《Effective C++》第三章
第三章 资源管理
条款13:以对象管理资源
当申请一块动态内存时,可能会发生内存泄漏。
class Investment {};
void f() {
Investment* pInv = createInvestment();
// ...
delete pInv; // 释放pInv所指对象
}
-
对于以上程序,发生内存泄漏的情况:
- 忘记
delete
。 - 有
delete
,但是在delete
之前就跳出了控制流。比如提前return
、发生异常等。
- 忘记
-
解决这种情况比较好的方法是使用对象管理资源,它包括两个关键想法:
- 获得资源后立刻放进管理对象内,即资源取得时机便是初始化时机(
RAII
)。 - 管理对象运用析构函数确保资源被释放。
- 获得资源后立刻放进管理对象内,即资源取得时机便是初始化时机(
#include <memory>
// 使用智能指针auto_ptr,C++11中使用unique_ptr,auto_ptr是其老版本。
void f() {
std::auto_ptr<Investment> pInv(createInvestment());
// std::unique_ptr<Investment> pInv(createInvestment());
// ...
}
// 使用智能指针shared_ptr,推荐使用
void f() {
std::shared_ptr<Investment> pInv = std::make_shared<Investment>(createInvestment());
}
请记住
- 为防止资源泄漏,请使用
RAII
对象,它们在构造函数中获得资源并在析构函数中释放资源。 - 常用的
RAII classes
有unique_ptr
,auto_ptr
,shared_ptr
,推荐使用shared_ptr
。
条款14:在资源管理类中小心copying
行为
并非所有资源都是动态内存,除此之外还有锁等资源,也应该通过"对象管理资源"来确保获取资源后能够正确的释放,根据资源的类型和不同的需求,可能需要定义不同的copy
行为。
- 禁止复制:比如说锁资源,管理锁资源的对象复制通常并不合理。因此应该禁止这类对象的复制。
- 对底层资源使用"引用计数法":如果希望保有资源,直到它的最后一个使用者(某对象)被销毁。这种情况下复制
RAII
对象时,应该将资源的"被引用数"递增。 - 复制底部资源:这种情况下,希望在复制
RAII
对象时,同时复制其关联的底层资源。展现出一种"深拷贝"行为。 - 转移底部资源的拥有:如果希望任一时刻一个资源只由一个
RAII
对象管理,那么在复制RAII
对象时,应该实现拥有权的"转移",原RAII
对象拥有的资源设为null
。
请记住
- 复制
RAII
对象必须一并复制它所管理的资源,所以资源的copying
行为决定RAII
对象的copying
行为。 - 普遍而常见的
RAII class copying
行为是:抑制copying
、施行引用计数法(reference counting
)。不过其他行为也都可能被实现。
条款15:在资源管理类中提供对原始资源的访问
取得RAII
对象所管理资源的办法可以通过显式转换或隐式转换:
- 显式转换(比较安全,但不易用):如
shared_ptr
的get()
方法。 - 隐式转换(比较易用,但不安全):如
shared_ptr
的operator*
和operator->
。
请记住
- APIs往往要求访问原始资源。所以每一个RAII class应该提供一个"取得其所管理之资源"的办法。
- 对原始资源的访问可能经由显式转换和隐式转换。一般而言显式转换比较安全,但隐式转换对客户比较方便。
条款16:成对使用new
和delete
时要采取相同形式
当使用new
和delete
时,发生两件事:
- new:
- 内存被分配出来(通过
operator new
函数)。 - 针对此内存会有一个(或更多)构造函数被调用。
- 内存被分配出来(通过
- delete:
- 针对此内存会有一个(或更多)析构函数被调用。
- 内存被释放(通过
operator delete
函数)。
std::string* stringPtr1 = new std::string;
std::string* stringPtr2 = new std::string[100];
// ...
delete stringPtr1;; // 删除一个对象
delete [] stringPtr2; // 删除一个由对象组成的数组
请记住
- 如果你在
new
表达式中使用[]
,必须在相应的delete
表达式中也使用[]
。如果你在new
表达式中不使用[]
,一定不要在相应的delete
表达式中使用[]
。
条款17:以独立语句将newed
对象置入智能指针
// 揭示处理程序的优先权
int priority();
void processWidget(std::shared_ptr<Widget> pw, int priority);
// 调用
processWidget(new Widget, priority());
// 显然上面的调用不对
processWidget(std::shared_ptr<Widget>(new Widget), priority());
虽然如此,上面的调用还是可能泄露资源。因为在调用processWidget
之前,编译器必须创建代码,做以下三件事:调用priority;执行new Widget;调用std::shared_ptr构造函数。但是在C++
中编译器对于这三件事的调用顺序不定,所以当priority
的调用导致异常,可能执行new Widget
返回的指针将会遗失。即在资源被创建和资源被转换为资源管理对象两个时间点之间可能发生异常干扰。
为了解决上面的问题,可以使用分离语句。
// 分成两步调用
std::shared_ptr<Widget> pw(new Widget);
processWidget(pw, priority());
请记住
- 以独立语句将
newed
对象存储于(置入)智能指针内。如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄露。