3 资源管理
什么是资源——一旦使用,就必须还给系统的东西。C++程序员最长使用的资源就是动态分配内存(因为如果你分配内存却不曾归还,会导致内存泄露),但显然内存只是你必须管理的众多资源之一。其他常见资源:文件描述器、互斥锁、图形界面中的字型和笔刷、数据库连接 以及 网络sockets。
条款13:以对象管理资源
1.常常在函数开头new一个新对象,在函数结尾delete之。但是由于过早return、循环中的continue或goto、抛出异常等动作,可能指向不到函数结尾的delete
2.将资源放进对象内,倚赖C++的析构函数自动调用机制确保资源被释放
3.“以对象管理资源”的两个关键想法:获得资源后立刻放进管理对象内,即“资源取得时机便是初始化时机”(RAII);管理对象运用析构函数确保资源被释放。
4.auto_ptr 是个类指针(pointer-like)对象,也就是所谓的——智能指针,其析构函数自动对其所指对象调用 delete。其有个不寻常的性质:若通过 copying 函数(copy构造函数 或 copy assignment 操作符 )复制它们,它们会变成null,而复制所得的指针将取得 资源的唯一拥有权。
std::auto_ptr<Investment> pInv1(createInvestment() ); // pInv1 指向 函数返回的对象
std::auto_ptr<Investment> pInv2(pInv1); // 现在pInv2指向那个对象,pInv1 为 null
pInv1 = pInv2; // 现在 pInv1指向那个对象,pInv2为 null
5.RCSP(reference-counting smart pointer,引用计数型智慧指针)也是个智能指针,如TR1的 tr1::shared_ptr, 它可以持续追踪共有多少对象指向某笔资源,并在无人指向它时自动删除该资源,这种行为类似于垃圾回收( garbage collection),不同的是 RCSPs无法打破环状引用。
6.auto_prt 和 tr1::shared_ptr 两者都在析构函数内做 delete 而非 delete[] 动作,所以动态分配而得到的 array上使用 auto_ptr 或 tr1::shared_ptr 是个错误的选择,遗憾的是这样也可以通过编译。
7.小结:
<1>为防止资源泄漏,请使用RAII 对象,它们在构造函数中获得资源并在析构函数中释放资源。
<2>两个常被使用的RAII类 分别是 auto_ptr 和 tr1::shared_ptr。后者通常是较佳的选择,因为它 copy 行为比较直观。若选择 auto_ptr,复制动作会使它(被复制物)指向null
条款14:在资源管理中小心copying行为
1.建立自己的资源管理类
处理类型为 Mutex 的 互斥器对象时常用的以下两个函数
void lock(Mutex* pm); // 锁定互斥器
void unlock(Mutex* pm); // 解锁互斥器
为确保我们不会忘记将一个被锁住的 Mutex 解锁,可能会希望建立一个类来管理它,让类在析构期间释放它:
class Lock {
public:
explicit Lock(Mutex* pm) : mutexPtr(pm)
{ lock(mutexPtr); }
~Lock() { unlock(mutexPtr); }
private:
Mutex *mutexPtr;
};
用法:
Mutex m; // 定义所需要的互斥器
...
{ // 建立一个区块来定义临界区
Lock m1(&m); // 锁定互斥器
...
} // 在区块最末尾,自动解除互斥器锁定
2.禁止RAII对象复制。将copying操作声明为 private
class Lock : private Uncopyable {
public:
... // 同前一样
};
3.对底层资源祭出“引用计数法”(reference-count)。将成员变量定义为 tr1::shared_ptr 类型
class Lock {
public:
explicit Lock(Mutex* pm) : mutexPtr(pm,unlock)
{
lock(mutexPtr.get() );
}
private:
std::tr1::shared_ptr<Mutex> mutexPtr; // 使用 shared_ptr 替换 raw pointer
};
而且本例中的 Lock class 不再声明析构函数。因为没有必要。
4.复制底部资源。复制资源管理对象,应该同时也复制其所包含的资源,即复制对象时,进行“深度拷贝”
5.转移底部资源的拥有权。 使用auto_ptr使RAII 对象被复制时资源的拥有权会从 被复制物 转移到 目标物
6.Copying函数有可能被编译器自己自动创建出来,因此除非编译器所生成版本做了你想做的事,否则你需要自己编写它们
7.小结:
<1>复制RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定RAII对象的copying行为
<2>普遍而常见的RAII 类 copying 行为是:抑制 copying 、 施行引用计数法(reference counting)。不过其他行为也都可能被实现
条款15:在资源管理类中提供对原始资源的访问
1.有时候不得不绕过资源管理对象而直接访问原始资源
2.显式转换:tr1::shared_ptr 还是 auto_ptr 都会提供一个 get成员函数,可以用来获取内部的原始指针。
3.隐式转换:(几乎)所有的智能指针都重载了指针取值操作符( operator-> 和 operator* ),它们允许隐式转换至指针底部原始指针
class Investment { //根类
public:
bool isTaxFree() const;
...
};
Investment* createInvestment(); //factory函数
std::tr1::shared_ptr< Investment > pi1(createInvestment()); // 令tr1::shared_ptr 管理一笔资源
bool taxable1 = !(pi1->isTaxFree() ); // 通过 operator-> 访问资源
...
std::auto_ptr<Investment> pi2( createInvestment() ); // 令auto_ptr 管理一笔资源
bool taxable2 = !((*pi2).isTaxFree()); // 通过 operator* 访问资源
4.显示转换函数安全但是相对麻烦;隐式转换函数使用自然但有可能引发“非故意的类型转换”。是否该提供一个显式转换函数( 例如get成员函数)将RAII 类转换为底部资源,或是提供隐式转换函数,主要取决于RAII 类被设计执行的特定工作,以及它被使用的情况。最佳设计应该是坚持条款18的忠告: 让接口容易被正确的使用,不易被误用。
5.小结:
<1>APIs往往要求访问原始资源,所以每一个RAII类应该提供一个“取得其所管理资源”的办法。
<2>对原始资源的访问可能经由显式转换或隐式转换。一般而言显式转换比较安全,但隐式转换对客户比较方便。
条款16:成对使用 new 和 delete 时要采用相同的形式
1.使用 new 时内存被分配出来 (通过名为 operator new 的函数)同时会有一个(或更多)构造函数被调用;使用 delete 时会有一个(或更多)析构函数被调用,然后此内存才会被释放(通过 operator delete 的函数)
2.使用delete时须考虑即将被删除的指针,指向单一对象还是对象数组,因为两者内存布局不一样。
3.如果调用 new 时,用了[ ] ,你必须在对应调用delete时使用 [ ] ; 如果调用 new 时,没用[ ] ,你也不应该在用 delete时用 [ ]
std::string* stringPtr1 = new std::string;
std::string* stringPtr2 = new std::string[100];
...
delete stringPtr1; // 删除一个对象
delete [ ] stringPtr2; // 删除对象数组
4.最好不要对数组形式进行typedef动作,因为delete的使用不够清晰。可以改用C++标准程序库含有 string、vector这样的template
typedef std::string AddressLines[4]; // 每个人的地址有四行,每行类型都是 string
std::string* pal = new AddressLines; // 它返回一个string*对象,就像new string[4]
...
delete pal; // 错误,行为未有定义!
delete [ ] pal; // 这样才对
5.小结: 如果你在 new 表达式中使用[],必须在相应的delete表达式中也是用[]。如果你在 new 表达式中,不使用[],一定不要在相应的delete表达式中使用[]
条款17:以独立语句将 newed 对象置入智能指针
1.不推荐如下格式:
processWidget( std::tr1::shared_ptr<Widget>( new Widget ) , priority() );
该行代码要执行三件事:①调用 priority;②执行 ” new Widget ” ;③调用 tr1::shared_ptr 构造函数。而这三步的执行顺序有一定弹性,假设执行顺序为②->①->③,万一中间①调用priority出现错误,导致异常,则之前②中返回的指针将会丢失,因为它尚未被置入 tr1::shared_ptr,从而引发资源泄漏 。
2.推荐使用如下分离语句:
std::tr1::shared_ptr<Widget> pw( new Widget ) //在单独语句内以智能指针存储newed所得对象
processWidget(pw,priority() );//这个调用动作绝对不至于造成资源泄漏
因为,C++ 编译器无法跨越语句进行重新排列,只能在语句内重新排列
3.小结:以独立语句将 newed 对象存储于(置入)智能指针内。如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄漏