首页 > 代码库 > 《Effective C++》资源管理:条款13-条款15

《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就应该这样。

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