13 以对象管理资源
- 资源就是一旦使用,将来必须还给系统,C++最常使用的资源是动态分配内存,除此之外常见的资源还包括文件描述器、互斥锁、图形界面的字型和笔刷、数据库连接以及网络sockets
- 基于对象管理资源建立在C++对构造函数、析构函数、copying函数的基础上
// 假设A是root class,B是派生类
B* createB(); // 条款7中提到的工厂函数,调用后需要删除
void f()
{
B* pB = createB();
...
delete pB;
}
- 这样看起来没问题,但假如“...”中有一个return或者抛出一个异常,或delete位于一个由于某个continue过早退出的循环中,这样delete就会被略过,泄漏的不只是内含对象B的内存,还包括对象B保存的任何资源
- 为了确保createB返回的对象总被释放,借用C++的析构函数自动调用机制,将资源放进对象中,当控制流离开f,该对象的析构函数会自动释放资源
- 许多资源被动态分配于heap内而后被用于单一区块或函数内,它们应在控制流离开那个区块或函数时被释放,标准库提供的智能指针就是为此而设计的,其析构函数自动对其所指对象调用delete
#include <memory>
void f()
{
std::shared_ptr<B> pB(createB());
...
}
- 以对象管理资源的关键想法是,使用RAII对象,获得资源后立即放进管理对象,管理对象运用析构函数确保资源被释放
- shared_ptr在析构函数内做delete而不是delete[],所以不能给动态分配的array用智能指针,并没有特别针对C++动态分配数组而设计的类似shared_ptr的东西,因为vector和string几乎总是可以取代动态分配的数组,如果希望针对数组设计智能指针,Boost中有boost::shared_array
14 在资源管理类中小心copying行为
- 并非所有资源都是heap-based,对这些资源智能指针往往不适合resource handlers,因此偶尔需要建立自己的资源管理类,例如使用C API函数处理Mutex类型的互斥器对象,有lock和unlock两函数可用
void lock(Mutex* pm); // 锁定pm所指的互斥器
void unlock(Mutex* pm);
- 为确保不会忘记将一个被锁住的Mutex解锁,建立一个基本结构遵循RAII守则的class来管理
class Lock{
public:
explicit Lock(Mutex* mu) : mutexPtr(mu)
{
lock(mutexPtr);
}
~Lock()
{
unlock(mutexPtr);
}
private:
Mutex* mutexPtr;
};
// 客户对Lock的用法符合RAII
Mutex m; // 定义互斥器
...
{ // 建立一个区块用来定义critical section
Lock m1(&m); // 锁定互斥器
... //执行critical section 内的操作
} // 在区块末尾,自动解除互斥器的锁
// 这很好,但如果RAII对象被复制是不合理的
Lock m2(&m); // 锁定m
Lock m3(m2); // 将m2复制到m3上
- 如果RAII对象复制,意味着同一资源被多个管理类管理,资源管理应该有独占性,因此有以下选择
- 禁止复制,参考条款6(阻止拷贝的方式)
- 对底层资源使用引用计数,如将Mutex*改为shared_ptr<Mutex>,不过shared_ptr是在引用计数为0时删除所指物,对于Mutex我们想做的动作是锁定而不是删除,可以自己定义删除器,删除器在引用计数为0时被调用
- 复制底部资源,进行深拷贝。若某类中含有堆或其他资源,该类对象复制时,资源重新分配的过程就是深拷贝,反之则为浅拷贝,浅拷贝前后两个指针指向同一对象,浅拷贝资释放时会因为资源归属不清导致出错
- 转移底层资源的拥有权,即利用unique_ptr
15 在资源管理类中提供对原始资源的访问
16 成对使用new和delete时要采取相同形式
- 使用一条new表达式时,实际执行了三步操作
- new表达式调用标准库中的operator new(或operator new[])函数,该函数分配一块足够大的、原始的、未命名的内存空间以便存储特定类型的对象(或对象的数组)
- 编译器针对此内存调用一个或多个构造函数来构造对象,并为其传入初始值
- 对象被分配空间并构造完成,返回一个指向该对象的指针
- delete则执行两步操作
- 先调用一个或多个析构函数
- 编译器调用标准库中的operator delete(或operator[])函数释放内存
- delete时要采用和new相同的形式,否则会造成未定义错误
std::string str1 = new std::string;
std::string str2 = new std::string[100];
delete str1;
delete [] str2;
- 注意typedef时可能会隐藏创建的是数组这一信息
typedef std::string Arr[4];
std::string* p = new Arr; // Arr是一个数组
delete [] p; // 必须匹配数组对应的delete形式
17 以独立语句将newed对象置入智能指针
int f();
void g(shared_ptr<A> p, int f);
g(shared_ptr<A>(new A), f());
- 编译器产生g的结果之前必须先计算传递的实参,于是在调用g之前,编译器必须先创建代码完成三件事
- 调用f
- 执行new A表达式
- 调用shared_ptr构造函数
- 完成顺序与编译器的实现有关,但new一定执行于shared_ptr构造之前。对f的调用可以排在第一第二或第三,如果排在第二,最终顺序是
- 执行new A表达式
- 调用f
- 调用shared_ptr构造函数
- 如果调用f发生异常,则new A返回的指针还未置于shared_ptr就丢失了,于是发生了资源泄露
- 解决方法很简单,分离语句,以独立语句将new出来的对象存储在智能指针中
shared_ptr<A> p(new A);
g(p, f);
网友评论