Effective C++学习笔记之第三章

Chapter 3 资源管理

item13:以对象管理资源
以对象管理资源有两个关键的想法:
1)获得资源后立刻放进管理对象(managing object)内。实际上这个以"以对象管理资源"的观念常被称为"资源取得时机便是初始化时机"(Resource Acquisition Is Initialization;RAII)。因为我们几乎总是在获得一笔资源后于同一语句内以它来初始化某个管理对象,或者说用获得的资源来赋值(而非初始化)。不管怎样都是把获得的资源立刻放到管理对象中。
2)管理对象(managing object)运用析构函数确保资源被释放。不论控制流如何离开区块,一旦对象被销毁(即对象离开作用域),其析构函数就会被调用,然后释放资源。如果在资源释放的时候抛出异常,就比较麻烦,其处理的方法在item8中有介绍。
这里介绍的RAII有auto_ptr和std::tr1::shared_ptr两种。由于管理对象被销毁时会自动删除它所指之物,所以一定要注意如果有多个指针指向同一个对象的问题。这两者在这上面的处理是不相同的。
★auto_ptr:auto_ptr是不允许有多个指针指向同一个对象。为了预防同样个资源被删除了两次以上,auto_ptr规定,若通过copy constructor或者copy assignment复制它们时,它们会变成null,而复制所得到的指针将取得资源的唯一拥有权。但是这种复制的行为看起来很诡异。
★std:tr1:shared_ptr是一种"引用计数型智慧指针"(reference-counting smart pointer;RCSP),也就是说,这个指针会持续追踪共有多少个对象指向某笔资源,并在无人指向它是自动删除该资源。有点类似于垃圾回收(garbage collection)机制,不同的是RCSPs无法打破环状引用。比如说两个其实已经没有被使用的对象彼此互指,看起来好像还处在"被使用"状态。不过较前者是更佳选择。
另外需要注意的是:auto_ptr和tr1::shared_ptr两者都在其析构函数内做delete而不是delete[]的动作。那意味着将动态分配的array转交给auto_ptr或tr1::shared_ptr是不可行的,但是这样是可以通过编译的。对应于array也有类似的解决方案,见item55,不过现在还没看啦~

item14:在资源管理类中小心coping行为

当一个RAII对象被复制,会发生什么事?大多数情况下,会选择以下两种可能:
1)禁止复制。许多时候允许RAII对象被复制并不合理。如果复制动作对RAII class并不合理,那就禁止它。item6中说了该怎么做:将coping操作声明为private。
2)对底层资源进行"引用计数"(reference-count)。有时候我们希望保有资源,知道它的最后一个使用者(某对象)被销毁。这种情况下复制RAII对象时,应该将资源的"被引用数"递增。tr1::shared_ptr就是如此。如果对同步异步的线程来说,可能当计数器为0时,我们想要的释放动作是解除锁定而不是删除。当然,幸运的是tr1::shared_ptr允许指定所谓的"删除器"(deleter),是一个函数或函数对象。当引用次数为0时便被调用。删除器对tr1::shared_ptr构造函数而言是可有可无的第二参数,所以代码看起来就像:

mutexPtr(pm, unlock);
private:
    std::tr1::shared_ptr<Mutex> mutexPtr;

3)复制底部资源 。只要有需要,可以针对一份资源拥有其任意数量的复件(副本)。而我们需要"资源管理类"的唯一理由是,当不再需要某个复件时确保它被释放。在此情况下复制资源管理对象,应该同时也复制其所包覆的资源,也就是说,复制资源管理对象时,进行的是"深度copy",即不论指针还是其所指的内存都被制作出了一个复件。
4)转移底部资源的拥有权。某些罕见的场合下可能希望确保永远只有一个RAII对象指向一个原始资源(raw resource),即使RAII对象被复制也是如此。此时资源的拥有权就会从被复制物转移到目标物。其实就是auto_ptr所奉行的复制的标准。
当然还有其他的复制行为也是可行的。

