C++的55个条款——资源管理

资源管理

所谓资源就是,一旦用了它,将来必须归还给系统。

C++程序中最常用的资源就是动态分配内存,但内存只是我们要管理的众多资源之一。其他常见的资源包括:文件描述器、互斥锁、数据库连接、网络socket。

接下来我们将学习基于对象的资源管理方法,建立在C++对构造函数、析构函数、copying函数的基础上。当我们严格按照这些条款来做的时候,几乎可以消除资源管理的问题。


条款13:以对象管理资源

假设我们创建一个用来建模投资行为(股票、基金等)的程序库,其中各式各样的投资类型继承自基类Investment。这个程序库通过一个工厂函数(返回基类指针,指向继承体系内动态分配的对象)来获取某个特定的Investment对象。

class Investment{ ... };

Investment* createInvestmen();     //工厂函数

void f()
{
    Investment* pInv = createInvestment();    //调用工厂函数
    ...
    delete pInv;
}

在上述代码中,函数f内调用了工厂函数,得到了一个基类类型的指针,当我们使用完毕后,应当归还指针指向的内存。但如果在delete之前,函数就已经return,那么 该指针指向的对象对象所包括的所有资源 都无法得到释放。

为了确保createInvestment返回的资源总是被释放,我们需要将资源放进对象内,当控制流离开函数f,该对象的析构函数会自动释放那些资源。也就是,把资源放入对象内,依赖C++的析构函数自动调用机制 确保资源被释放

1. auto_ptr

标准库为我们提供了“类指针对象”auto_ptr,也就是智能指针,它的析构函数自动对其所指对象调用delete。顾名思义,auto_ptr为一个类的指针的模板类。

#include 

void f()
{
    auto_ptr pInv(createInvestment());
    //pInv是Investment类型的智能指针,以createInvestment()函数的返回值初始化
}

这个例子告诉我们以对象管理资源的两个关键想法:

  • 获得资源后立刻放进管理它的对象内。上述代码中,createInvestment()返回的资源被作为pInv的初值。因此“以对象管理资源”常被成为“资源取得时机便是初始化时机”。
  • 管理对象运用析构函数确保资源被释放。

由于auto_ptr被销毁时会自动删除它所指向的内存,所以一定不能让多个auto_ptr同时指向同一对象。若通过拷贝构造函数和拷贝赋值运算符复制它们,则原有的auto_ptr变为null。

auto_ptr pInv1(createInvestment());

auto_ptr pInv2(pInv1);        //pInv1变为null,pInv2指向对象

pInv1 = pInv2;                            //pInv2变为null,pInv1指向对象
2. shared_ptr

当我们要完成正常的复制行为时,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时只会删除数组的第一个元素

总结:

  • 为防止资源泄露,使用资源管理类对象,它们在构造函数中获得资源,在析构函数中释放资源。
  • 两个常被使用的资源管理类是shared_ptr和unique_ptr。前者是较佳选择。

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

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对象进行拷贝操作(若用合成的拷贝操作函数,则会出现多个指针指向一个对象),我们一般有以下四种方法,其中前两种比较常见。

  • 禁止复制 如果复制动作对资源管理类对象并不合理,如上述的Lock类,应将拷贝构造函数和拷贝赋值运算符定义为删除的 来禁止复制。
  • 对底层资源使用“引用计数” 也就是将资源管理类内的指针声明为shared_ptr。但需要注意的是,当shared_ptr对象的引用计数为0时,会删除所指对象。但有些时候我们不希望这样,比如上述的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()函数
  • 复制底部资源。 也就是我们所说的深拷贝
  • 转义底部资源的拥有权。 也就是auto_ptr所用的方法。

总结:

  • 复制资源管理对象必须一并复制它所管理的资源,所以资源的拷贝行为决定资源管理对象的拷贝行为。
  • 普遍的资源管理对象拷贝行为是:抑制拷贝、引用计数法拷贝。

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

我们在使用资源时,希望依赖资源管理类对象来进行与资源之间的互动,而非直接处理资源。

例如条款13中的createInvestment函数。

Investment* createInvestmen();     //工厂函数

int getDay(const Investment* pi);  

int day = getDay(pInv);            //pInv是一个shared_ptr对象

上述过程是无法通过编译的,因为getDay函数的参数是一个Investment类型的指针,而我们在调用时却传给它了一个shared_ptr< Investment >类型的对象。因此,我们需要在资源管理类中提供对原始资源访问的接口,来实现通过资源管理类对象与资源互动的目标。

我们需要完成 资源管理类对象—> 原始资源 转换功能的函数,有显示转换和隐式转换两种。

  • 显示转换 shared_ptr和unique_ptr都提供了get成员函数,用来执行显示转换,返回原始资源的指针。
int day = getDay(pInv.get());
  • 隐式转换 shared_ptr和unique_ptr也重载了指针取值操作符 ->和*,允许隐式转换至原始指针。
class Investment{
public:
    bool isTaxFree() const;
    ...
}

Investment* createInvestmen();     //工厂函数

shared_ptr pi1(createInvestment());
shared_ptr pi2(createInvestment());

bool taxable1 = pi1->isTaxFree();    //由->访问资源
bool taxable2 = (*pi2).isTaxFree();  //由*访问资源

而当我们自己设计资源管理类时,也要提供对原始资源的访问函数,一般来说,显示转换比较受欢迎,因为隐式转换有可能会发生不想要的转换。

总结:

  • 每一个资源管理类都应该提供一个“取得其所管理的资源”的函数。
  • 对原始资源的访问可以使显示转换或隐式转换,一般而言显示转换比较安全。

条款16:成对使用new和delete时要采取相同形式

string* stringArray = new string[100];

delete stringArray;             //错误
delete[] stringArray;           //正确

原声数组在内存中包括数组大小的记录,当我们以第一条语句删除stringArray时,编译器会认为stringArray指向一个单一对象,因此可能只会删除(析构函数+释放内存)第一个成员。而当我们以第二条语句删除stringArray时,显示地告诉编译器这是一个原声数组。

总结:

  • 在new中使用[ ],必须在相应的delete中也使用[ ];如果在new中不使用[ ],在相应的delete中也不应该使用[ ]。

条款17:以独立语句将newed对象置入智能指针

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对象置入智能指针。

你可能感兴趣的:(C++)