所谓资源就是,一旦你用了它将来必须还给系统。
C++程序中最常使用的资源就是动态分配内存,但内存只是你必须管理的众多资源之一。例如:文件描述符、互斥锁、图形界面中的字型和笔刷、数据库连接及网络socket。不论是哪一种资源,当你不再使用它的时候,必须将它还给系统。
条款13:以对象管理资源
先来看个例子:
class Investment {...}; //继承体系中的root class
Investment* createInvestment();
//返回指针,指向Investment继承体系内动态分配的的对象,调用者有责任删除它。
//函数fun履行了这个责任
void fun()
{
Investment* pInv=createInverstment();
...
delete pInv;
}
这个例子乍一看没有什么问题,动态申请的对象在函数结尾也delete了,但有些情况fun可能无法删除pInv
- 假设在“…”区域内有一个过早的return语句,如果这个return语句被执行了,那么程序就不可能会触及delete语句。
- 还可能是在pInv的使用和delete动作在某个循环内,而该某个循环由于某个continue或者goto语句过早退出,也可能不会delete
- 最后一种可能是“…”区域内的语句抛出异常,那么同样不会执行到delete
为了确保createInvestment返回的资源总是被释放,我们需要将资源放进对象内,当控制流立刻fun,该对象的析构函数会自动释放那些资源。
标准程序库提供的auto_ptr正是针对这种形式设计的特质产品,也就是所谓的“智能指针”。来看看这个对象的使用:
void fun()
{
std::auto_ptr<Investment> pInve(createInverstment());
...
}
调用createInvestment函数,一如既往的使用pInv,经由auto_ptr的析构函数自动删除pInv。
以对象管理资源主要有两个关键想法:
- 获得资源后立刻放进管理对象内
- 管理对象运用析构函数确保资源被释放
由于auto_ptr被销毁的时会自动删除它所指之物,所以不要让多个auto_ptr指向同一个对象,如果这样做了,那么就会出现同样的对象被删除多次的情况。为了防止这个问题,auto_ptr有一个性质:若通过拷贝构造函数或赋值运算符重载复制它们,它们就会变成NULL,而复制所得的指针将取得资源唯一的拥有权。但这意味着auto_ptr并非管理动态内存的神兵利器,因为它无法有正常的复制行为,而我们有的时候需要这种行为,例如STL。
取代auto_ptr就是shared_ptr——–引用计数型智能指针,在下一个条款会详细介绍。
请记住:
- 为了防止内存泄漏,请使用RAII对象,它们在构造函数中获得资源并在析构函数中释放资源
条款14:在资源管理类中小心copying行为
刚刚的条款13引入的RAII机制和智能指针,并非所有的资源都适用,对于一些不是heap_based的资源,智能指针往往不适合作为资源掌管者。那么这个时候,你就需要建立自己的资源管理类
例如下面这个类:
class Lock
{
public:
explict Lock(Mutex* pm)
:mutexPty(pm)
{
lock(mutexPtr);
}
~Lock()
{
unlock(mutexPtr);
}
private:
Mutex* mutexPtr;
};
客户对Lock的用法符合RAII方式,但是如果Lock对象被复制会发生什么现象?
Mutex m;
Lock mu1(&m); //锁定m
Lock mu2(mu1); //将mu1复制到mu2身上???
当一个RAII对象被复制,会发生什么事?大多数时候你会选择以下两种情况:
- 禁止复制—-许多时候允许RAII对象被复制并不合理。那就可以将拷贝构造函数和赋值运算符重载声明为私有,而且不实现。—-unique_ptr
- 底层资源用“引用计数法”—-也就是shared_ptr的实现方法。但是,有的时候我们并不希望析构的时候删除所指物,例如刚刚的mutex,我们想要做的释放动作是解除锁定。这就要用到“定制删除器”了。所谓的删除器也就是一个函数或函数对象。当引用次数为0时便被调用。
复制RAII对象必须一起复制它所管理的资源,所以资源的拷贝行为决定RAII的拷贝行为。
条款15:在资源管理类中提供对原始资源的访问
条款13说过,使用智能指针保存createInvestment函数的调用结果,假设你希望以某个函数处理Investment对象,像这样
int daysHeld(const Investment* pi); //返回投资天数
你想这样调用它:
int days = daysHeld(pInv); //错误
这样是不能通过编译的,因为daysHeld需要的是Investment*类型的指针,而pInv却是一个shared_ptr的对象,这时候你需要一个函数可以将RAII类对象转换为其内部的原始资源。有两个做法可以达成目标:隐式转换、显式转换
shared_ptr和auto_ptr内部都提供一个get成员函数,用来执行显式转换,也就是get函数会返回智能指针内部的原始指针。
int days = daysHeld(pInv.get());
还有一种方式是提供一个隐式转换函数,看下面的例子:
FontHandle getFont(); //这是一个C API,省略参数
void releaseFont(FontHandle fh); //来自同一组C API
class Font
{
public:
explicit Font(FontHandle fh)
:f(fh)
{}
~Font()
{
releaseFont(f);
}
private:
FontHandle f;
};
假设现在有大量C API都要处理FontHandle,那么将Font对象转换为Fonthandle对象会是一种和频繁的操作,那么Font类就可以提供一个显示转换函数,像get那样:
class Font
{
public:
FontHandle get()const
{ return f; }
...
};
但是可以想到,只有我们想使用API的时候就需要调用get函数,那么就可以令Font类提供隐式转换函数,将类型转为FontHandle:
class Font
{
public:
operator FontHandle()const
{ return f; }
...
};
但是这种隐式类型转换有时候会出现错误,假设我们可能会在需要Font类型时,却意外创建了一个FontHandle类型。
是否应该提供一个显示转换函数还是隐式转换函数,取决于RAII类被设计执行的特定工作,以及它被使用的情况。一般而言显式转换比较安全,但隐式转换对客户比较方便。
条款16:成对使用new和delete时要采取形同形式
当你使用new的时候有两件事发生:
- 内存被分配出来
- 针对此内存会有一个或多个构造函数被调用
当你使用delete的时候也有两件事发生:
- 针对此内存有一个或多个析构函数被调用
- 内存被释放
delete的最大问题在于:即将被删除的内存里究竟存有多少对象?这个问题的答案决定了有多少个析构函数必须被调用。实际上这个问题可以更简单些:即被删除的那个指针,所指的是单一对象或对象数组?
当你对着一个指针使用delete,唯一能够让delete知道内存中是否存在一个数组的方法就是由你来告诉它。如果你使用delete时加上方括号[ ],delete便认定指针指向一个数组,否则它便认为指针指向单一对象。
也就是说,如果你调用new时使用[ ],那么对应的在调用delete时也使用[ ]。如果你调用new时没有使用[ ],那么也不应该在对应调用delete时使用[ ]。
条款17:以独立语句将new的对象置入智能指针
先来看个例子:
int priority();
void processWidget(shared_ptr<Widget> pw, int priority);
现在调用processWidget:
processWidget(new Widget, priority());
这个调用形式是不能通过编译的,因为shared_ptr构造函数需要一个原始指针,但该构造函数是个explicit构造函数,无法进行隐式转换,将得到“newWidget”的原始指针转换为processWidget所要求的shared_ptr。下面就可以通过编译:
processWidget(shared_ptr<Widget>(new Widget), priority());
来剖析一个这个式子:
编译器产生出一个processWidget调用码之前,必须先核对参数。上述第二个实参只是单纯对priority函数的调用,但第一个实参shared_ptr(new Widget)由两部分组成
- 执行“new Widget”表达式
- 调用shared_ptr的构造函数
编译器完成的这些事情是以什么顺序呢?不是很确定,但是可以肯定的是“new Widget”一定执行在shared_ptr构造函数之前,如果是下面的顺序:
- 执行“new Widget”表达式
- 调用priority
- 调用shared_ptr的构造函数
如果在调用priority函数期间导致异常,那么“new Widget”指针将会遗失,因为它尚未置入shared_ptr内,所以就可能引发内存泄漏。来看看避免的方法:
shared_ptr<Widget> pw(new Widget);
processWidget(pw, priority());
只需要使用分离语句,分别写出创建Widget,将它置入智能指针内,然后再把智能指针传给processWidget。
所以:以独立语句将new的对象存储于只能指针内,如果不这样做,一旦异常被抛出就会产生难以察觉的内存泄漏。