• 【Effective C++ 笔记】( 三 )资源管理


    祝各位程序员节日快乐~~

    三 、资源管理

    条款13 : 以对象管理资源

    Use objects to manage resources.

    熟悉智能指针的人肯定不会对此觉得陌生。利用C++中对象自动析构的特性,自动地释放资源。 C++编译器并未提供自动的垃圾回收机制,因此释放资源的责任落在了开发者的头上。 我们被要求总是成对地使用new和delete,例如:

    Investment *pInv = createInvestment();
    ...	
    delete pInv;
    
    • 1
    • 2
    • 3

    createInvestment这样的方法属于工厂方法(factory function),用来创建一个对象的实例。
    上述代码确实能够在不泄漏内存的情况下很好地执行,但问题出在createInvestment()函数把释放资源的责任交给了客户, 但并未显式地声明这一点,因而客户有时并不知情。即使客户知道该资源需要销毁, 也可能由于流控制语句或者异常的出现而使得资源未被及时释放。
    幸运的是,我们可以用对象来包装资源,并在析构函数中释放它。这样客户便不需要维护资源的内存了。 std::auto_ptr 便是这样一个对象(auto_ptr 已在 C11 废弃,请使用 unique_ptr,感谢 Enyala 的评论),它被称为智能指针(smart pointer)。 典型的使用场景是资源在堆空间中存储但只在局部被使用:

    void f(){
      std::auto_ptr<Investment> pInv(createInvestment());
    }
    
    • 1
    • 2
    • 3

    关于C++中堆空间、栈空间的使用方式,可以参考:进程的地址空间:TEXT,DATA,BSS,HEAP,STACK
    在f()调用结束时pInv退出作用域,析构函数被调用,最终使得资源被释放。 事实上,让createInvestment直接返回智能指针是更好的设计。 可以看到,使用对象来管理资源的关键在于:创建资源后立即放入资源管理对象中,并利用资源管理对象的析构函数来确保资源被释放。
    资源管理对象的实现框架正是RAII原则:acquisition is initialization,用一个资源来初始化一个智能指针。指针的析构函数中释放资源。
    值得注意的是,为了防止对象被多次释放,auto_ptr应当是不可复制的。 复制一个auto_ptr会使它变成空,资源被交付给另一个只能指针。

    std::auto_ptr<int> p1 (new int);
    *p1.get()=10;
    
    std::auto_ptr<int> p2 (p1);
    
    std::cout << "p2 points to " << *p2 << '\n';
    // p2 points to 10
    // (p1 is now null-pointer auto_ptr)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    .get方法返回资源的指针。
    auto_ptr古怪的复制行为导致它并不是管理资源的最佳方式,甚至在STL中auto_ptr的容器也是不允许的: 可以创建这样的容器,但往里面添加元素(例如push_back)时会导致编译错。

    auto_ptr<int> p1(new int);
    
    vector<auto_ptr<int>> v;    // OK,可以编译
    v.push_back(p1);            // 编译错!
    
    • 1
    • 2
    • 3
    • 4

    此处我们引入一个引用计数(reference-counting smart pointerRCSP)的指针shared_ptr。 它在没有任何其他指针引用到该资源时,进行资源的释放。不同于垃圾回收器,shared_ptr未能解决环状引用的问题。
    值得注意的是auto_ptr和shared_ptr只能管理单个资源,因为它们是使用delete而非delete[]来实现资源释放的。常见的错误便是传递数组进去:

    std::tr1::shared_ptr<int> spi(new int[1024]);
    
    • 1

    在最新的C++标准中,智能指针已经归入std命名空间了。我们可以这样使用:std::shared_ptr。
    虽然智能指针有这样的问题,但C++并未提供管理数组的智能指针,因为vector等容器就可以很好地完成这个工作。 如果你真的需要,可以求助与Boost社区的boost::scoped_array和boost::shared_array。

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

    Think carefully about copying behavior in resource-managing classes.

    Item 13:使用对象来管理资源中提出了基于RAII的资源管理对象,auto_ptr和shared_ptr。 智能指针可以有不同的拷贝策略。当你实现这样一个资源管理对象时,需要特别注意。比如一个典型的RAII风格的互斥锁实现:

    class Lock {
    public:
    explicit Lock(Mutex *pm):mutexPtr(pm){
        lock(mutexPtr);
    }
    ~Lock(){ unlock(mutexPtr); }
    private:
    Mutex *mutexPtr;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    用explicit限定构造函数,可以防止隐式转换的发生:Lock l = pm
    该互斥锁的使用方式很简单,只需要为每个临界区创建一个C++代码块,在其中定义Lock的局部变量:

    Mutex m;            // 定义互斥锁
    {                   // 创建代码块,来定义一个临界区
        Lock m1(&m);    // 互斥锁加锁
        ...             // 临界区操作
            }                   // m1退出作用域时被析构,互斥锁自动解锁
    
    • 1
    • 2
    • 3
    • 4
    • 5

    当m1被复制时情况会怎样?把当前作用域的代码加入到同一个临界区中?拷贝互斥锁并定义一个新的临界区?还是简单地给互斥锁换一个资源管理者? 资源管理对象的拷贝行为取决于资源本身的拷贝行为,同时资源管理对象也可以根据业务需要来决定自己的拷贝行为。可选的拷贝行为不外乎下面这四种:

    1. 禁止拷贝。简单地私有继承一个Uncopyable类便可以让它禁止拷贝。参见:Item 6:禁用那些不需要的缺省方法
    2. 引用计数,采用shared_ptr的逻辑。恰好shared_ptr构造函数提供了第二个参数deleter,当引用计数到0时被调用。 所以Lock可以通过聚合一个shared_ptr成员来实现引用计数:
    class Lock{
    public: 
    explicit Lock(Mutex *pm): mutexPtr(pm, unlock){
        lock(mutexPtr.get());  // .get 在条款15里
    }
    private: 
    std::shared_ptr<Mutex> mutexPtr;
    }
    // `Lock`的析构会引起`mutexPtr`的析构,而`mutexPtr`计数到0时`unlock(mutexPtr.get())`会被调用。
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    1. 拷贝底层资源。当你可以任意拥有底层资源时,可以直接拷贝它。比如string的行为:内存存有指向对空间的指针,当它被复制时会复制那片空间。
    2. 转移底层资源的所有权。auto_ptr就是这样做的,把资源移交给另一个资源管理对象,自己的资源置空。

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

    在资源管理类中提供对原始资源的访问 (显式 隐式都搞一份)
    Provide access to raw resources in resource-managing classes.

    在一个完美的设计中,所有的资源访问都应通过资源管理对象来进行,资源泄漏被完美地克服。然而世界是不完美的, 很多API会直接操作资源,尤其是一些C语言的API。总之,你会时不时地发现有需要直接访问资源, 所以资源管理对象需要提供对原始资源访问。获取资源的方式有两类:隐式地获取和显式地获取。 通常来讲,显式的资源获取会更好,它最小化了无意中进行类型转换的机会。

    显式地获取资源

    shared_ptr提供了get方法来得到资源。

    shared_ptr<Investment> pInv;
    int daysHeld(Investment *pi);
    
    int days = daysHeld(pInv.get());  // 通过get获取指针 执行显示装换
    
    • 1
    • 2
    • 3
    • 4

    为了让pInv表现地更像一个指针,shared_ptr还重载了解引用运算符(dereferencing operatoroperator->operator*

    class Investment{
    public: 
    	bool isTaxFree() const;
    };
    
    shared_ptr<Investment> pi1(createInvestment());
    bool taxable1 = !(pi1->isTaxFree());
    bool texable2 = !((*pi1).isTaxFree());
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    隐式地获取资源

    提供get方法、operator->operator*已经让资源访问很方便了。然而不幸的是,程序员是懒惰的,我们还是希望能够更加简便。 隐式转换操作符便可以完成这个工作,比如操作系统提供了FontHandle来操作字体:

    FontHandle getFont(); // C API
    void releaseFont(FontHandle fh); // CAPI
    void changeFontSize(FontHandle f, int newSize);// 将 Font 隐式装换为 FontHandle
    
    • 1
    • 2
    • 3

    我们封装了Font来管理资源:

    class Font{ // RAII class
    private:
    	FontHandle f;
    public:
        explicit Font(FontHandle fh): f(fh){}  // pass-by-value
        ~Font(){ releaseFont(f); };
        FontHandle get() const {  // 显示转换
            return f;
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    通过get方法来访问FontHandle
    显示转换确实麻烦, 但是这样可以有效地避免资源(字节)泄漏

    Font f(getFont());
    int newFontSize;
    changeFontSize(f.get(), newFontSize);
    
    • 1
    • 2
    • 3

    如果提供一个隐式类型转换运算符将Font转换为FontHandle,那么接受FontHandle类型作为参数的函数将会同样地接受Font类型。 一切将会变得简单:

    class Font{
    	operator FontHandle() const{  // 隐式装换
        	return f;
        }
    };
    
    Font f(getFont());
    int newFontSize;
    changeFontSize(f, newFontSize); // 隐式转换 为 forthandle
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    然而问题也随之出现:

    Font f1(getFont());
    FontHandle h2 = f1; // 愿意拷贝一个Font对象 但是把f1隐式装换为FontHandle
    
    • 1
    • 2

    用户无意间拷贝了一份资源!该资源并未被管理起来。这将会引发意外的资源泄漏。所以隐式转换在提供便利的同时, 也引起了资源泄漏的风险。在考虑是否提供隐式转换时,需要权衡考虑资源管理类的设计意图,以及它的具体使用场景。 通常来讲,显式的资源获取会更好,它最小化了无意中进行类型转换的机会。

    must remenber :

    • APIs 往往要求访问原始资源,所以没用过RAII class 应该提供一个“取得其所管理的资源”的办法
    • 对原始资源的访问可能经有显示转换or隐式转换。 一般而言显示转换比较安全,但隐式转换对客户比较方便 。

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

    成对使用new 和 delete 时要采用相同形式
    Use the same form in corresponding uses of new and delete.

    这是C++界中家喻户晓的规则:如果你用new申请了动态内存,请用delete来销毁;如果你用new xx[]申请了动态内存,请用delete[]来销毁。 不必多说了,来个例子吧:

    int* p = new int[2]{11, 22};
    printf("%d, %d", *p, *(p+1));
    delete[] p;
    
    • 1
    • 2
    • 3

    输出是:

    11, 22
    
    • 1

    如果delete的形式不同于new,则会产生未定义的行为。 因为delete需要调用相应的析构函数,所以它需要知道被删除的是数组还是单个对象。 即使是基本数据类型,错误的调用也会导致未定义行为。
    不过在Homebrew gcc 5.1.0中,在int数组上调用delete不会引发严重后果。只是后面的动态内存未被释放而已。 但是用delete来删除string数组,会有如下错误:

    malloc: *** error for object 0x7fcd93c04b38: pointer being freed was not allocated
    
    • 1

    不管怎样,只需要记住用使用同样的形式来new和delete就好了。唯一的问题在于:typedef。请看例子:

    typedef string address[4];  // 每个地址是四个字符串
    
    string* addr = new address;  // 相对于使用string[4]
    delete[] addr;
    
    • 1
    • 2
    • 3
    • 4

    注意!此时用new来申请空间,却需要使用delete[]来释放。可能你会想这样写:

    address* addr = new address;
    delete addr;
    
    • 1
    • 2

    问题在于addr的初始化语句中,等号两边的类型不兼容:

    • 等号右边:new address的返回值与new string[4]具有同样的类型:string*。
    • 等号左边:addr的类型是数组指针:string (*)[4]。

    最终的解决办法还是避免使用typedef来定义数组,你可以使用更加面向对象的vector、string等对象。

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

    以独立语句将newed对象置入智能指针
    Store newed objects in smart pointers in standalone statements.

    在单独的语句中将new的对象放入智能指针,这是为了由于其他表达式抛出异常而导致的资源泄漏。 因为C++不同于其他语言,函数参数的计算顺序很大程度上决定于编译器。
    如果你在做Windows程序设计,或者DLL开发,可能会经常碰到类似__cdecl,__stdcall等关键字。它们便是来指定参数入栈顺序的。
    还是举个例子比较清晰:

    processWidget(shared_ptr<Widget>(new Widget), priority());
    
    • 1

    上述代码中,在processWidget函数被调用之前参数会首先得到计算。可以认为包括三部分的过程:

    1. 执行new Widget
    2. 构造shared_ptr
    3. 调用priority()

    多数情况下编译器有权决定这三部分过程的顺序,如果很不幸由于某种效率原因,编译器认为顺序应当是1, 3, 2,即:

    1. 执行new Widget
    2. 调用priority()
    3. 构造shared_ptr

    那么如果priority抛出了异常,新的Widget便永远地找不回来了。虽然我们处处使用智能指针,资源还是泄漏了!
    于是更加健壮的实现中,应当将创建资源和初始化智能指针的语句独立出来:

    shared_ptr<Widget> pw = shared_ptr<Widget>(new Widget);
    processWidget(pw, priority());
    
    • 1
    • 2

    以独立语句将newed对象存储于智能指针内,如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄漏 。

  • 相关阅读:
    基于EF Core存储的国际化服务
    [Linux] 数据链路层-----以太网帧协议、ARP协议
    如何隐藏 Apache 的版本号和操作系统信息
    RCP系列-第一章 环境安装
    稳态视觉诱发电位 (SSVEP) 分类学习系列 (1) :FFT-CNN-CCA
    好利来做宠物蛋糕,为啥品牌争相入局宠物赛道?
    孟晚舟最新发声!华为吹响人工智能的号角,发布“全面智能化”战略部署
    unity驱动3d模特跳舞 穿模问题 穿透
    基于微信小程序的自习室系统设计与实现,可作为毕业设计
    疫情防控系统怎么选?还是国产化的靠谱
  • 原文地址:https://blog.csdn.net/weixin_49486457/article/details/134018870