所谓资源就是,一旦用了它,将来必须归还给系统。
C++程序中最常用的资源就是动态分配内存,但内存只是我们要管理的众多资源之一。其他常见的资源包括:文件描述器、互斥锁、数据库连接、网络socket。
接下来我们将学习基于对象的资源管理方法,建立在C++对构造函数、析构函数、copying函数的基础上。当我们严格按照这些条款来做的时候,几乎可以消除资源管理的问题。
假设我们创建一个用来建模投资行为(股票、基金等)的程序库,其中各式各样的投资类型继承自基类Investment。这个程序库通过一个工厂函数(返回基类指针,指向继承体系内动态分配的对象)来获取某个特定的Investment对象。
class Investment{ ... };
Investment* createInvestmen(); //工厂函数
void f()
{
Investment* pInv = createInvestment(); //调用工厂函数
...
delete pInv;
}
在上述代码中,函数f内调用了工厂函数,得到了一个基类类型的指针,当我们使用完毕后,应当归还指针指向的内存。但如果在delete之前,函数就已经return,那么 该指针指向的对象 及 对象所包括的所有资源 都无法得到释放。
为了确保createInvestment返回的资源总是被释放,我们需要将资源放进对象内,当控制流离开函数f,该对象的析构函数会自动释放那些资源。也就是,把资源放入对象内,依赖C++的析构函数自动调用机制 确保资源被释放。
标准库为我们提供了“类指针对象”auto_ptr,也就是智能指针,它的析构函数自动对其所指对象调用delete。顾名思义,auto_ptr为一个类的指针的模板类。
#include
void f()
{
auto_ptr pInv(createInvestment());
//pInv是Investment类型的智能指针,以createInvestment()函数的返回值初始化
}
这个例子告诉我们以对象管理资源的两个关键想法:
由于auto_ptr被销毁时会自动删除它所指向的内存,所以一定不能让多个auto_ptr同时指向同一对象。若通过拷贝构造函数和拷贝赋值运算符复制它们,则原有的auto_ptr变为null。
auto_ptr pInv1(createInvestment());
auto_ptr pInv2(pInv1); //pInv1变为null,pInv2指向对象
pInv1 = pInv2; //pInv2变为null,pInv1指向对象
当我们要完成正常的复制行为时,auto_ptr无法满足我们,并且C++新标准中已将auto_ptr删除,新增为unique_ptr。
shared_ptr也是一个智能指针模板类,它会持续追踪有多少个对象指向某笔资源,在无人指向它的时候自动删除该资源。
void f()
{
shared_ptr pInv1(createInvestment());
shared_ptr pInv2(pInv1);
pInv1 = pInv2;
... //pInv1与pInv2被销毁,它们所指的对象也被销毁
}
需要注意的一点是,智能指针的析构函数调用所指类型的析构函数,因此将它指向vector、string、array等这些已封装好的类是绝对没有问题的。但是我们千万不能将它指向数组类型 [ ],以string的数组为例,若我们将智能指针指向它,在调用析构函数时,只会delete数组的第一个元素。
shared_ptr<int> spi(new int[1024]);
shared_ptr<string> sps(new string[2]);
//delete时只会删除数组的第一个元素
总结:
unique_ptr和shared_ptr只是标准库提供给我们的用于管理堆上资源的两个资源管理类,告诉我们以对象管理资源的重要性。在很多时候,我们还需要自己写资源管理类来管理我们的资源。
假如,我们有两个函数处理类型为Mutext的互斥器对象,为确保互斥器加锁后会被解锁,我们要建立一个资源管理类Lock。
void lock(Mutex *pm); //为互斥器加锁
void unlock(Mutext *pm); //为互斥器解锁
class Lock
{
public:
explicit Lock(Mutex* pm) :mutexPtr(pm)
{
lock(mutexPtr);
}
~Lock()
{
unlock(mutexPtr);
}
private:
Mutex *mutexPtr;
}
Mutex m;
Lock m1(&m);
...
但如果我们要对Lock对象进行拷贝操作(若用合成的拷贝操作函数,则会出现多个指针指向一个对象),我们一般有以下四种方法,其中前两种比较常见。
class Lock{
public:
expicit Lock(Mutex* pm):mutexPtr(pm,unlock) //删除器对构造函数而言是可有可无的第二参数
{ //此处将删除器定义为unlock函数
lock(mutexPtr.get());
}
private:
shared_ptr mutexPtr;
};
//我们不需要再为Lock类提供析构函数,因为合成的析构函数会自动调用mutexPtr的析构函数,
//而mutexPtr当引用计数为0时会自动调用shared_ptr的删除器,也就是unlock()函数
总结:
我们在使用资源时,希望依赖资源管理类对象来进行与资源之间的互动,而非直接处理资源。
例如条款13中的createInvestment函数。
Investment* createInvestmen(); //工厂函数
int getDay(const Investment* pi);
int day = getDay(pInv); //pInv是一个shared_ptr对象
上述过程是无法通过编译的,因为getDay函数的参数是一个Investment类型的指针,而我们在调用时却传给它了一个shared_ptr< Investment >类型的对象。因此,我们需要在资源管理类中提供对原始资源访问的接口,来实现通过资源管理类对象与资源互动的目标。
我们需要完成 资源管理类对象—> 原始资源 转换功能的函数,有显示转换和隐式转换两种。
int day = getDay(pInv.get());
class Investment{
public:
bool isTaxFree() const;
...
}
Investment* createInvestmen(); //工厂函数
shared_ptr pi1(createInvestment());
shared_ptr pi2(createInvestment());
bool taxable1 = pi1->isTaxFree(); //由->访问资源
bool taxable2 = (*pi2).isTaxFree(); //由*访问资源
而当我们自己设计资源管理类时,也要提供对原始资源的访问函数,一般来说,显示转换比较受欢迎,因为隐式转换有可能会发生不想要的转换。
总结:
string* stringArray = new string[100];
delete stringArray; //错误
delete[] stringArray; //正确
原声数组在内存中包括数组大小的记录,当我们以第一条语句删除stringArray时,编译器会认为stringArray指向一个单一对象,因此可能只会删除(析构函数+释放内存)第一个成员。而当我们以第二条语句删除stringArray时,显示地告诉编译器这是一个原声数组。
总结:
int priority();
void processWidget(shared_ptr pw,int priority) ;
processWidget( new Widget,priority() ); //1
processWidget( shared_ptr(new Widget) ,priority() ); //2
shared_ptr w(new Widget);
processWidget( w,priority() ); //3
如上述代码所示,有两个函数,一个是返回优先级的priority,一个是根据优先级进行某些操作的processWidget。
当我们以语句1调用processWidget时,编译不通过,因为该函数的第一个形参为shared_ptr对象,我们无法将原生指针赋值给它。
当我们以语句2调用processWidget时,在调用processWidget函数之前,编译器需要做三件事:
① new Widget对象 ② 调用shared_ptr构造函数 ③ 调用priority函数
扯蛋的是,编译器对这三件事的先后顺序没有明确的定义。当编译器按照①③②来执行,并且调用priority函数时有异常发生,那么原生资源的指针便会丢失,它所拥有的内存便无法被释放。
当我们以语句3调用processWidget时,一切都会顺利进行。因此,我们要以独立语句将newed对象置入智能指针。