第三章 资源管理

      所谓资源就是,一旦用了它,将来必须还给系统。如果不这样,糟糕的事情就会发生。C++程序中最常见使用的资源就是动态分配内存,导致的内存泄漏,但内存只是你必须管理的众多资源之一。其他常见的资源还包括文件描述器(file descriptors)、互斥锁(mutex locks)、图形界面中的字型和笔刷、数据库连接、以及网络sockets.不论哪一种资源,重要的是,当你不再使用它时,必须还给系统。

 

条款13 以对象管理资源

    许多资源被动态分配与heap内而后被用于单一区块或函数内,他们应该在控制流离开那个区块或者函数时被释放。标准库通的auto_ptr正是针对这种形势特制的。auto_ptr是个“类指针对象”,也就是所谓的智能指针,其析构函数自动对其所指对象调用delete。用法如下:

 C++ Code 
1
2
3
4
5
void f()
{
    std::auto_ptr<Investment> pInv(createInvestment());
     //调用工厂函数,一如既往的使用pInv,经由auto_ptr的析构函数自动删除pInv
}
     根据上面的例子,我们对“以对象管理资源”的两个关键想法。

     获得资源后立刻放进管理对象内。(RAII:资源获取实际便是初始化时机)

     管理对象运行析构函数确保资源被释放。

    

上面给出了auto_ptr的智能指针有个问题就是,一定不能让多个auto_ptr同时指向同一个对象,原因在于auto_ptrs有一个不寻常的性质:若通过copy构造函数或copy assignment操作符复制它们,它们会变成null,而复制所得的指针将获得资源的唯一拥有权,见如下代码:

 C++ Code 
1
2
3
4
5
6
std::auto_ptr<Investment> pInv1(createInvestment());
//pInv1指向createInvestment返回对象
std::auto_ptr<Investment> pInv2(pInv1);
//pInv2指向createInvestment返回对象 ,pInv1为NULL
pInv1=pInv2;
//pInv1指向createInvestment返回对象,pInv2为NULL

      解决方案二:

auto_ptr的替代方案是“引用计数智慧指针(RCSP)”,所谓的RCSPs也是智能指针,持续跟踪共有多少个对象指向某笔资源,并在无人指向它时自动删除该资源。RCSPs提供的行为类似垃圾回收,不同的是RCSPs无法打破环状引用,具体用法如下所示

    

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
void f()
{
    std::tr1::shared_ptr<Investment> pInv1(createInvestment());
     //pInv1指向createInvestment返回对象
    std::tr1::shared_ptr<Investment> pInv2(pInv1);
     //pInv2和pInv1同一个对象
    pInv1 = pInv2;
     //同上
    ...
     //pInv2和pInv1b被销毁
     //他们所指的对象同样被销毁
}


不适用情况:

     auto_ptr和tr1::shared_ptr两者都在其析构函数内做delete而不是delete[]动作,因此动态分配而得到的array身上适用这两个智能指针是个坏主意。

            

 C++ Code 
1
2
std::auto_ptr<std::string> aps( new std::string[ 10]);   //坏主意,能编译
std::tr1::shared_ptr< int> spi( new  int[ 1024]);  //相同问题


请记住:

    为了防止资源泄漏,请使用RAII对象,它们在构造函数中获得资源并在析构函数中释放资源。

   两个常用的RAII分别是tr1::shared_ptr和auto_ptr。前者通常是较佳的选择,因为其copy行为比较直观。若选择auto_ptr,复制它动作是它指向null。


条款14 在资源管理类中小心copying行为

 有时候,我们需要自己建立资源管理类,在这个时候,我们需要特别小心copying行为。如下例子:
 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Lock
{
public:
     explicit Lock(Mutex *pm):mutexPtr(pm)
    {
     lock(mutexPtr); //获得资源
    }
    ~Lock()
    {
     unlock(mutexPtr);   //释放资源
    }
private:
    Mutex *mutexPtr;
}
//定义互斥器
Mutex m;
...              //建立一个区块用来定义critical section
Lock m1(&m);     //锁定互斥器
...              //执行critical section 内的操作、在区块最末尾,自动解除互斥器锁定

///如果Lock对象被复制,会发生什么呢?
Lock m11(&m);  //锁定m
Lock m12(m11);   //复制
      这种复制行为会为程序带来不确定性。因为我们在进行copying行为时,应该要有以下四重考虑:
1、许多时候允许RAII对象被复制并不合理。对一个像Lock这样的类这是可能的,因为很少能够合理拥有“同步化基础物”的副本。如果复制对RAII并不合理,你应该禁止复制。条款6告诉我们怎么做:将copying操作声明为private,对Lock而言看起来是这样的:
      
 C++ Code 
1
2
3
4
5
class Lock :  private Uncopyable //禁止复制
{
public:
    ...
};

2、 有时候我们希望保持资源,直到它最后一个使用者被销毁,这种情况下复制RAII对象时,应该将资源的“被引用数”递增,trl::shared_ptr便是如此。

通常只要内含一个tr1::shared_ptr成员变量,RAII类便可实现出引用计数行为。如果前述Lock打算使用使用技术,它可以改变mutexPtr类型,将它从Mutex*改为tr1::shared_ptr<Mutex>。然而不幸tr1::shared_ptr的缺省行为是“当引用次数为0时删除其所指物”,那不是我们所要的行为。幸运的是tr1::shared_ptr允许指定所谓的“删除器”,那是一个函数或函数对象,当引用次数为0时便调用。删除器对tr1::shared_ptr构造函数而言是可有可无的第二参数,所以代码看起来像这样:

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
class Lock
{
public:
     explicit Lock(Mutex *pm) //以某个Mutex初始化shard_ptr并以unlock函数
        : mutexPtr(pm, unlock)  //为删除器
    {
        lock(mutexPtr.get()); //条款15谈到 get
    }
private:
    std::tr1::shared_ptr<Mutex> mutexPtr;  //使用shared_ptr
};

本例的Lock不再声明析构函数,因为没有必要。条款5说过,类的析构函数会自动调用其non_static成员变量的析构函数。而mutexPtr的析构函数会在互斥器的引用次数为0的时候自动调用tr1::shared_ptr的删除器。

3.复制底层资源

有时候只要你喜欢,可以针对一份资源拥有其任意数量的副件,而你需要“资源管理类”的唯一理由是,当你不再需要某个副本的时候确保它被释放,在此情况下复制资源管理对象,应该同时也复制其所包覆的资源,也就是说,复制资源管理了对象时,进行的是“深度拷贝”。

4.转移底部资源的拥有权

某些罕见场合下你可能希望确保永远只有一个RAII对象指向一个未加工资源,即使RAII对象被复制依然如此。此时资源的拥有权会从被复制物转移到目标物。如条款13所诉。这是atuo_ptr奉行的复制意义。

 

copying函数有可能被编译器自动创建出来,因而除非编译器所生成版本做了你想要做的事情,否则你的自己编写它们。某些情况下你或许也想支持这些函数版本,这样的版本描述于条款45

 

请记住:

复制RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定RAII对象的copying行为。

普通而常见的RAII类copying行为是:抑制copying,施行引用计数法,不过其他行为都可能被实现。


条款15 在资源管理类中提供对原始资源的访问

1.如何访问原始资源

    在上两条款我们知道如何使用智能指针管理我们的申请的资源,但是读者是否发现,我们如何去访问我们原始资源的方法呢?在这一条款将得到解答以及类型转换的相关问题。
    首先,我们用代码来说话吧,及输出的结构看到
 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// pro_acce.cpp : 定义控制台应用程序的入口点。
