EC之Resource Management

本文探讨了在C++中如何有效地使用智能指针和其他RAII技术来管理资源,包括auto_ptr和tr1::shared_ptr的使用场景及其优缺点,并讨论了如何在资源管理类中实现复制行为。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

条款13:Use objects to manage resources

假设我们使用一个用来模拟投资行为的程序库,各种投资类型继承自root class Investment:

class Investment { ... }; //“投资类型”继承体系中的root class

进一步假设,该程序库通过一个工厂函数提供某个特定类型的Investment对象:

Investment* createInvestment(); //简单起见,省略参数

调用createInvestment者有责任在使用了函数返回的对象后删除之。假设有个函数f做了这项工作:

void f() {
  Investment *pInv = createInvestment();
  ...
  delete pInv;
}

这看起来妥当,但在某些情况下f可能无法删除它得自createInvestment的投资对象——这可能是因为一个过早的return语句;也有可能对createInvestment的使用及delete动作位于某循环内,而该循环由于某个continue或goto语句过早退出;又或者delete之前有异常抛出。无论如何,一旦delete没有执行,我们泄漏的不只是投资对象的内存,还包括投资对象所保存的任何资源。

当然,谨慎地编写程序可以避免此类错误。但你必须考虑到软件接受维护的情况,后来者可能修改了某些语句导致delete不被执行,因此单纯地倚赖f来释放资源是行不通的。

为确保资源被释放,我们需要将资源放入对象内,当对象被销毁时,其析构函数将会自动释放资源。“以对象管理资源”的观念也叫“资源获得即初始化”(Resource Acquisition Is Initialization;RAII),因为我们总在获取资源后用其初始化某个管理对象。

标准库提供了auto_ptr的RAII类来管理资源。auto_ptr是个“类指针对象”,即“智能指针”,下面介绍如何使用auto_ptr来避免f函数潜在的资源泄漏可能性:

void f() {
  std::auto_ptr<Investment> pInv(createInvestment());
  ...    //auto_ptr的析构函数将自动删除pInv
}

由于auto_ptr被销毁时将自动删除所指物,所以一定要注意别让多个auto_ptr指向同一个对象,否则对象将会被删除多次,其结果未定义。为了预防这个问题,auto_ptrs有个特殊的性质:若通过copy构造函数或copy assignment操作符复制它们,它们会变成null,而复制所得指针将获取资源的唯一所有权。

std::auto_ptr<Investment>
  pInv1(createInvestment());

std::auto_ptr<Investment> pInv2(pInv1); 
//现在pInv2指向对象,pInv1为null

pInv1 = pInv2;
//现在pInv1指向对象,pInv2为null

这一诡异的复制行为,加上其底层条件:“受auto_ptrs管理的资源必须绝对没有一个以上的auto_ptr同时指向它”,造成了auto_ptr在资源管理上的局限性。比如,auto_ptr不能用作STL容器的元素,因为STL容器要求其元素有正常的复制行为。

auto_ptr的替代方案是“引用计数型智能指针”(reference-counting smart pointer;RCSP)。RCSP将统计指向某笔资源的对象数目,当没有对象指向该资源时自动删除它。其行为类似垃圾回收(garbage collection),不同的是RCSP无法打破环状引用。

TR1的tr1::shared_ptr就是个RCSP,我们可以重写f如下:

void f() {
  std::tr1::shared_ptr<Investment>
  pInv(createInvestment());
  ...
}

这段代码和使用auto_ptr的版本几乎一模一样,但shared_ptrs的复制行为就正常多了:

void f() {
  std::tr1::shared_ptr<Investment>
  pInv1(createInvestment());

  std::tr1::shared_ptr<Investment>
  pInv2(pInv1);
  //pInv1和pInv2指向同一对象

  pInv1 = pInv2;
  //同上

  ...   //pInv1和pInv2被销毁,
        //它们所指的对象也被自动销毁。
}

值得注意的是,auto_ptr和tr1::shared_ptr两者都在析构函数中做delete而不是delete[],因此不要在动态数组上使用auto_ptr或tr1::shared_ptr,尽管这么做可以通过编译:

std::auto_ptr<std::string>
  aps(new std::string[10]);
std::tr1::shared_ptr<int> spi(new int[1024]);
//会用上错误形式的delete

总结:

  • 为防止资源泄漏,请使用RAII对象,它们在构造函数中获得资源并在析构函数中释放资源。
  • 两个常用的RAII类分别是auto_ptr和tr1::shared_ptr,后者是较好的选择,因为其有着正常的复制行为。

条款14:Think carefully about copying behavior in resource-managing classes

对于非heap-based资源,auto_ptr和tr1::shared_ptr并不适用。因此有时我们需要建立自己的资源管理类。

假设我们使用C API处理类型为Mutex的互斥器对象,共有lock和unlock两函数可用:

