您的位置:首页 > 其它

第三章 资源管理

2014-08-07 22:47 183 查看
所谓资源就是,一旦用了它,将来必须还给系统。如果不这样,糟糕的事情就会发生。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对象存储于(置入)智能指针内.如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源

泄露.
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: