C++进阶_Effective_C++第三版(三) 资源管理 Resource Management

资源管理 Resource Management

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

13、以对象管理资源

Use objects to mangage resources.
假设有一个用来模拟投资行为的程序库,其他各式各样的投资类型继承自一个class Investment,然后这个程序库通过一个工厂函数创建特定的Investment对象:

class Investment {};  //继承体系的基类
Investment* createInvestment();  //返回指针,指向动态分配对象。

对于上述工厂函数的调用者有责任删除它:

void f()
{
	Investment* pInv = createInvestment();  //调用factory函数//一些操作
	delete pInv;					//释放pInv所指对象
}

上述代码有很多情况无法删除它得自createInvestment的对象:如“…”区域内的一个过早的return语句,或者某个循环里的continue或goto语句的过早退出,“…”区域内语句抛出异常。对于delete被略过,将泄漏对象所保存的任何资源。为解决此问题,可以将资源放进对象内,利用C++的析构函数自动调用机制确保资源被释放。标准库提供的auto_ptr正是针对这种形势而设计的。auto_ptr是个类指针(pointer-like)对象,也就是所谓的智能指针,其析构函数自动对齐所指对象调用delete。使用如下:

void f()
{
	std::auto_ptr<Investment> pInv(createInvestment());}

此实现利用两个关键想法:获得资源后立刻放进管理对象内(“以对象管理资源”又称“资源取得时便是初始化时”(Resource Acquisition Is Initialization:RAII)),管理对象运用析构函数确保资源被释放。
由于auto_ptr被销毁时会自动删除它所指之物,所以一定要注意不能让多个auto_ptr同时指向同一个对象。为了预防此问题,auto_ptrs有一个性质,若通过copy构造函数或copy assignment操作符复制它们,它们会变成null,而复制所得的指针将取得资源的唯一拥有权:

std::auto_ptr<Investment> pInv1(createInvestment());  // pInv1指向返回的对象
std::auto_ptr<Investment> pInv2(pInv1);	// pInv2指向对象,pInv1被设为null
pInv1 = pInv2;				// pInv1指向对象,pInv2被设为null

但是如此受auto_ptr管理的资源必须绝对没有一个以上的auto_ptr同时指向它,但是比如对于STL容器要求其元素发挥正常的复制行为,此时无法使用auto_ptr。auto_ptr的替代方案是“引用计数型智慧指针”(references-counting smart pointer:RCSP)。所谓的RCSP也是个智能指针,持续追踪共有多少对象指向某个资源,并在无人指向它时自动删除该资源。其无法打破环状引用(cycles of references,例如两个其实已经没被使用的对象彼此互指)。
TR1的tr1::shared_ptr就是个RCSP,所以可以这样实现f:

