《Effective C++》资源管理:条款13-条款15

在系统中,资源是有限的,一旦用完必须归还给系统,否则可能会造成资源耗尽或其他问题。例如,动态分配的内存如果用完不释放会造成内存泄漏。

这里说的资源不仅仅是指内存,还包括其他,例如文件描述符、网络连接、数据库连接、互斥锁等。

在任何情况下都要把不使用的资源归还系统是一件非常困难的事情。尤其是考虑到异常、函数内多重回传路径等。

基于对象的资源管理办法几乎可以消除资源管理的问题。下面介绍的方法也可以弥补一些不足。

条款13:以对象管理资源

现在假设用一个类来模拟投资行为,各种投资类都继承自一个基类Investment。

class Investment//基类
{
	……
};

用工厂模式来创建特定的投资对象

Investment* CreateInvestment()
{
	……
}

在一个作用域内使用

void fun()
{
	Investment* pInv=CreateInvestment();
	……
	delete pInv;//释放资源
}

这样看起来没有问题,但是任何情况下,都能确保执行“delete pInv”吗?

显然不是,如果在“……”提前返回,或者出现异常,都不会执行到“delete pInv”。当然,谨慎编写程序可以防止这些想到的问题,但是我们编写的代码可能被后人维护和修改。如果他人在中间加上一个return或者其他空余语句都有可能改变执行的流程。所以,单纯依靠delete来删除远远不够。

为了确保资源总是在出了作用域内被释放,可以用对象来控制资源。把对象建到栈上,当对象出了作用域时自然会析构,在这个析构函数释放资源。这就是对象管理资源的机制。

标准库里提供了一些指针,正是针对上面这种情况的。例如,使用auto_ptr来实现资源管理

void fun()
{
	Investment* pInv=CreateInvestment();
	……
	delete pInv;//释放资源
}

上面以资源管理对象的关键在于两点

1、RAII:资源获取即初始化(resource acquisition is initialization)。获取资源后立即放进对象内进行管理。

2、管理对象运用析构函数确保资源释放。管理对象是开辟在栈上面的,离开作用域系统会自动释放管理对象,自然会调用管理对象的析构函数。

auto_ptr指针会有资源的唯一使用权。当auto_ptr指针给其他指针赋值时,对资源的使用权将被剥夺。更多关于auto_ptr内容请参考这里

由于对资源的唯一使用权的这个行为使得auto_ptr使用比较受限。例如STL容器中的元素不能使用auto_ptr。

还有一种指针是引用计数器型智能指针(refercence-counting smart pointer;RCSP)。它会记录有多少个对象在使用资源,当使用资源的计数器为零时,就会释放资源。可以参考这里。在标准库里面是shared_ptr。

需要注意的时,auto_ptr和shared_ptr释放资源用的都是delete,而不是delete[],所以不能用于数组。关于数组方面的指针,有shared_array来对应。类似的还有scope_array,可以参考这里

最后还要说明一点:createInvestment返回的是未加工指针(raw pointer),调用者极易忘记释放,即便是使用智能指针,也要首先把CreateInvestment()返回的指针存储于智能指针对象内。后面的条款18将会就这个问题进行讨论。

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

在条款13中,讲述了资源获取即初始化(resource acquisition is initialization;RAII)的资源管理方法,并介绍了智能指针auto_prt和shared_ptr指针。上述方法管理的资源都是开辟在堆上(heap-based),但是并不是所有资源都是开辟在堆上,有时候我们需要自己建立资源管理类。

例如现在使用C语言API函数处理类型为Mutex的互斥器对象(mutex object),有lock和unlock两个函数可以用。

void lock(Mutex* mu);//加锁
void unlock(Mutex* mu);//解锁

为了确保给上锁的Mutex变量解锁,我们需要建立一个类来管理Mutex锁。这个类按照RAII方法来建立。

class Lock{
public:
	explicit Lock(Mutex* mu):mutexPtr(mu)
	{
		lock(mutexPtr);
	}
	~Lock()
	{
		unlock(mutexPtr);
	}
private:
	Mutex* mutexPtr;
};

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

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

上面是个一般性的例子。由此可以引出一个问题:当一个RAII对象被复制,会发生什么?

Lock m1(&m);
Lock m2(m1);

这是个一般性的问题。当RAII对象复制时,应该怎么做?通常有以下几种做法:

1、禁止复制

许多情况下,复制RAII对象并不合理。例如Lock类,这时候便可以禁止复制,只需将coping函数设为私有,条款6有讲述。