//2011/9/21  by wallwind on sunrise;
#include  "stdafx.h"
#include <iostream>
#include <memory>
using  namespace std;
class myClass
{
public :
    myClass()
    {
        cout<< "myClass()"<<endl;
    }
    ~myClass()
    {
        cout<< "~myClass()"<<endl;
    }
     void printFunc()
    {
        cout<< "printFunc()"<<endl;
    }
int getType() const
    {
         return type;
    }
private:
     int type; 
};
myClass* creatMyClass()
{
    myClass *my= new myClass();
     return my;
}
void issue(myClass *my) { 
     delete my;

int _tmain( int argc, _TCHAR* argv[])
{
    auto_ptr<myClass> apMy(creatMyClass());
    myClass* myC=apMy.get(); //auto_ptr 给我们提供的函数,用来访问原始资源
    myC->printFunc(); //调用了myClass的方法
     return  0;
}

从这里我们可以看到,程序输出了我们想要的结果。如书中所述:

tr1::shared_ptr 和 auto_ptr 都提供了一个 get 成员函数来进行显式转换,也就是说,返回一个智能指针对象中的裸指针(的副本):

myClass* myC=apMy.get();//

    似乎所有的智能指针类,包括 tr1::shared_ptr 和 auto_ptr 等等,都会重载指针解析运算符( operator-> 和 operator* ),这便使得对原始裸指针进行隐式转换成为现实,在这里我就不实际举例子了。下面使用书中的片段代码来说明一下问题吧:

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
std::tr1::shared_ptr<Investment> pi1(createInvestment()); 
// 使用 tr1::shared_ptr 
// 管理资源 
bool taxable1 = !(pi1->isTaxFree()); 
// 通过 operator-> 访问资源 
... 
std::auto_ptr<Investment> pi2(createInvestment()); 
// 使用 auto_ptr 管理资源 
bool taxable2 = !((*pi2).isTaxFree()); 
// 通过 operator* 访问资源
出色的资源管理类型可以避免资源泄露并有效的管理资源,但世界并非是如你所愿的。当某个API需要使用资源管理类型把持的原始资源时,这样的麻烦又会随之而来。

   也许您注意到了

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
void issue(myClass *my) 
        { 
         delete my;
        }  
//这个函数没有使用到,那么当你使用这个时候
int _tmain( int argc, _TCHAR* argv[])
{
    auto_ptr<myClass> apMy(creatMyClass());
    issue(apMy.get());
     return  0;
}
就出现了问题。结果大家可以试一下。

2.隐式转换
 
    因为有时候要取得RAII对象内原始资源, RAII设计者使用了一种隐式转换函数,

 

如果让资源管理类型提供隐式转换函数,可以让行为变的更自然,但这样的作法没有好下场,只会增加客户端发生错误的机率。比如下面的代码(简单的编写了一个自定义的AutoPtr,它重载了隐式转换operator):

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include  "stdafx.h"
#include <stdlib.h>
#include <memory>
#include <string>
using  namespace std;

template< typename T>
class AutoPtr
{
public:
    AutoPtr(T *tP)
        : _tP(tP), _released( false) { }
    ~AutoPtr()
    {
        Release();
    }
    T * operator->()
    {
         return _tP;
    }
     operator T *()  const
    {
         return _tP;
    }
     void Release( void)
    {
         if (!_released)
        {
             delete _tP;
            _released =  true;
        }
    }
private:
    T *_tP;
     bool _released;
};

typedef  struct Point
{
     double X;
     double Y;
};

void PrintPoint(Point *pTP) { }

int _tmain( int argc, _TCHAR *argv[])
{
    AutoPtr<Point> apI( new Point());
    PrintPoint(apI);
    Point *pTP = apI;
    apI.Release();
    system( "pause");
     return  0;
}

pTP也获得了apI提供的Point指针,当apI作用域结束后或显示调用了Release方法,pTP也就成了迷途指针了。通常来说提供Get方法是比较大众切容易接受的正确方法,它将隐式转换所带来的恶意最小化。
也许在诸如auto_ptr、shared_ptr只提供原始指针的访问违背了面向对象的封装性,可能是令人产生误解和迷惑的地方。因为RAII classes并不是为了封装某物而存在的,更多的它只是一种模版编程的概念。

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
#include  "stdafx.h"

#include <stdlib.h>

#include <memory>

#include <string>

using  namespace std;



template< typename T>

class AutoPtr
{

public:

    AutoPtr(T *tP)

        : _tP(tP), _released( false) { }

    ~AutoPtr()
    {

        Release();

    }

    T * operator->()
    {

         return _tP;

    }

     operator T *()  const
    {

         return _tP;

    }

     void Release( void)
    {

         if (!_released)
        {

             delete _tP;

            _released =  true;

        }

    }

private:

    T *_tP;

     bool _released;

};



typedef  struct Point
{

     double X;

     double Y;

};



void PrintPoint(Point *pTP) { }



int _tmain( int argc, _TCHAR *argv[])
{

    AutoPtr<Point> apI( new Point());

    PrintPoint(apI);

    Point *pTP = apI;

    apI.Release();

    system( "pause");

     return  0;

}
     pTP也获得了apI提供的Point指针,当apI作用域结束后或显示调用了Release方法,pTP也就成了迷途指针了。通常来说提供Get方法是比较大众切容易接受的正确方法,它将隐式转换所带来的恶意最小化。
     也许在诸如auto_ptr、shared_ptr只提供原始指针的访问违背了面向对象的封装性,可能是令人产生误解和迷惑的地方。因为RAII classes并不是为了封装某物而存在的,更多的它只是一种模版编程的概念。

牢记在心

l API 通常需要访问原始资源,因此每个 RAII 类都应该提供一个途径来获取它所管理的资源。

l 访问可以通过显式转换或隐式转换来实现。一般情况下,显式转换更安全,但是隐式转换对于客户端程序员来说使用更方便。

 

条款16 成对使用new和delete时要采用相同形式

原理还是:使用new,配对使用delete,使用new[],配对使用delete[] 。但是使用new[]时,采用delete呢?会导致析构函数少调用情况。
要有就是尽量少使用数组。因为C++标准库含有string、vector等template,可将数组的需求降至几乎为0.

条款17 以独立语句将newed对象置入智能指针

     c++编译器会优化你的代码,会根据优先权来选择优先执行哪些代码。C++编译器以什么样的此讯完成这些事情呢?弹性很大。这和其他诸如java,c#不同。她们总是以特定顺序来执行。
 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// store_new.cpp : 定义控制台应用程序的入口点。

#include  "stdafx.h"
#include <iostream>
#include <memory>
using  namespace std;
class Widget
{
public:
    Widget()
    {
        cout <<  "Widget()" << endl;
    }
    ~Widget()
    {
        cout <<  "Widget()" << endl;
    }
};
int priority()
{
     throw  new runtime_error( "Exception");
     //return 0;
}
void processWidger(auto_ptr<Widget> pw,  int priority) {}

int _tmain( int argc, _TCHAR *argv[])
{
    processWidger(auto_ptr<Widget>( new Widget()), priority());
    system( "pause");
     return  0;
}
Priority函数返回一个执行的优先级,  processWidger 函数则根据优先级来处理某个类型的对象(许多程序员有时候愿意将某个函数直接做为参数传递进另个函数内)。
1、调用X的构造函数。
2、调用auto_ptr< Widget >的构造函数。
3、调用Priority函数。
看上去井然有序的条件,C++编译器未必会选择这么做。也许编译器选择将调用Priority函数放在第二的位置会生成更高效的代码也说不定。那么顺序就会改为:
1、调用X的构造函数。
2、调用Priority函数。
3、调用auto_ptr< Widget >的构造函数。
那么如果调用Priority函数产生异常怎么办?auto_ptr并没有获得它需要保管的资源,而那段资源也不会遭到释放,有一种资源泄漏的方式。
解决方式就是在外面完成智能指针的存储,编译器对于跨越语句的各项操作不会选择重新排列。这样智能指针依然获得了对所指向资源的保护。
 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// store_new.cpp : 定义控制台应用程序的入口点。
#include  "stdafx.h"
#include <iostream>
#include <memory>
using  namespace std;
class Widget
{
public:
    Widget()
    {
        cout <<  "Widget()" << endl;
    }
    ~Widget()
    {
        cout <<  "Widget()" << endl;
    }
};
int priority()
{
     throw  new runtime_error( "Exception");
     //return 0;
}
void processWidger(auto_ptr<Widget> pw,  int priority) {}
int _tmain( int argc, _TCHAR *argv[])
{
    auto_ptr<Widget> pw( new Widget());
    processWidger(pw, priority());
    system( "pause");
     return  0;
}

由于编译器对"跨越语句块的各项操作"失去了执行重新排列的自由,所以编译器不能在它们之间任意选择执行顺序.

请记住:
■ 以独立语句将newed对象存储于(置入)智能指针内.如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源
泄露.



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值