《Effective C++》 第三章: 资源管理

文中段落序号对应着书中的item编号。本文介绍item 13~17

本章主要介绍资源管理的一些思想和技巧。

什么是资源?简要地说就是用了之后必须归还的事物,比如说new申请的堆内存、文件、数据库连接、互斥锁等等。这些资源容易出现一些问题(比如使用后忘了释放),利用C++的一些机制可以有效避免。

13. 使用类来管理资源

先看这样一段代码:

Investment* createInvestment();//动态申请一个Investment对象,返回对象的指针

void f()
{
Investment *pInv = createInvestment();
/*一些操作*/
delete pInv;
}

函数f在执行时先申请了一个Investment对象,最后回收。看起来这没什么问题,对吧?

问题是中间的一些操作,万一里面有return、exit、throw等等呢?类似的还有在循环体中的break、continue等。听起来这是正常人不会犯的低级错误,但是假如代码非常复杂呢?假如有个不很了解这段代码的人半路接手了这个代码呢?假如你时隔一年之后已经忘了自己怎么实现的,但又想优化一下代码呢?

这些事情非常容易发生。不过幸好,我们可以借助C++的类机制来管理这些资源。比如说将申请资源的代码写在构造函数中,释放资源的代码写在析构函数中。因为析构函数会自动调用,我们可以在析构函数中自动地回收资源。

auto_ptr

auto_ptr是一个管理指针的对象。
如果使用auto_ptr来优化前面的f函数,就变成了下面这样

void f()
{
std::auto_ptr<Investment> pInv(createInvestment()); 
/*一些操作*/
}

pInv会在析构时自动delete Investment的指针。
不过要小心指针被重复delete的问题。假如有两个auto_ptr保存着同样的指针,那么该指针会被重复delete。
另外一个奇怪的地方是auto_ptr赋值后赋值的一方指针会变成null(拷贝构造函数同理)。

std::auto_ptr<Investment>  pInv1(createInvestment()); 
std::auto_ptr<Investment> pInv2(pInv1); // pInv1现在变成null
pInv1 = pInv2; // pInv2变成null

由于STL的底层需要用到正常的赋值操作,auto_ptr不适合使用STL。

shared_ptr

shared_ptr是一个引用计数智能指针(reference-counting smart pointer)。它会自动统计每个生成的对象有多少的引用,当引用数为0时再回收该对象。听起来像是垃圾回收机制,不过它不能处理环状引用问题。

void f()
{
...
std::tr1::shared_ptr<Investment>
pInv(createInvestment());
/*一些操作*/
}

shared_ptr的赋值和拷贝构造函数就正常多了,也可以用在STL中。

auto_ptr和shared_ptr不能用于数组

auto_ptr和shared_ptr中的资源回收都是通过delete实现的,而不是delete[]。 也就是它不可以回收数组。

14. 自定义资源管理类

auto_ptr 和tr1::shared_ptr功能比较有局限性,很多时候我们需要创建自己的资源管理类。
以互斥锁为例。

void lock(Mutex *pm); // lock mutex pointed to by pm
void unlock(Mutex *pm); // unlock the mutex

如果不借助类,互斥锁应当这样实现:lock→临界代码区→unlock
如果借助资源管理类的实现如下所示:
定义类:

class Lock {
public:
explicit Lock(Mutex *pm)
: mutexPtr(pm)
{ lock(mutexPtr); } // acquire resource
~Lock() { unlock(mutexPtr); } // release resource
private:
Mutex *mutexPtr;
};

Mutex m; //定义互斥量
...
{
Lock ml(&m); //加锁
... //临界区
}

赋值/拷贝的处理

在前面的例子中,假如对Lock进行拷贝会怎么样?

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

在资源管理类中,对于拷贝和赋值必须小心。通常有以下策略。

    1. 不允许拷贝。将拷贝构造函数设置为private
    1. 引用计数。类似于tr1::shared_ptr的策略,只有当计数为0时再回收。这个方案可以用到tr1::shared_ptr,一个例子如下:
class Lock {
public:
explicit Lock(Mutex *pm) // 
: mutexPtr(pm, unlock) // mutexPtr销毁时会调用unlock,而不是delete
{ 
lock(mutexPtr.get()); // item15会介绍"get"
}
private:
std::tr1::shared_ptr<Mutex> mutexPtr; 
};
    1. 移交所有权。 类似于auto_ptr的实现,a=b则b不再占有该资源。
    1. 彻底地复制。通常需要深复制,确保不会出现差错。

15. 提供访问原始资源的函数

将资源封装成类之后,经常有必要提供一个对原始资源的访问函数:因为有可能会用到这些原始资源。(书中对这部分讲得比较啰嗦,我感觉其实不需要多解释这个问题)

不过一个需要思考的问题是:原始资源以怎样的方式暴露在外?
以下几个方法值得考虑:

  1. 使用一个get函数
  2. 重载对象的行为,使之和所管理的资源一致。
  3. 重载强制转换函数,使得封装对象可以转换为原始资源。这里需要注意是否允许隐式转换。

当然这几点并没有最佳做法,关键是视资源的使用环境而定。

16. new 和delete要匹配

如果是new int[10], 对应delete []
如果是new int, 对应delete.

一定要注意匹配。

如果使用了typedef,一个变量可能看起来是单个元素实际上是一个数组,delete时尤其要注意。

17. 在使用对象封装资源时请使用独立的语句

先看一个反例:

/*
已知函数:
int priority();
processWidget(std::tr1::shared_ptr<Widget> widget, int priority);
*/
processWidget(std::tr1::shared_ptr<Widget>(new Widget), priority());

这一行代码可以通过编译,但其实是比较危险的。原因在于写在函数参数中的语句并没有确定的执行顺序

在执行processWidget函数时,实际的调用顺序可能是:

  • new Widget
  • priority()
  • std::tr1::shared_ptr(Widget* )
  • processWidget( param*)

假如priority()函数抛出异常,new Widget的资源就被泄漏了。

所以正确的写法是:

std::tr1::shared_ptr<Widget> pw(new Widget); 
processWidget(pw, priority()); 
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值