class Lock:private Uncopyable{
……

2、对管理资源使用引用计数法。

有时候我们希望保持资源直到最后一个使用者。这时RAII对象复制时,应该将持有资源的引用计数器加一。例如shared_ptr。

通常只需要在RAII class中包含一个shared_ptr成员变量便可实现引用计数的方法。如果上述Lock类使用引用计数器的话,只需把mutexPrt变为类型从Mutex*变为shared<Mutex>即可。但shared_ptr默认是当引用计数为0时,删除多指向对象,这不是我们想要的,我们想要的是调用unlock函数。幸运的是在shared_ptr中允许指定“删除器”,即引用计数为0时调用的函数。所以修改后如下:

class Lock:private {
public:
	explicit Lock(Mutex* mu):mutexPtr(mu,unlock)//以某个Mutex初始化,unlock作为删除其
	{
		lock(mutexPtr);
	}
private:
	shared_prt<Mutex> mutexPtr;
};

需要注意的是在这个类中并没有自己编写析构函数。因为mutexPtr是类中的普通成员变量,编译器会自动生成析构函数类析构这样的变量。这个在条款5中有说明。

3、复制底部资源。

使用资源管理类的目的是保证不需要这个资源时能正确释放。如果这种资源可以任意复制,我们只需编写好适当的copying函数即可。确保复制时是深度复制。关于深复制,参考这里

C++中的string类,内部是指向heap的指针。当string复制时,底层的指针指向的内容都会多出一份拷贝。

4、转移底层资源的拥有权。

有时候资源的拥有权只能给一个对象,这时候当资源复制时,就需要剥夺原RAII类对该资源的拥有权。像auto_ptr。在C++11新标准中的std::move便是这个功能。可以把一个左值转换为一个右值。

copying函数如果你不编写,编译器会帮你合成,其合成版本行为可参考条款5。要记住的是不论是自己编写还是编译器合成,都要符合自己资源管理类的需要。

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

使用资源管理类来屏蔽原始资源,对抗内存泄露等问题,避免使用原始资源(raw resources)。但是在现实中并不是这样,许多APIs要求使用原始资源。例如在条款13中,使用shared_ptr保存factory函数的返回值。

shared_prt<Investment> pInv=(createInvestment());

但是如果一个API是这样的

int dayHeld(const Investment* pi);

显然是无法使用shared_ptr对象的。这时候资源管理类需要一个函数,将管理的原始资源暴露出来(即返回原始资源的直接访问方法)。

杂shared_ptr和auto_ptr都提供一个get函数,用于执行这样的显示转换。这时如果在调用上面的API时:

dayHeld(pInv.get());

为了使智能指针使用起来像普通指针一样,它们要重载指针取值(pointer dereferencing)操作符(operator->和operator*),它们允许转换至底部原始指针。

例如Investment类中有个public函数isTaxFree:

class Investment{
public:
	bool isTaxFree() const;
	……
};
shared_prt<Investment> pi(CreateInvestment());
pi->isTaxFree();//通过operator->访问

(*pi).isTaxFree();//通过operator*访问

有时候我们必须使用RAII class内的原始资源。通常有以下两种做法。考虑下面一个例子:

FontHandle getFont();//C的API。为求简化省略参数
void releaseFont(FontHandle fh);

class Font{//RAII class
public:
	explicit Font(FontHandle fh):f(fh)
	{}
	~Font(){releaseFont(f);}
private:
	FontHandle f;
};

如果有大量的API,它们要求处理的是FontHandle,那么将Font转换为FontHandle是一个比较频繁的需求。这时可以提供一个显示的转换函数,像shared_ptr中get那样

class Font{
public:
	FontHandle get()const{return f;}
	……
};

这时,如果需要使用FontHandle,直接调用get函数即可。

如果到处都要使用get函数,这样看起来很难受,也增加了内存泄露的可能,因为我们把原始资源返回给其他API了。

还有一种方法是提供隐式转换

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

这样使用起来就比较自然。但是隐式转换可能在我们不希望发生的时候发生,例如

Font f1(getFont());
FontHandle f2=f1;//发生了隐式转换

在隐式转换中,把f1隐式转换为FontHandle,把底部资源给了f2。这样把f1的底层资源给了f2,如果f2释放,那么f1还不知情,这样就失去了资源管理的意义。

那么在实际中,是使用显示get这样的转换呢,还是使用隐式转换?

答案是取决于RAII class被设计用来执行那么工作,在哪些情况下使用。通常来说,get比较受欢迎,因为它避免了隐式转换带了的问题。

现在看来,RAII class内的返回资源的函数和封装资源之间有矛盾。的确是这样,但这样不是什么灾难。RAII class不是为了封装资源,而是为确保资源释放。当然可以在这个基本功能之上再加上一层封装,但是通常不是必要的。但是也有些RAII class结合十分松散的底层资源封装,以获得真正的封装实现。例如shared_ptr将引用计数器封装起来,但是外界很容易访问其所内含的原始指针。它隐藏了客户看不到的部分,但是具备客户需要的东西。良好的class就应该这样。

时间: 2024-10-20 10:00:10

《Effective C++》资源管理:条款13-条款15的相关文章

Effective C++ 条款13/14 以对象管理资源 || 在资源管理类中小心拷贝行为

三.资源管理       资源就是一旦你使用了它,将来不用的时候必须归还系统.C++中最常用的资源就是动态内存分配.其实,资源还有 文件描述符.互斥器.图形界面中的字形.画刷.数据库连接.socket等. 1.        以对象管理资源       void f() {     investment *plv = createInvestment();     //这里存在很多不定因素,可能造成下面语句无法执行,这就存在资源泄露的可能.     delete plv; }      这里我们

《Effective C++》学习笔记——条款15

***************************************转载请注明出处:http://blog.csdn.net/lttree******************************************** 三.Resource Management Rule 15:Provide access to raw resources in resource-managing classes 规则 15:在资源管理类中提供对原始资源的访问 1.引言 资源管理类(resou

effective c++ 条款13 use object to manage resources.

请求的系统资源需要最终还回系统,为了避免遗忘返还这个动作,可以利用析构函数在object销毁时自动调用的特点来实现. 简单说就是用object来管理资源. 以内存资源为例 class Investment {}; Investment* creatInvestment(){...} // factory function to produce investment object void main() { Investment* pInv = creatInvestment();//call t

Effective C++——条款13(第3章)

第3章    资源管理 Resource Management 所谓资源就是,一旦用了它,将来必须还给系统.C++程序中最常使用的资源就是动态内存分配(如果分配内存从来都增归还,会导致内存泄露).其他常见的资源还有文件描述符(file descriptors),互斥锁(mutex locks),图形界面中的字型和笔刷,数据库连接,以及网络sockets.不论哪一种资源,重要的是,不再使用它时,必须将它还给系统. 条款13:    以对象管理资源 Use objects to manage res

effective C++ 读书笔记 条款12与条款13

条款12:确定你的public继承塑膜出is-a关系: 这个条款主要将了一些特殊情况:比如企鹅是鸟,企鹅可以继承于鸟,但是鸟会飞,企鹅却不能飞:还有让正方形继承矩形可能也会造成这种尴尬! 这个问题以前想过,但是不知道怎么解决,如果现实生活当中确实要这么使用:比如 猫 狗 鱼  猪等等许多动物继承Animal类,但是猫狗等不会游泳, 假如这里是有很多动物,不能采用鱼里面专门加一个方法!  这个现在还没想出来,条款12也没有讲如果要这么用该怎么处理就是将要避免这样. is - a; 在面向对象程序设

Effective C++_笔记_条款08_别让异常逃离析构函数

(整理自Effctive C++,转载请注明.整理者:华科小涛@http://www.cnblogs.com/hust-ghtao/) C++并不禁止析构函数吐出异常,但它不鼓励你这样做.考虑如下代码: 1: class Widget{ 2: public: 3: ... 4: ~Widget() {...} //假设这个可能吐出一个异常 5: }; 6:  7: void doSomething() 8: { 9: vector<Widget> v ; //v在这里被自动销毁 10: ...

Effective C++_笔记_条款12_复制对象时勿忘其每一个成分

(整理自Effctive C++,转载请注明.整理者:华科小涛@http://www.cnblogs.com/hust-ghtao/) 编译器会在必要时候为我们的classes创建copying函数,这些“编译器生成版”的行为:将被烤对象的所有成员变量都做一份拷贝. 如果你声明自己的copying函数,意思就是告诉编译器你并不喜欢缺省实现中的某些行为.编译器仿佛被冒犯似的,会以一种奇怪的方式回敬:当你的实现代码几乎必然出错时却不告诉你.所以自己实现copying函数时,请遵循一条规则:如果你为c

Effective C++ 阅读笔记_条款27 尽量少做转型动作

Effective C++ 阅读笔记_条款27 尽量少做转型动作 1.转型的三种形式,可以分为两大类. (1)旧式转型(old-style casts) (1.1) (T) expresstion (1.2) T (expression) (2) 新式转型(c++-style casts) (2.1)const_cast<T> (expression) (2.2)dynamic_cast<T> (expression) (2.3)reinterpret_cast<T>

Effective C++ (笔记) : 条款11 -- 条款17

条款11:在operator=中处理"自我赋值" 自我赋值有时候不是那么明显,在处理循环(a[i] = a[j]).指针(*px = *py)和参数传递(func(const Base &rb, const Derived *pd))的时候可能会发生. Widget::operator=(const Widget& rhs) { delete pb; pb = new Bitmap(*rhs.pb); return *this; } 这是一份不安全的实现版本,在自赋值的