三、资源管理--条款13-15

概述

  • 资源就是一旦用了它,以后必须还给系统的东西。C++中最常用的资源就是动态内存分配。其它的资源还包括文件描述符器、互斥锁、图形界面中的字型和笔刷、数据库连接、以及网络socket
  • 无论是哪一种资源,我们都要确保当自己使用完之后还给系统。

条款13:以对象管理资源

1. 资源并没有还给系统。

概述中已经说到,资源用完之后要还给系统。 我们考虑以下函数会发生什么:

void f()
{
    Investment * pInv = createInvestment();
    ...
    delete pInv;
}

1.1 倘若我们在delete之前的函数中有一个分支,会进行return。那么我们就永远不会执行delete,这样函数执行结束之后并没有将动态分配的资源还给系统,会有内存泄漏的风险。

1.2 倘若中间有goto语句,也是如此。

1.3 倘若抛出了异常,delete函数也无法被执行到。

2.1 使用智能指针————auto_ptr.

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

当智能指针出了函数的作用域,会调用其析构函数自动删除pInv.

这个简单例子示范“以对象管理资源”的两个关键想法:

  • 获得资源后立即放入管理对象。“资源取得时机便是初始化时机”。(个人认为如果不第一时间放进管理对象,那么很有可能代码因为某种原因return了,内存就泄漏了。)
  • 管理对象运用析构函数确保资源被释放。

auto_ptr的一个性质:

auto_ptr<Investment> pInv1(createInvestment()); //pInv1指向对象
auto_ptr<Investment> pInv2(pInv1); //pInv指向对象,pInv1为null
pInv1 = pInv2;  //pInv1指向对象,pInv2为null

这块代码已经注释,也就是aotu_ptr的性质:

如果通过copy构造函数或copy assignment函数复制它们,原来的auto_ptr将变成null,新的指向才指向对象。

原因:

如果让多个auto_ptr指向了同一个对象,那么如果多个auto_ptr的析构函数调用,会对一个对象进行多次删除,但实际上第一次删除之后就不存在了,后面的删除会造成未定义的行为。

2.2 使用“引用计数型智慧指针”替代auto_ptr

也就是shared_ptr:

void f()
{
    shared_ptr<Investment> pInv1(createInvestment()); //pInv1指向对象
    shared_ptr<Investment> pInv2(pInv1); //pInv2指向对象,pInv1也指向对象
    pInv1 = pInv2;  //pInv1指向对象,pInv2也指向对象
}

在这个函数里面,shared_ptr允许多个指针指向同一个对象。它会对指针进行计数,直到计数为0的时候才会调用析构函数,删除对象。所以并不会出现多次删除同一个对象的情况。

注意:

auto_ptr和shared_ptr在其析构函数中做的都是delete操作而没有delete[]操作。所以我们要注意别在动态分配的array中使用这两个指针。

auto_ptr<string> aps(new string[10]);
shared_ptr<int> spi(new int[1024]);

这两个操作都是十分危险的。

作者总结:

为防止资源泄漏,请使用RAII对象,它们在构造函数中获得资源并在析构函数中释放资源。

两个常被使用的RAII classes分别是shared_ptr和auto_ptr.前者是较佳的选择,因为其copy行为比较直观。若选择auto_ptr,复制动作会使它指向null.

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

当我们使用shared_ptr的时候,当引用计数变为0时,会删除这个对象。但实际情况里,我们可能不想要删除。

比如一个互斥锁的类,构造的时候就加锁,析构的时候解锁,这样也符合RAII对象的特性。

class Lock
{
public:
    explicit Lock(Mutex *pm)
    :mutexPtr(pm)
    {
        lock(mutexPtr); // 获得资源
    }
    ~Lock()
    {
        unlock(mutexPtr);   // 释放资源
    }
private:
    Mutex *mutexPtr;
}

现在让我们看看,在调用的时候进行copy的行为会造成什么:

Mutex m;
...
{
    Lock m1(&m);
    ...
}

这段代码是正确的行为,在出作用域的时候,析构函数被调用,就会自动解锁。

但是如果客户端出现copy行为:

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

这会造成m2还在使用资源的时候,假如m1析构函数调用了,那么资源就被释放了,m2也就无法使用了。

解决方法:

  • 禁止复制。用条款6中所述的使用一个Uncopyable类,通过将拷贝构造声明为private的来禁止调用。

  • 对底层资源祭出“引用计数法”。 这个方法要注意shared_ptr在计数为0的时候会删除这个对象,但是我们只需要释放它,所幸shared_ptr允许我们指定一个删除器。我们可以改成:

    class Lock
    {
    public:
    explicit Lock(Mutex *pm)
    :mutexPtr(pm,unlock)
    {
    lock(mutexPtr.get()); // 获得资源,get函数是获取原始指针
    }
    private:
    shared_ptr mutexPtr;
    }
    我们指定看一个unlock函数作为删除器,计数器为0的时候会执行。所以我们无需一个析构函数。

  • 复制底部资源。就是执行“深拷贝”。“深拷贝”是会在新的内存中存一份和原数据一模一样的数据,不会使用原来的地址。那么就是两份相同数据存在不同的地方。在这个例子里,就是两份资源了,释放掉原来的资源并不影响新的资源。

  • 转移底部资源所有权。 确保只有一个RAII对象指向一个资源,复制时候资源的所有权从被复制物转向目标物。 可以参考auto_ptr的做法,讲原来的指针置为null,确保只有一份资源被使用。

作者总结

复制RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定对象的copying行为。

普遍而常见的RAII class copying行为是:抑制copying、施行引用计数法(reference counting)。不过其他行为也都可能被实现。

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

  1. shared_ptr获得原始指针,只需要调用get函数就可以。

    shared_ptr pInt;
    int *p = pInt.get();

  2. shared_ptr和auto_ptr都重载了指针取值的方法。

    class Investment
    {
    public:
    bool isTaxFree() const;
    ...
    };

此时执行:

shared_ptr<Investment> pInv(createInvestment());
bool taxFree1 = pInv->isTaxFree();
bool taxFree2 = (*pInv).isTaxFree();

以上"->"运算符和"."运算符都是适用的。

原文地址:https://www.cnblogs.com/love-jelly-pig/p/9629800.html