`
网络接口
  • 浏览: 43333 次
文章分类
社区版块
存档分类
最新评论

C++之资源管理

 
阅读更多

所谓资源就是一旦使用完,就必须将其归还给系统,否则就有你好果子吃。在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对象存储于(置入)智能指针内,如果不这样做,一旦异常抛出,就有可能导致难以察觉的资源泄露。

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics