条款13:以对象管理资源
所谓资源就是,一旦用了它,将来就必须还给系统。
对于那些分配在堆区的资源,单纯依赖“函数总会执行其 delete 语句”是不靠谱的,如提前 return, delete 前发生异常等,控制流就绝不会触及 delete 语句。一个绝妙的应对措施是“以对象管理资源”,确保资源在析构函数被释放。相对应的,C++提供了形如 auto_ptr 、tr1::shared_ptr 这样的智能指针(类指针对象),便于我们使用。
需要注意的是,auto_ptr 使用 copying 函数会使得以前的 auto_ptr 对象变成 NULL,而复制所得到的指针将取得资源的唯一拥有权。
tr1::shared_ptr 使用了 reference-count 机制,支持 copying 行为,当资源 reference-count 为 0 时,自动调用析构函数。不过,这两种方法在其析构函数内做的是 delete 而不是 delete[]。即对分配的数组资源管理支持还不够。此时,看看 boost 吧,里面的 boost::scoped_array 和 boost::shared_array classes ,它们提供我们想要的行为。
简单的使用以上两种智能指针:
#include <iostream>
#include <tr1/memory>
using namespace std;
class base
{
public:
base(){cout << "base()" << endl;}
~base(){cout << "~base()" << endl;}
};
int main()
{
auto_ptr<base> pb1(new base);
auto_ptr<base> pb2(pb1);
cout << pb1.get() << endl;
cout << pb2.get() << endl;
tr1::shared_ptr<base> pb3(new base);
tr1::shared_ptr<base> pb4(pb3);
cout << pb3.get() << endl;
cout << pb4.get() << endl;
return 0;
}
输出如下:
base()
0
0x961a008
base()
0x961a018
0x961a018
~base()
~base()
请记住:
1、为防止资源泄露,请使用 RAII 对象,它们在构造函数中获得资源并在析构函数中释放资源。
2、两个常被使用的 RAII classes 分别是:tr1::shared_ptr 和 auto_ptr 。前者通常是较佳选择,因为其 copying 行为比较直观。若选择 auto_ptr ,复制动作会使它(被复制物)指向NULL。
条款14:在资源管理类中小心 copying 行为
以上的 tr1::shared_ptr 和 auto_ptr 都是用来管理 heap-based 资源,但并非所有资源都是 heap-based,对于那些资源,我们需要使用其他的资源管理者,有时后需要我们自己建立自己的资源管理类。这样就有了条款14 和条款15。
就拿管理互斥锁为例,此时我们关心的资源管理并不是锁资源的分配与释放,而是锁的上锁与解锁。于是我们用一个对象来管理这一组行为。
class Lock
{
public:
explicit Lock(mutex *pm): mutexptr(pm){lock(mutexptr);}
~Lock(){ulock(mutexptr);}
private:
mutex *mutexptr;
}
用户对 Lock 对象的使用符合 RAII 方式:
mutex m; //定义互斥锁
...
{
Lock l1(&m);
...
}
由于我们使用了 Lock 对象来管理锁,其析构函数会自动释放锁。
那么当一个 RAII 对象被复制,比如出现如下的代码:
Lock l1(&m);
Lock l2(l1);
对于 RAII 对象的复制行为,我们该如何定义呢? (这里我们主要讨论拷贝构造函数)
有四种方法:
1、禁止复制
参见条款06,继承 class Uncopyable 即可。如:
class Lock: private Uncopyable
{
public:
...
}
2、对底层资源祭出"引用计数法"(reference-count)
此种方法与 tr1::shared_ptr 类似,不同的是 tr1::shared_ptr 在引用数为 0 时将会销毁资源。幸运的是, tr1::shared_ptr 允许指定所谓的“删除器”(deleter),那是一个函数或函数对象,当引用次数为 0 是便被调用(此机能并不存在与 auto_ptr——它总是销毁资源)。删除器对于 tr1::shared_ptr 构造函数而言是可有可无的第二参数。
简单应用一下:
#include <iostream>
#include <tr1/memory>
using namespace std;
class base
{
public:
base(){cout << "base()" << endl;}
~base(){cout << "~base()" << endl;}
};
void MyDeleter(base* pb)
{
cout << "MyDeleter()" << endl;
}
class base_tr1
{
public:
base_tr1(base *pb): spb(pb, MyDeleter){cout << "base_tr1()" << endl;}
private:
tr1::shared_ptr<base> spb;
};
class base_auto
{
public:
base_auto(base *pb): spb(pb){cout << "base_auto()" << endl;}
private:
auto_ptr<base> spb;
};
int main()
{
base *pb1 = new base;
base *pb2 = new base;
base_tr1 bt1(pb1);
base_auto ba1(pb2);
cout << "Enter if(1)" << endl;
if(1)
{
base_tr1 bt2(bt1);
base_auto ba2(ba1);
}
cout << "Leave if(1)" << endl;
return 0;
}
运行结果:
base()
base()
base_tr1()
base_auto()
Enter if(1)
~base()
Leave if(1)
MyDeleter()
该 demo 说明 tr1::shared_ptr 确实可以修改其删除器,bt1和 bt2 共同管理的资源在程序结束时由于引用数为 0 而调用了指定函数 MyDeleter() 函数。
同时该段代码进一步说明 auto_ptr 对象的复制行为确实会使得被复制的对象失效,只留下复制的对象。因为 ba2 和 ba1 所管理的资源在离开 if(1) 之后就被销毁了。
这里的例子还举得不够合适。修改删除器的目的在于使得 base_tr1 类的构造函数(注意:是构造函数,并不包括拷贝构造函数)与删除器能够成对管理资源(此处并不是指简单的内存分配与回收这样的管理。比如对于mutex的加锁与解锁例子,在构造函数中调用 lock(),在删除器调用 unlock(),锁本身资源并不由该类管理)。
3、复制底部资源
这种方法是在复制资源管理对象的时候同时复制一份资源,即“深拷贝”。
4、转移底部资源的拥有权
auto_ptr 使用的就是这种方法。
请记住:
1、复制 RAII 对象必须一并复制它所管理的资源,所以资源的 copying 行为决定 RAII 对象的 copying 行为。
2、普遍而常见的 RAII class copying 行为是:抑制 copying、实施引用计数法(reference counting)。不过其他行为也都可能被实现。
条款15:在资源管理类中提供对原始资源的访问
这里有个小插曲,书中出现了“隐式转换函数”。啥?这词儿还是头回听。详细的介绍可以参见这篇文章http://blog.youkuaiyun.com/onejune2013/article/details/16803321 (《C++转换构造函数和隐式转换函数》码农时代)
估计博主也是在这里第一次听说这个词儿吧。\(^o^)/~
转换构造函数:当一个构造函数只有一个参数,而且该参数又不是本类的const引用时,这种构造函数称为转换构造函数。(很眼熟吧,其实我们平时自己写的一些构造函数就是转换构造函数,只是我们自己不知道罢了)
这里使用转换构造函数举个例子:
有趣的是插曲里又有了插曲,那就是 RVO。RVO(Return value optimization),是C++编译器对值传递返回会产生临时对象这一效率缺陷做出的优化。
详见:https://en.wikipedia.org/wiki/Return_value_optimization(Wikipedia)
其次,这两篇分析也具有参考价值。
https://www.ibm.com/developerworks/community/blogs/5894415f-be62-4bc0-81c5-3956e82276f3/entry/RVO_V_S_std_move?lang=en(《RVO V.S. std::move》Zhao Wu)
http://blog.youkuaiyun.com/virtual_func/article/details/48709617(《详解RVO与NRVO(区别于网上常见的RVO)》Virtual_Func的博客)
#include <iostream>
#include <tr1/memory>
using namespace std;
class base
{
public:
base(int id): id(id){cout << "base() " << id << endl;}
base(const base& b): id(b.id){cout << "base(const base& b) " << id << endl;}
~base(){cout << "~base() " << id << endl;}
base operator+(const base& b)
{
return base(id + b.id);
}
void display(){cout << id << endl;}
private:
int id;
};
int main()
{
base b1 = 1; //相当于 base b1(1);
base b2 = b1 + 2; //相当于 base b2(b1 + base(2));
return 0;
}
输出结果为:
base() 1
base() 2
base() 3
~base() 2
1
3
~base() 3
~base() 1
不科学啊!一个拷贝构造函数都没有被调用?这就是 RVO 优化过得结果。使用 -fno-elide-constructors 参数在编译时取消掉 RVO,再次打印结果:
base() 1 //这边的 base b1 = 1;在不优化时居然是先将1转换成base(1)后再拷贝赋值到b1,这里先不深究
base(const base& b) 1
~base() 1
base() 2
base() 3
base(const base& b) 3
~base() 3
base(const base& b) 3
~base() 3
~base() 2
1
3
~base() 3
~base() 1
这次熟悉了吧。编译器果然优化不少。
对于RAII class,我们需要提供访问其原始资源的方法。有两个做法:
1、显式转换
显式转换的方式如同 tr1::shared_ptr 和 auto_ptr 都提供一个 get() 成员函数一样,用来执行显式转换,也就是它会返回智能指针内部的原始指针(的复件):
2、隐式转换
就像所有(几乎)智能指针一样,tr1::shared_ptr 和 auto_ptr 也重载了操作符 operator-> 和 operator* ,它们允许隐式转换至底部原始指针。如下:
class Investment
{
public:
bool isTaxFree() const;
...
};
Investment* creatInvestment();
std::tr1::shared_ptr<Investment> pi1(creatInvestment());
bool taxable1 = !(pi1 -> isTaxFree());
...
std::tr1::shared_ptr<Investment>pi2(creatInvestment());
bool taxable2 = !((*pi2).isTaxFree());
有时候还必须取得 RAII 对象内的原始资源,于是有了一种隐式转换函数法。如下:
FontHandle getFont();
void releaseFont(FontHandle fh);
void changeFontSize(FontHandle f, int newSize); //以上函数为一组 C API,用来管理字体
class Font
{
public:
explicit Font(FontHandle fh):f(fh){}
~Font(){releaseFont(f);}
FontHandle get() const {return f;} //显式转换函数
operator FontHandle() const {return f;} //隐式转换函数
private:
FontHandle f; //原始字体资源
}
Font f(getFont());
int newFontSize;
changeFontSize(f.get(), newFontSize); //显式转换
changeFontSize(f, newFontSize); //隐式转换
请记住:
1、APIs往往要求访问原始资源(raw resources),所以每一个 RAII class 应该提供一个“取得其所管理之资源”的办法。
2、对原始资源的访问可能经由显式转换或隐式转换。一般而言显式转换比较安全,但隐式转换对客户比较方便。