zoukankan      html  css  js  c++  java
  • Effective C++读书笔记~3 资源管理

    条款13:以对象管理资源

    Use objects to manage resources.

    传统new/delete 申请、释放资源的问题

    class Investment { ... };
    class Factory 
    { 
    public:    
        static Investment* createInvestment(); // 工厂函数
    };
    
    void f( ) // 客户函数
    {
            Investment* pInv =Factory::createInvestment(); // 调用工厂函数,创建Investment对象
        ...
        delete pInv; // 释放pInv所指对象
    }
    

    正常时,没有问题。而"..."代码段如果提前return,或者出现异常,提前退出f()函数,那么pInv所指Investment对象就无法释放。
    为解决这个问题,可以使用RAII(Resource Acquisition Is Initialization,资源取得时机便是初始化时机)对象的机制,即对象创建时便初始化资源,对象析构时便销毁资源。而智能指针便是一种较好的管理资源对象的方式。

    使用智能指针unique_ptr管理对象

    对于上述问题,可以使用智能指针管理资源。
    unique_ptr是一种智能指针,能确保某个时刻只能有一个unique_ptr指向一个给定对象。当unique_ptr被销毁时,它所指向的对象也被销毁。C++11引入,可用来替换auto_ptr。

    void f()
    {
           unique_ptr<Investment> pInv(Factory::createInvestment());  // OK
           //...
           unique_ptr<Investment> pInv2(pInv); // 错误:unique_ptr不支持2个指针指向同一个对象
           unique_ptr<Investment> pInv3;
           pInv3 = pInv; // 错误:unique_ptr不支持2个指针指向同一个对象
           // f退出前调用unique_ptr析构函数,自动删除pInv
    }
    

    使用引用计数智能指针shared_ptr管理对象

    unique_ptr的缺点是无法复制,同一时刻只能有一个指针指向被管理的对象,也就是说资源无法共享。
    可以使用引用计数型智能指针shared_ptr,来追踪有多少个对象指向某个资源,并且在无人指向它时自动删除该资源。
    shared_ptr的缺点:无法打破环状引用,即2个已经没被使用的对象彼此互指,资源无法正常释放。

    void f()
    {
           shared_ptr<Investment> pInv(Factory::createInvestment());  // OK
           //...
           shared_ptr<Investment> pInv2(pInv); // OK
           shared_ptr<Investment> pInv3;
           pInv3 = pInv; // OK
           // f退出前调用shared_ptr析构函数,自动删除pInv, pInv2, pInv3
    }
    

    需要注意的是:unique_ptr和shared_ptr不能释放array。
    因为两者在其析构函数内做delete,而不是delete[]。如果要使用自动释放的数组,建议用vector、string,或者boost::scoped_array和boost::shared_array classed。

    小结

    1)为防止资源泄漏,建议使用RAII对象,在构造函数中获得资源,在析构函数中释放资源;
    2)两个经常被使用的RAII classes是:unique_ptr, shared_ptr。排他性的资源,前者较佳;共享性的资源,后者较佳。两者都只适合用来管理head-based资源。

    [======]

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

    Think carefully about copying behavior in resource-managing classes.

    对于不是heap-based资源,unique_ptr和shared_ptr 往往不适合作为资源的掌管着(resource handlers)。

    我们以对POSIX 的pthread库里的互斥量用C++进行封装为例:

    // Lock掌管不是heap-based的资源
    class Lock {
    public:
        // 这里互斥锁是RAII资源,用Lock进行管理
        explicit Lock(pthread_mutex_t *pm): mutexPtr(pm) { // 构造即获得互斥锁
            pthread_mutex_lock(mutexPtr); // 获得互斥锁
            cout << "locked" << endl;
        }
        ~Lock() { // 析构即释放锁
            pthread_mutex_unlock(mutexPtr); // 释放互斥锁
            cout << "unlocked" << endl;
        }
    private:
        pthread_mutex_t *mutexPtr; // RAII资源
    };
    
    // 客户多Lock的用法
    pthread_mutex_t m; // 定义需要的互斥锁
    void func()
    {
        Lock ml1(&m); // OK
        Lock ml2(ml1); // 拷贝ml1到ml2上,会发生什么?
    }
    

    当一个RAII对象被复制时,会发生什么?
    通常,会有2种选择:
    1)禁止复制。参见条款06,可以将基类的copying操作声明为private,也可以使用delete禁止编译器合成copy构造函数和copy assignment运算符。
    2)对底层资源使用“引用计数法”(reference-count)。有时,我们希望保持资源,共享给多个用户,直到最后一个使用者(某个对象)被销毁。此时应该使用shared_ptr。

    使用shared_ptr当引用次数为0时,默认行为是调用delete操作符 删除所指物。而使用向互斥锁时,我们期望的是引用计数为0时,就解锁而非删除,因此需要手动指定其“删除器”(deleter)。删除器的本质是一个函数或函数对象(function object),当引用次数为0时调用。

    // RAII资源管理类
    class Lock {
    public:
        explicit Lock(pthread_mutex_t *pm): mutexPtr(pm, pthread_mutex_unlock) {
            lock(mutexPtr.get());
        }
        Lock(const Lock& lck) {
            mutexPtr = lck.mutexPtr; // mutexPtr次数-1, lck.mutexPtr次数+1. mutexPtr引用次数为0时, 指向内存释放, 并指向lck.mutexPtr指向的内存
            lock(mutexPtr.get());
        }
    #if 1 // 析构函数可以依靠自动合成的, 也可以手动编写
        ~Lock() { // 因为编译器自动合成的析构函数, 调用每个non-static对象成员的析构函数
            // mutexPtr的析构函数会在互斥锁引用次数为0时, 自动调用其创建时指定的删除器
            unlock(mutexPtr.get()); // get获得shared_ptr指向的内存, i.e. shared_ptr第一个参数
        }
    #endif
    private:
        shared_ptr<pthread_mutex_t > mutexPtr; // 使用shared_ptr替换了raw pointer, 指向RAII资源
        int lock(pthread_mutex_t *pm) {
            int res = pthread_mutex_lock(pm);
            cout << "shared locked" << endl;
            return res;
        }
        int unlock(pthread_mutex_t *pm) {
            int res = pthread_mutex_unlock(pm);
            cout << "unlocked" << endl;
        }
    };
    
    pthread_mutex_t m;
    void func()
    {
        Lock ml1(&m); // OK
        Lock ml2(ml1); // OK. 使用shared_ptr管理mutex后, 允许多个用户同时指向同一个互斥锁资源, i.e. 互斥锁资源被共享了
    
        cout << "exit func" << endl;
    }
    
    int main() {
        func();
        cout << "exit main" << endl;
        return 0;
    }
    

    这里用shared_ptr<pthread_mutex_t > mutexPtr替换了原来的pthread_mutex_t *mutexPtr,允许共享资源。

    复制底部资源

    需要“资源管理类”的唯一理由:当不再需要某个副本时,可以确保它被释放。此时,复制资源管理对象,应该同时也复制其所包含的资源。i.e. 复制资源管理器对象时,进行的是“深度拷贝”(deep copying)。
    深度拷贝是指:不仅复制指针本身,还复制指针指向的内存内容。

    转移底部资源的拥有权

    某些场合下,你可能希望确保永远只有一个RAII对象指向一个未加工资源(raw resource),即使RAII对象被复制依然如此。此时,资源的拥有权会从被复制物转移到目标物。如条款13所述,unique_ptr的复制意义。
    同一时刻,只允许一个unique_ptr指向给定对象。如果要从一个unique_ptr转移到另外一个,可以使用如下方法:

    unique_ptr p1(new string("hello")); // 让p1指向string对象
    unique_ptr p2(p1.release()); // 将p1指向的string对象转移给p2

    小结

    1)复制RAII对象必须一并复制它所管理的资源(深度拷贝),所以资源的copying行为决定RAII对象的copying行为;
    2)通常的RAII class copying行为是:抑制copying(禁用copying函数),或者引用计数法(reference counting)(可使用shared_ptr)。

    [======]

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

    Provide access to raw resources in resource-managing classes.

    条款13中的pInv是对象,如果要访问shared_ptr指向的那块Investment对象(称为原始资源,raw resource),要怎么办?

    shared_ptr<Investment> pInv(Factory::createInvestment());  // OK
    

    比如,希望通过daysHeld返回投资天数

    int daysHeld(const Investment* pi); // 返回投资天数
    

    要如何调用?
    要将RAII class对象转换为内含的原始资源,可以分为两种方式:

    1. 显示转换

    使用shared_ptr的get成员函数(unique_ptr也提供),返回智能指针内部的原始指针(的副本),来进行显式转换。

    int days = daysHeld(pInv.get()); // get获取shared_ptr指向的内存
    

    2. 隐式转换

    使用shared_ptr重载了的指针取值操作符(operator ->和operator *),允许隐式转换至所指向的资源的原始指针。unique_ptr同样重载了这2个操作符。

    class Investment {
        public:
        bool isTaxFree() const;
        ...
    };
    class Factory {
    public:
        static Investment* createInvestment() // 工厂函数
      { return new Investment;}
    };
    
    shared_ptr<Investment> pi1(Factory::createInvestment()); // shared_ptr管理资源
    bool taxable1 = !(pi1->isTaxFree()); // 利用shared_ptr的 -> 操作符, 访问raw resource对象的成员函数
    
    unique_ptr<Investment> pi2(Factory::createInvestment()); //unique_ptr管理资源
    bool taxable2 = !((*pi2).isTaxFree()); // // 利用unique_ptr的 * 操作符, 访问raw resource对象的成员函数
    

    小结

    1)APIs往往要求访问原始资源(raw resources),所以每个RAII class应该提供一个“取得其所管理的资源”的办法(就像shared_ptr的get方法,解引用方法(*),指针访问所指目标的-> 方法);
    2)对原始资源的访问可能经由显示转换/隐式转换。一般而言,显示转换比较安全,但隐式转换对客户比较方便。

    [======]

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

    Use the same from in corresponding uses of new and delete.

    对象、数组的申请与释放

    string* stringPtr1 = new string; // 申请一个string对象
    string* stringPtr2 = new string[100]; // 申请一个string对象数组,大小为100

    delete stringPtr1; // 删除一个对象
    delete[] stringPtr2; //删除一个由对象组成的数组

    1)如果在new表达式中使用[],必须在相应的delete表达式中也使用[]。如果在new表达式中不用[],一定不要在相应的delete表达式中使用[]。

    [======]

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

    Store newd objects in smart pointers in standalone statements.

    资源创建和资源转换之间的异常

    假设我们有函数priority用于揭示程序的优先权,而函数processWidget用于动态分配的Widget上进行带有优先权的处理,

    class Widget {};
    int priority();
    void processWidget(shared_ptr<Widget> pw, int priority);
    

    我们“以对象管理资源”(条款13),在processWidget中使用Widget的raw pointer来构造智能指针shared_ptr,来管理Widget:

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

    上述代码看似没有问题,却可能造成资源泄漏。原因是,priority()发送异常时,Widget的raw pointer丢失,无法被正常释放。

    调用processWidget之前,编译其会创建代码做以下三件事:

    • 调用priority
    • 执行new Widget
    • 调用shared_ptr构造函数

    然而,编译器以何种次序完成这三件事却没有定数,唯一能确定的是“执行new Widget”发生在“调用shared_ptr构造函数”之前。如果编译器为了取得更高效的代码,可能最终的操作序列会是这样:
    1)执行new Widget
    2)调用priority
    3)调用shared_ptr构造函数

    当执行到第2)步时,如果调用priority发生异常,那么第1)步new Widget得到的指针就会丢失,从而造成内存泄漏。

    注意:
    1)调用惯例(cdecl, thiscall)只能确保参数的入栈顺序,并不能确保在这之前的参数构造、核算顺序。
    2)其他语言如Java、C#不存在这个问题,因为它们总是以特定次序完成函数参数的核算。

    解决办法

    这类发生在“资源被创建(new Widget)”和“资源被转换为资源管理对象(shared_ptr)”2个时间点之间的异常干扰的问题,要如何解决?
    办法很简单,就是分离语句,让资源的创建、转换和可能异常语句不要在同一行,分别写出:
    1)创建Widget并置入智能指针内;
    2)把指针传递给processWidget;

    shared_ptr<Widget> pw(new Widget); // 单独语句内以智能指针存储newed所得对象
    processWidget(pw, priority()); // 该调用不会因异常而导致Widget指针丢失, 从而造成泄漏的问题
    

    这么做有效的原因是因为:编译器对于“跨越语句的各项操作”没有重新排列的资源(只有在语句内才拥有那个自由度)

    小结

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

    [======]

  • 相关阅读:
    Shadow SSDT详解、WinDbg查看Shadow SSDT
    两种方法获取shadow ssdt
    r0遍历系统进程方法总结
    枚举PEB获取进程模块列表
    用户层获取TEB PEB结构地址 遍历进程模块.doc
    Process32First 返回FALSE的原因
    windows内核需要注意的
    hive中遇到的问题
    解读:计数器Counter
    hadoop System times on machines may be out of sync. Check system time and time zones.
  • 原文地址:https://www.cnblogs.com/fortunely/p/15565919.html
Copyright © 2011-2022 走看看