Item15:在资源管理类中提供对原始资源的访问
1)API经常需要访问原始的资源,所以每个RAII类都应该提供一个取得它所管理的资源的方法。如果想通过RAII(resource acquisition is initialization)(如std::tr1::shared_ptr和auto_ptr)来访问原始资源,则有两种形式:显式转换和隐式转换。显式转换的优点是不容易出错,比如说加入一个get函数来获得原始资源,但是用起来形式上没有那么直观和自然。隐式转换相对来说形式上看起来要自然一些,并且用起来会很方便,但是不安全。它通过重载取值操作符(解引用符)(operator ->或operator *)或者提供一个隐式转换的函数,有时候其实你不需要隐式转换,但是它却自己发生了,详见中文版P104。
这里有必要明白一点,显式转换函数和隐式转换函数是怎么定义的。比如定义了一个ClassManage的类,其中的Resource变量即原始资源变量。
显式转换函数:ClassManage get() const {return Resource;}
隐式转换函数:operator ClassManage() const {return Resource;}
2)如果这样提供了一个取得资源的方法,会不会就是破换了类的封装性呢?其实不是,RAII类不是为了封装什么而存在,它是为了保证它所管理的资源被正确的释放掉。所以,如果需要的话,可以把封装性放在RAII类的首位,但是没有必要。其实RAII类是对它的某些实现实行了严格的封装,比如说它的计数器,而对底层的资源实行了比较松散的封装,这样使得用户在需要的时候可以访问到资源。这种设计非常好,既隐藏了用户不需要看到的东西,又提供了必要的资源的接口。

item16:成对使用new和delete时要采用相同的形式
1)我们在new一个object或者new一个array时,在内存中的layout是有区别的。如果是单个的object,在内存中可能就直接存放了,但如果是一个array,那么在前面就可能包含了size of array的信息。如下所示:
single object:object
array:n | object | object | object |....
在delete的时候,是根据需要释放的object的个数来invoke对应数量的destructor。所以如果我们对一个单个的对象使用delete [],或者对一个array使用delete,这样的结果都是undefined。
2)在使用typedef的时候,需要特别注意,如果:
typedef std::string AddressLines[4];//相当于每个人的地址有4行,每行是一个string变量。这样AddressLine就是一个array。
std::string *pal = new AddressLines;//注意到new AddressLine返回的是一个string*,跟"new string[4]" 返回的是一样的。
那么:delete pal;//undefined
delete [] pal; //fine
所以在需要用到typedef的时候,应该尽量避免,因为这个很容易出错。相应的,我们可以用STL中的string或者vector模板,这样更安全,更方便。

item17 :以独立的语句将newed对象置入智能指针
1)将newed对象置入智能指针,是方便我们对资源进行管理,防止resource leak。但是如果没有用独立的语句来完成这个操作的话,当有异常抛出的时候,仍可能发生resource leak。下面用一个例子来深入解析。如果定义了两个函数如下:
int priority();//取得优先级的函数
void processWidget(std::tr1::shared_ptr<Widget> pw,int priority);//按照优先级做出响应的函数
那么使用processWidget(new Widget,priority());将无法编译,因为str::shared_ptr的接受一个原始资源指针的constructor是explicit,所以不能进行隐式的类型转换,so,必须写成如下形式方可编译:

processWidget(std::tr1::shared_ptr<Widget>(new Widget),priority());虽然我们这里用了资源管理类,但还是有可能造成资源泄露。原因如下:
由于("std::tr1::shared_ptr<Widget>(new Widget)")这个语句必须要拆分成两步:先执行"new Widget",再调用tr1::shared_ptr的构造函数。因为前者是后者的一个实参。所以在processWidget调用之前,必须做三件事
1、调用priority函数
2、执行"new Widget"
3、调用tr1::shared_ptr的构造函数
但是在C++中是以什么顺序完成这些事情呢?这和其他语言如Java和C#不同,那两种语言总是以特定的次序完成函数参数的核算。但在这里,priority的调用可以放在第一或第二或第三。如果编译器选择以第二的顺序执行它(如果这样可以生成更高效的代码),最终的操作顺序就是这样的:
1、执行"new Widget"
2、调用priority函数
3、调用tr1::shared_ptr的构造函数
如果这时在priority的执行中导致异常,那么"new Widget"所返回的指针就遗失了,因为它还没有来得及转交给智能指针,所以也不会有对应的资源释放的动作。所以如果以独立的语句将这两个动作和在一起,比如:
std::tr1::shared_ptr<Widget> pw(new Widget);
processWidget(pw, priority());
这样,priority的调用就不能介入到它们中间成为"第三者"了,so,没有小三的世界才是和谐的。要想尽办法避免小三的介入。


 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值