void f()
{
	std::tr1::shared_ptr<Investment> pInv(createInvestment());//调用factory函数}	//经由shared_ptr析构函数自动删除pInv

看起来和auto_ptr差不多,但是shared_ptr复制行为就很正常:

void f()
{
	std::tr1::shared_ptr <Investment> pInv1(createInvestment());  // pInv1指向返回的对象
	std::tr1::shared_ptr <Investment> pInv2(pInv1);	// pInv1和pInv2指向同一个对象,
	pInv1 = pInv2;				// pInv1和pInv2指向同一个对象。}   // pInv1和pInv2被销毁,它们所指的对象也被自动销毁。
  • 为防止资源泄漏,请使用RAII对象,它们在构造函数中获取资源并在析构函数中释放资源。
  • 两个常被使用的RAII
    classes分部是tr1::shared_ptr和auto_ptr。前者通常是较佳选择,因为其copy行为比较直观。若选择auto_ptr复制动作会使被复制物指针指向null。

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

Think carefully about copying behavior in resource-mangaging classes.
对于有些不是heap-based资源,像tr1::shared_ptr和auto_ptr这样的智能指针往往不适合作为资源掌管者,有可能需要建立自己的资源管理类。比如使用C API函数处理类型为Nutex的互斥器对象(mutex object),共有lock和unlock两个函数可用:

void lock(Mutex* pm); 		//锁定pm所指的互斥器
void unlock(Mutex* pm);	//将互斥器解除锁定
为确保绝对不会忘记将一个被锁住的Mutex解锁,需要建立一个class用来管理互斥锁,使用RAII守则:
class Lock{
public:
	explicit Lock(Mutex* pm):mutexPtr(pm)
		{	lock(mutexPtr);	}	//获得资源
	~Lock() {	unlock(mutexPtr);	}	//释放资源
private:
	Mutex * mutexPtr;
};

客户对Lock的用法符合RAII方式:

Mutex m;		//定义需要的互斥器{			//建立一个区块用来定义critical section
	Lock m1(&m)	//锁定互斥器// 执行critical section内的操作
}		//	在区块最末尾自动解除互斥器锁定

但是对于如果复制m,此时可以的处理如下:
禁止复制。很多时候对于允许RAII对象被复制并不合理,比如像Lock这样的class,因为很少能够合理拥有“同步化基础器物”的副本。所以应该将copying操作声明为private。或者如下实现:

class Lock: private Uncopyable{	//禁止复制
public:};

对底层资源使用“引用计数法”。可以使用tr1::shared_ptr,因为其允许指定所谓的删除器,可以是一个函数或者函数对象,当引用次数为0时便被调用,删除器对tr1::shared_ptr构造函数而言是可有可无的第二参数。如此可以如下实现:

class Lock{
	public:
		explicit Lock(Mutex* pm):mutexPtr(pm, unlockj)
		{	lock(mutexPtr.get());}//以Mutex初始化shared_ptr,并以unlock函数为删除器
private:
	tr1::shared_ptr<Mutex> mutexPtr; //使用shared_ptr替换raw pointer
};

此时不需要声明构造函数,因为不需要,因为在引用次数为0时自动调用tr1::shared_ptr的删除器。

  • 复制RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定RAII对象的copying行为。
  • 普遍而常见的RAII class copying行为是:抑制copying、实行引用计数法(references counting)。

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

Provide access to raw resource in resource-managing classes.
资源管理类(resource-managing classes)很强大,可以避免很多资源泄漏,但是许多API需要直接访问原始资源,这样需要绕过资源管理对象直接访问原始资源。例如,使用智能指针保存factory函数如createInvestment的调用结果:

std::tr1::shared_ptr <Investment> pInv(createInvestment());

某个函数处理Investment对象:

int daysHeld(const Investment* pi);

调用它:

int days = daysHeld(pInv);//错误

此时,代码会编译不过,因为daysHeld需要的是Investment*指针,但是传递给的是std::tr1::shared_ptr 的对象。这时候需要一个函数将RAII对象转换为其所内含的原始资源。有两个做法:
tr1::shared_ptr和auto_ptr都提供一个get成员函数,用来执行显式转换,也就是会返回智能指针内部的原始指针:

int days = daysHeld(pInv.get());

和所有的智能指针一样tr1::shared_ptr和auto_pt也重载了指针取值(pointer dereferencing)操作符(operator->和operator*),它允许隐式转换至底部原始指针:

class Investment{
public:
	bool isTaxFree() const;};
Investment* createInvestment();	//factory函数
std::tr1::shared_ptr <Investment> pi1(createInvestment());//tr1::shared_ptr管理一笔资源
bool taxable1 = !(pi1->isTaxFree());	//经由operator->访问资源
std::tr1::shared_ptr <Investment> pi2(createInvestment());//tr1::shared_ptr管理一笔资源
bool taxable2 = !((*pi2).isTaxFree());	//经由operator*访问资源

由于有时候的必须取得RAII对象内的原始资源,可以提供一个隐式转换函数。例如下面用于字体的RAII class(对C API而言字体是一种原始数据结构):

FontHandle getFont();    //C API
void releaseFont(FontHandle fh);    //C API
class Font {
public:
	explicit Font(FontHandle fh):f(fh)	//获得资源,采用pass-by-value
	{	}
	~ Font() {		releaseFont(f);		}  //释放资源
private:
	FontHandle f;		//原始字体资源
};

假设由大量与字体相关的C API,它们处理的是FontHandles,那么将Font对象转换为FontHandle会是一个很频繁的需求,可以提供一个显式转换函数,像get一样,但是这样每当使用API时就必须调用get,如此大规模使用,代码可读性变差,而且增加了泄漏字体的可能性,而设计Font class的主要设计目的时为了防止资源(字体)泄漏。所以提供一个隐式转换函数,转型为FontHandle:

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

如下使用:

Font f(getFont());
int newFontSize;changeFontSize(f, newFontSize);  //将Font隐式转换为FontHandle
但是如此做会增加错误发生机会,如在需要Font时意外创建一个FontHandle:
Font f1(getFont());
…
FontHandle f2 = f1;		//原意要拷贝一个Font对象,
//却反而将f1隐式转换为其底部的FontHandle,然后才复制它。

上述程序的FontHandle由Font对象f1管理,但是FontHandle也可通过直接使用f2取得。这样如果当f1被销毁,字体被释放,而f2会成为虚吊的(dangle)。

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

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

Use the same form in corresponding uses of new and delete.
例如以下代码:

std:: string* stringArray = new std:: string[100];delete stringArray;

看起来程序写的很没啥问题,但是stringArray含有100各string对象中的99各不太可能被适当的删除,因为它们的析构函数很可能没被调用。
当使用new(通过new动态生成一个对象),由两种情况,第一,内存被分配处理啊,第二,针对此内存会有一个(或更多)构造函数被调用。当使用delete也有两件事发生,针对此内存会有一个(或更多)析构函数被调用,然后内存才被释放。
当对也给指针使用delete,唯一能够让delete知道内存中是否存在一个“数组大小记录”的办法就是,由调用者告诉它,使用delete时加上中括号,delete便认定指针指向一个数组,否则便认定指针指向单一对象:

std::string* stringPtr1 = new std::string;
std::string* stringPtr2 = new std::string[100];delete stringPtr1;	//删除一个对象
delete [] stringPtr2;	//删除一个由对象组成的数组

对于使用new创建typedef类型的对象时,要特别注意原始类型,防止出现错误。因为C++标准程序库含有string,vector等templates,可将数组的需求降至几乎为零。

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

17、以独立语句将newed对象置入智能指针
Store newed objecs in smart pointers in standalone statements.
假设有个函数用来指示处理程序的优先权,另一个函数用来在某些动态分配所得的Widget上进行某些带有优先权的处理:

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

根据以对象管理资源的思路,processWidget决定对其动态分配得来的Widget使用智能指针。如下调用processWidget:

processWidget(new Widget, priority());	//这样参数类型不一致无法编译
processWidget(std::tr1::shared_ptr<Widget>(new Widget), priority());//可以通过编译了。

但是在此使用对象管理式资源,调用却可能泄漏资源。编译器产出一个processWidget调用码之前,必须首先核算即将被传递的各个实参。上次第一个实参std::tr1::shared_ptr(new Widget)由两部分组成:执行new Widget表达式;调用std::tr1::shared_ptr构造函数。所以在调用processWidget之前,编译器必须创建代码,做以下三件事:调用priority;执行new Widget表达式;调用std::tr1::shared_ptr构造函数。但是编译器执行的顺序不一定时上述理想的方式,如果是执行new Widget表达式;调用priority;调用std::tr1::shared_ptr构造函数,在对priority调用时导致异常,此时new Widge的指针将会遗失,因为还没有被置入std::tr1::shared_ptr内,此时可能引发资源泄漏。为避免此问题,分离语句:

std::tr1::shared_ptr<Widget> pw(new Widget);
processWidget(pw, priority());
  • 以独立语句将newed对象存储于(置于)智能指针内,如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄漏。

上一篇: C++进阶_Effective_C++第三版(二) 构造/析构/赋值运算 Constructors,Destructors,and Assignment Operators
下一篇: C++进阶_Effective_C++第三版(四) 设计与声明 Designs and Declarations

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
有人说c++程序员可以分成两类,读过effective c++的和没读过的。世界顶级c++大师scott meyers成名之作的第三版的确当得起这样的评价。当您读过这本书之后,就获得了迅速提升自己c++功力的一个契机。.(2-1)   在国际上,本书所引起的反响,波及整个计算技术出版领域,余音至今未绝。几乎在所有c++书籍的推荐名单上,本书都会位于前名。作者高超的技术把握力、独特的视角。诙谐轻松的写作风格、独具匠心的内容组织,都受到极大的推崇和仿效。这种奇特的现象,只能解释为人们对这本书衷心的赞美和推祟。《effective c++》前两个版本的确抓住了全世界无数程序员的目光。原因十分显明:scott meyers 极富实践意义的c++ 研讨方式,描述出专家用以产出干净、正确、高效代码的经验法则和行事法则——也就是他们几乎总是做或不做的某些事。   这本书不是读完一遍就可以束之高阁的快餐读物,也不是用以解决手边问题的参考手册,而是需要您去反复阅读体会的,c++是真正程序员的语言,背后有着精深的思想与无与伦比的表达能力,这使得它具有类似宗教般的魅力。希望这本书自瞄帮助您跨越c抖的重重险阻,领略高处才有的壮美风光,做—个成功而快乐的c++程序员。...      本书一共组织 55 个准则,每一条准则描述一个编写出更好的 c++ 的方式。每一个条款的背后都有具体范例支撑。第三版有一半以上的篇幅是崭新内容,包括讨论资源管理和模板(templates)运用的两个新章。为反映出现代设计考虑,对第二版论题做了广泛的修订,包括异常(exceptions)、设计模式(design patterns)和多线程(multithreading)。      《effective c++》的重要特征包括:    * 高效的 classes、functions、templates 和inheritance hierarchies(继承体系)方面的专家级指导。    * 崭新的 "tr1" 标准程序库功能应用,以及与既有标准程序库组件的比较。    * 洞察 c++和其他语言(例如java、c#、c)之间的不同。此举有助于那些来自其他语言阵营的开发人员消化吸收 c++ 式的各种解法。(2-1)
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

风华一叶知秋

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值