所谓资源就是一旦使用完,就必须将其归还给系统,否则就有你好果子吃
。在C++总,常用的资源包括动态分配内存、文件描述符、互斥锁、UI中的字型和笔刷、数据库连接,网络socket等。接下来本文介绍几种管理资源的方式,以解决资源泄露或回收问题。
一、以对象管理资源
假设我们使用一个用来朔模投资行为的程序库,其中各式各样的投资类型继承自一个root class Investment:
class Investment {...}; //投资类型继承体系中的root class
进一步假设,这个程序库通过一个工厂函数供应我们某个特定的Investment对象:
Investment* createInvestment();
上面函数返回一个指针,指向Investment继承体系内的动态分配对象,调用者有责任删除它。假设有个函数f履行了这个责任:
void f()
{
Investment* pInv = createInvestment(); //调用工厂函数
...
delete pInv; //释放pInv所指对象
}
咋一看上面天衣无缝,函数f能够自动删除它从createInvestment获得的投资对象,但是事实上危机重重:
(1)由于疏忽或者后期接收者维护代码导致“...”区域内的一个过早的return语句或goto语句。
(2)“...”区域内的语句抛出异常。
上面的任意情况发生,都会导致因无法执行到delete语句而内存泄露。事实上,函数f中将资源的管理依赖于人治,所以悲剧地发生只是时间问题。
为了解决如上问题,需要将返回的资源放进对象内,以对象来管理资源,当控制流离开f,该对象的析构函数会自动释放那些资源。
1、auto_ptr
许多资源被动态分配于堆上而后被用于单一区块或函数内,资源应该在控制流离开这个区块或函数时被释放,标准程序库提供的auto_ptr正是针对这种形式而设计的特制产品。
auto_ptr是一个“类指针(pointer-like)对象”,也就是所谓的智能指针,其析构函数自动对其所指对象调用delete。下面示范如何使用auto_ptr以避免函数f潜在的资源泄露可能性:
void f()
{
std::auto_ptr
pInv(createInvestment());
...
}
函数f中调用工厂函数产生投资对象,并将对象由auto_ptr管理,然后一如既往地使用pInv,在控制流离开函数f时,由auto_ptr的析构函数自动删除pInv。
上面代码中createInvestment返回的资源被当做其管理者auto_ptr的初值,这种方式被称为“资源获得时机便是初始化时机”(Resource Acquisition Is Initialization, RAII)。
需要注意的是:如果通过copy构造函数或copy assignment操作符复制auto_ptr,则其会变成NULL,而复制所得的指针将取得资源的唯一拥有权,也就是只能有一个auto_ptr对象管理资源,比如:
std::auto_ptr pInv1(createInvestment()); //pInv1指向createInvestment返回物
std::auto_ptr pInv2(createInvestment()); //pInv2指向对象,而pInv1被设为NULL
pInv1 = pInv2; //现在pInv1指向对象,而pInv2被设为NULL
受auto_ptr管理的资源必须绝对没有一个以上的auto_ptr同时指向它,这意味着auto_ptr并非管理动态分配资源的神兵利器,比如STL容器就要求其元素发挥“正常的”复制行为,因此就用不上auto_ptr。
2、shared_ptr
auto_ptr的替代方案是“引用计数型智能指针”(referece-counting smart pointer, RCSP)。所谓RCSP也是个智能指针,持续追踪共有多少对象指向某笔资源,并在无人指向它时自动删除该资源。RCSP提供的行为类似垃圾回收(garbage collection),不同的是RCSP无法打破环状引用(cycles of references,比如:两个其实已经没被使用的对象彼此互指,因而好像还处在“被使用”状态)。
TR1的tr1::shared_ptr就是一个RCSP,所以函数f的又一版本来了:
void f()
{
std::tr1::shared_ptr pInv(createInvestment()); //在离开函数时,由shared_ptr析构函数自动删除pInv
...
}
这与auto_ptr版本基本一致,但shared_ptr的复制行为正常多了:
void f()
{
std::tr1::shared_ptr pInv1(createInvestment); //pInv1指向createInvestment返回物
std::tr1::shared_ptr pInv2(pInv1); //pInv2和pInv2指向同一个对象
pInv1 = pInv2; //同上,无任何改变
...
}
在函数f控制流结束时,pInv1和pInv2被销毁,其所指对象也就被自动销毁。由于tr1::shared_ptr的复制行为“一如预期”,它可被用于STL容器以及其它“auto_ptr之非正统复制行为并不适用”的语境上。
但是auto_ptr和tr1::shared_ptr两者都在其析构函数内做delete而不是delete[],这意味其不能用于动态分配的数组,而boost::scoped_array和boost::shared_array class则能提供这种行为。
二、资源管理类中小心coping行为
我们在使用C API函数处理类型为Mutex的互斥量对象(mutex object)时,一般会提供lock和unlock函数:
void lock(Mutex* pm); //锁定pm所指的互斥量
void unlokc(Mutex* pm); //将互斥量解除锁定
为了确保绝不会忘记将一个被锁住的Mutex解锁,一般会创建一个class来管理锁。这样的class的基本结构由RAII守则支配,也就是“资源在构造期间获得,在析构期间释放”:
class Lock {
public:
explicit Lock(Mutex* pm)
: mutexPtr(pm)
{ lock(mutexPtr); } //获得资源
~Lock() { unlock(mutexPtr); } //释放资源
private:
Mutex *mutexPtr;
};
客户对Lock的用户符合RAII方式:
Mutex m; //定义你需要的互斥量
...
{ //建立一个区块用来定义critical section
Lock ml(&m); //锁定互斥量
... //执行critical section内的操作
} //在区块最末尾,自动解除互斥量锁定
上面代码看似很完美,但是如果Lock对象被复制,又将如何呢?
Lock m11(&m); //锁定m
Lock m12(ml1); //将ml1复制到ml2身上,会发送什么呢?
一般为了防止复制RAII对象带来的副作用,会采取禁止复制的方式,也就是将coping操作声明为private,包括拷贝构造函数和赋值操作符。
三、承诺使用new和delete是要采取相同型式
成对使用new和delete时要采用相同型式,比如:如果你在new表达式中使用[],必须在相应的delete表达式中也使用[],否则的话其结果未定义(在delete时可能会导致太少的析构函数被调用);如果你在new表达式中不使用[],一定不要在相应的delete表达式中使用[]。
为什么会如此,因为在new单一对象和对象数组时内存布局会不同,可能的布局如下:
单一对象 object
对象数组 n | object |object | ...
其中,n表示数组大小,当然不同的编译器做法不尽相同。
四、独立语句将newed对象植入智能指针
假设我们有个函数用来揭示处理程序的优先权,另一个函数用来在某动态分配所得的widget上进行某些带优先权的处理:
int priority();
void processwidget(std::tr1::shared_ptr pw, int priority);
假设我们如此调用:
processwidget(std::tr1::shared_ptr(new widget), priority());
上面代码看似用对象管理资源,但是事实上确还是有可能泄露资源。因为编译器在产出一个processwidget调用码之前,会先核算即将被传递的各个实参,并做如下几件事情:
(1)调用函数priority
(2)执行new widget
(3)调用tr1::shared_ptr构造函数
但C++编译器不一定会按照上面顺序来生成代码,最终可能的操作序列会是:
(1)执行new widget
(2)调用priority函数
(3)调用tr1::shared_ptr构造函数
好吧,问题来了,如果在调用priority函数时产生异常,则new widget返回的指针会被遗失,因为其尚未被置入tr1::shared_ptr内,从而引发资源泄露。
避免产生这种问题的方法很简单:使用分离语句,分别写出(1)创建widget;(2)将它置入一个智能指针内,然后将智能指针传给processwidget函数:
std::tr1::shared_ptr pw(new widget);
processwidget(pw, priority());
以独立语句将newed对象存储于(置入)智能指针内,如果不这样做,一旦异常抛出,就有可能导致难以察觉的资源泄露。
阅读(4166) | 评论(1) | 转发(1) |