void lock(Mutex* pm);    //锁定pm所指的互斥器对象
void unlock(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;      //定义互斥器
...
{             //建立区块定义临界区
Lock ml(&m);  //锁定互斥器
...           //执行临界区内的操作
}             //自动解除互斥器锁定

这很好,但如果Lock对象被复制,会发生什么事?

Lock ml1(&m);
Lock ml2(ml1);

一个一般化的问题时:当一个RAII对象被复制,会发生什么事?通常,有以下几种可能:

  • 禁止复制。很多时候允许RAII对象被复制并不合理。这时候,你便应该禁止copying行为。
  • 对底层资源进行引用计数。有时候我们希望保存资源,直到它的最后一个使用者被销毁。这种情况下复制RAII对象时,应该增加其被引用数。这正是tr1::shared_ptr的行为。

通常只要内含一个tr1::shared_ptr成员变量,RAII classes便可实现出reference-counting copying行为。

class Lock {
public:
  explicit Lock(Mutex* pm) : mutexPtr(pm, unlock) { //指定unlock函数为删除器
   lock(mutexPtr.get());
  }
private:
  std::tr1::shared_ptr<Mutex> mutexPtr;
};

这里不再声明析构函数,而是倚赖编译器的默认行为。

  • 复制底层资源
  • 转移底层资源的拥有权。就像auto_ptr表现的那样。

条款15:Provide access to raw resources in resource-managing classes

资源管理类很棒,它们是你对抗资源泄漏的堡垒。但很多APIs需要直接访问原始资源,一个设计良好的资源管理类必须能满足这种需求。

有两种做法可以达成目标:显式转换和隐式转换。

tr1::shared_ptr和auto_ptr都提供了一个get成员函数,用来执行显式转换。同时它们也重载了operator->和operator*,允许隐式转换成原始指针。

由于有时候还是必须取得RAII对象内的原始资源,某些RAII class设计者为RAII class提供一个隐式转换函数。考虑下面这个用于字体的RAII class(对C API而言字体是一种原生数据结构):

FontHandle getFont();             
void releaseFont(FontHandle fh);

class Font {
public:
  explicit Font(FontHandle fh) : f(fh) {}
  ~Font() { releaseFont(f); }
private:
  FontHandle f;
};

假设有大量与字体相关的C API,它们处理的是FontHandles,那么“将Font对象转换为FontHandle”会是一种很频繁的需求。Font class可为此提供一个显式转换函数,像get那样:

class Font {
public:
  ...
  FontHandle get() const { return f; }   //显式转换函数
  ...
};

不幸的是这使得客户每当想要使用API时就必须调用get。

另一个办法是令Font提供隐式转换函数,转型为FontHandle:

class Font {
public:
  ...
  operator FontHandle() const {  //隐式转换函数
    return f;
  }
  ...
};

这使得调用C API时比较轻松且自然,但是会增加错误发生机会。


总结:

  • APIs往往要求访问原始资源(raw resources),所以每个RAII class应该提供一个“取得其所管理的资源”的办法。
  • 对原始资源的访问可能经由显式转换或隐式转换。一般而言,显式转换比较安全,隐式转换对客户比较方便。

条款16:Use the same form in corresponding uses of new and delete

如果你在new表达式中使用[],必须在相应的delete表达式中也使用[]。如果你在new表达式中不使用[],一定不要在相应的delete表达式中使用[]。

条款17:Store newed objects in smart pointers in standalone statements

假设我们有个函数用来揭示处理程序的优先权,另一个函数用来在某动态分配所得的Widget上进行某些带有优先权的处理:

int priority();
void processWidget(std::tr1::shared_ptr<Widget> pw, int priority);

出于对“以对象管理资源”策略的实践,processWidget决定对其动态分配得来的Widget运用智能指针。现在考虑以下调用:

processWidget(std::tr1::shared_ptr<Widget>(new Widget), priority());

令人惊讶的是,尽管我们在此使用了“对象管理式资源”,上述调用仍可能造成资源泄漏。

这是因为C++没有规定表达式内的参数计算次序,因此尽管new Widget表达式的执行总是发生在tr1::shared_ptr构造函数之前,但调用priority的动作可能在最前面,中间或最后面。假设priority调用发生在两者之间,而且其调用导致异常,那么new Widget返回的指针将会遗失。是的,在对processWidget的调用过程中可能引发资源泄漏,因为在“资源被创建”和“资源被转换为资源管理对象”的过程中有可能发生异常干扰。

避免此类问题的方法很简单:使用分离语句。

std::tr1::shared_ptr<Widget> pw(new Widget);
processWidget(pw, priority());

以上方法可行,因为编译器对于“跨越语句的各项操作”没有重新排列的自由。

总结:

以独立语句将newed对象存储于智能指针内,否则,一旦抛出异常,有可能导致难以觉察的资源泄漏。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值