第一:以对象管理资源。
(1)为防止资源泄露,请使用RAII对象(智能指针),他们在构造函数中获得资源并在析构函数中释放资源。
如:Investment*createInvestment();
返回指针,指向Investment继承体系内的动态创建的分配对象。调用者有责任删除它。但是有时调用者忘记了,有时可能在删除资源的代码之前的代码中发生了意外,造成程序退出了,从而造成资源没有被释放,所以一般使用下面的形式:
void f()
{
auto_ptr<Investment>pInv(createInvestment());
}
上面把资源放进对象内,我们便可依赖C++的“析构自动调用机制”确保资源被释放。以上代码中createInvestment返回的资源被当做其管理者auto_ptr的初值。实际上“以对管理资源”的观念常被称为“资源取得时机便是初始化时机”(RAII),因为我们几乎总是在获得一笔资源后于同一语句内以它初始化某个管理对象。
(2)两个常用的RAII类分别是auto_ptr和shared_ptr。后者通常是较佳选择,因为其copy比较直观,很适合STL中对象的复制。如选择auto_ptr,复制动作会使他(被复制物)指向null,所以一般不用在STL中。
第二:在资源管理类中提供对原始资源的访问。
(1)APIs往往要求访问原始资源,所以每一个RAII类应该提供一个“取得其所管理之资源”的方法。
(2)对原始资源的访问可能经由显示转换或隐式转换。一般而言显示转换比较安全,但隐式转换对客户比较方便。如(第一中的例子):
shared_ptr<Investment>pInv(createInvestment());
假设你希望以某个函数处理Investment对象,像这样:
int daysHeld(constInvestment* pi);
如果你这么调用它
int days =daysHeld(pInv);
却通不过编译,因为daysHeld需要的是Investment*指针,而你传给他的确是个类型为shared_ptr<Investment>的对象。
现在有两种方法可以提供对其所含的原始资源进行访问:
显示转换:
auto_ptr和shared_ptr都提供了一个get成员函数,用来执行显示转换,也就是它会返回智能指针内部的原始指针。
int days =daysHeld(pInv.get()); //没问题
就像(几乎)所有智能指针一样,auto_ptr和shared_ptr也重载了指针取值操作符(operator->和operator*),他们允许隐式转换至底部原始指针。如:
class Investment //继承体系的根类
{
public:
bool isTaxFree()const;
};
Investment *createInvestment(); //工厂函数
shared_ptr<Investment>pi1(createInvestment());
bool taxable1 =!(pi1->isTaxFree()); //由operator->访问资源
.....
auto_ptr<Investment>pi2(createInvestment());
bool taxable2 =!((*pi2).isTaxFree()); //由operator*访问资源
隐式转换:
考虑下面的例子:
FontHandle getFont();
void releaseFont(FontHandle fh);
class Font
{
public:
explicit Font(FontHandle fh)
:f(fh){}
FontHandle get()const {return f;}
private:
FontHandle f;
};
现在假设有大量的API需要处理的是FontHandle 对象,那么将"Font转换为FontHandle"会是一项很频繁的操作,所以许多程序员就不愿意使用这个类。
所以另一种办法是采用隐式转换:
class Font
{
public:
......
operator FontHandle()const //隐式转换函数
{return f;}
};
现在假设有函数
void changeFontSize(FontHandle f,intnewSize);
就可以这样用了:
Font f;
int newFontSize;
changeFontSize(f,newFontSize);
但是这个隐式转换会增加错误的发生机会。例如客户可能会在需要Font时意外的创建一个FontHandle:
Font f1(getFont());
...
FontHandle f2 = f1;//愿意是拷贝一个Font对象,却反而将f1隐式转换为其底部的FontHandle然后才复制它
以上程序有个FontHandle由Font对象f1管理,但那个FontHandle也可以通过直接使用f2取得。那几乎不会有好下场。例如当f1被销毁,字体被释放,而f2因此成为“虚掉的”。
综上:使用显示的要安全得多,因此也受欢迎得多。
第三:在资源管理类中小心复制行为。
(1)复制RAII对象必须一并复制它所管理的资源,所以资源的复制行为决定RAII对象的复制行为。
(2)普遍而常见的RAII类复制行为是:抑制复制、施行引用计数法。不过其他行为也都可能被实现。
auto_ptr和shared_ptr是将观念表现在heap-base资源上,然而并非所有资源都是heap-base。所以有可能你需要建立自己的资源管理类。
通常只要内含一个shared_ptr成员变量,RAII类就可以实现出“引用计数法”行为。看下面的类
class Lock
{
public:
explicit Lock(Mutex* pm)
:mutexPtr(pm)
{lock(mutexPtr);} //获得资源
~Lock(){unlock(mutesPtr);} //释放资源
private:
Mutex* mutesPtr;
};
现在如果打算对Lock类使用“引用计数法”它可以改变mutexPtr的类型,将它从Mutex*改为shared_ptr<Mutex>。然而shared_ptr的缺省行为是“当引用计数为0时删除其所指物”,那不是我们想要的行为。当我们用上一个Mutex,我们想要做的释放动作是解除锁定而非删除。
幸运的是shared_ptr允许指定所谓的“删除器”,那是一个函数或函数对象。当引用计数为0时便被调用(此技能并不存在auto_ptr——它总是将指针删除)。删除器对shared_ptr构造函数而言是可以可无的第二个参数。所以代码看起来像这样:
class Lock
{
public:
explicit Lock(Mutex* pm) //以某个Mutex初始化shared_ptr,并以unlock函数为删除器
:mutexPtr(pm,unlock)
{lock(mutexPtr.get());}
private:
shared_ptr<Mutex> mutexPtr; //使用shared_ptr替换之
};
请注意,本例的Lock类不再声明析构函数。因为没有必要。class析构函数(无论自己写的还是编译器生成的)会自动调用其non-static成员变量(本例为mutexPtr)的析构函数。而mutexPtr的析构函数会在互斥器的引用计数为0时自动调用shared_ptr的删除器(本例为unlock)。