《Effective C++》笔记 第三章 资源管理

《Effective C++》笔记 第三章 资源管理,第1张

《Effective C++》笔记 第三章 资源管理

        如果分配内存却不归还,会导致内存泄漏,但内存只是必须管理的众多资源之一,图形界面中的字型和笔刷、数据库连接、以及网络sockets。不论哪一种资源,不再使用它时,必须将它还给系统。

        十三、以对象管理资源

                

class Investment { ... };    // 投资类型 继承体系中的root class
Investment* createInvestment(); // 返回指针,指向Investment继承体系内的动态分配对象。调用者
                                // 有责任删除他,这里为了简化,可以不写参数
void f()
{
    Investment* pInv = createInvestment();  // 调用factory函数
    ...
    delete pInv;                            // 释放pInv所指对象
}

        上述调用看起来没有问题。但是因为"..."区域的存在,如果在该位置执行一个return语句,这样就不会触及delete语句,类似的情况发生在createInvestment的使用及delete动作位于某些循环内,该循环由于某个continue或goto语句过早退出。最后一种可能是“...”内抛出异常。同样不会触及delete语句。这样就会导致资源泄漏。

        把资源放进对象内,我们可以依赖C++的“析构函数”自动调用机制确保资源被释放。

        许多资源被动态分配与Heap内而后被用于单一区块或函数内,在控制流离开那块区域或函数时被释放,标准库提供auto_ptr可以针对这种情况。其析构函数自动对其所指对象调用delete。

void f()
{
    // 调用factory函数,一如以往地使用pInv,经由auto_ptr的析构函数自动删除pInv。
    std::auto_ptr pInv(createInvestment());
}

        以对象管理资源的两个关键想法:

        1、获得资源后立刻放进管理对象(managing object)内。常被称为“资源去的时机便是初始化时机”(Resourec Acquisition Is Initialization; RAII),因为我们几乎总是在获得一笔资源后于同一块语句内以它初始化某个管理对象,有时候获得的资源拿来赋值某个管理对象,但是不论哪种做法,没一笔资源都在获得的同时立刻被放进管理对象中。

        2、管理对象(managing object)运用析构函数确保资源被释放。

        不要多个auto_ptr指向同一个对象,如果指向同一个对象,该对象会被删除一次以上。会使程序产生为定义行为。为了防止这个问题,使用copying函数赋值它们,它们会编程null,而赋值所得的指针将取得资源的唯一拥有权。

        

std::auto_ptr pInv1(createInvestment()); // pInv1指向createInvestment返回物
std::auto_ptr pInv2(pInv1);              // pInv2指向对象,pInv1被设为Null

pInv1 = pInv2                                        // pInv1指向对象,pInv2被设为null

        auto_ptr替代方案:“引用计数型智慧指针”(reference-counting smart pointer RCSP)。RCSP也是个智能指针,持续追踪共有多少个对象指向某笔资源,并在无人指向它时自动删除该资源。它提供的行为类似垃圾回收,不同的是RCSPs无法打破环状引用(例如,两个其实已经没被使用的对象彼此互指。)

void f()
{
    ...
    std::tr1::shared_ptr  // 调用factory函数,使用pInv
    pInv(createInvestment());         // 经由shared_ptr析构函数自动删除pInv
    ...
}

        虽然该段代码与auto_ptr没有太大差别,但是使用shared_ptrs的复制行为会正常很多。

        autp_ptr和tr1::shared_ptr两者都在其析构函数内做delete而不是delete[]动作。所以可以使用vectory和string而不是array

        注意:

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

        2、两个常使用的RAII classes分别是tr1::shared_ptr和auto_ptr。前者通常是较好的选择,因为其copy行为比较直观。auto_ptr复制动作会使他指向Null。

十四、在资源管理类中小心coping行为

        当一个RAII对象被复制,一般有一下两个选择

        1、禁止复制。

        2、对底层资源使用“引用计数法”。可以使用tr1::shared_ptr成员变量。但是,shared_ptr当计数器为0时,是删除其所指物,但是如mutex这种,我们想要的释放动作是解除锁定而非删除。因此可以使用shared_ptr所谓的删除其。它是一个函数或函数对象。当引用次数为0时便被调用。

class Lock {
public:
    explicit Lock(Mutex* pm)    // 以某个Mutex初始化shared_ptr
        : mutexPtr(pm, unlock)  // 并以unlock函数为删除器
    {
        lock(mutexPtr.get());   // 条款15讲get
    }
private:
    std::tr1::shared_ptr mutexPtr;  // 使用shared_ptr
}                                          // 替换raw pointer

        3、复制底部资源,在复制资源管理对象时,进行深度拷贝。

        4、转移底部资源的拥有权。

        注意:

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

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

十五、在资源管理类中提供对原始资源的访问

        需要一个函数可将RAII class对象转换为其所含的原始资源。有两种方法,显示转换和隐式转换。

        tr1::shared_ptr和auto_ptr都提供一个get成员函数,用来执行显示转换,它会返回智能指针内部的原始指针。

std::tr1::shared_ptr pInv(createInvestment());
int daysHeld(const Investment* pi);   // 返回投资天数
int days = daysHeld(pInv);            // 错误!daysHeld入参为Investment*指针,
                                      // 但是传入的是一个tr1::shared_ptr的对象
int days = daysHeld(pInv.get());

        就像所有智能指针一样,tr1::shared_ptr和auto_ptr也重载了指针取值(pointer dereferencing) *** 作符(operator->和operator*),他们允许隐式转换至底部原始指针。

class Investment {                  // investment继承体系的跟类
public:
    bool isTaxFree() const;
    ...
};

Investment* createInvestment();     // factory 函数
std::tr1::shared_ptr pi1(createInvestment()); // 令tr1::shared_ptr管理一笔资源
bool taxablel = !(pil->isTaxFree());// 经由Operator->访问资源
...
std::auto_ptr pi2(createInvestment()); // 令std::auto_ptr管理一笔资源

bool taxable2 = !((*pi2).isTaxFree());

        注意:

        1、APIs往往要求访问原始资源(ras resources),所以每一个RAII class应该提供一个“去的其所管理之资源”的办法

        2、对原始资源的访问可能经由显示转换或隐式转换。一般而言显示转换会比较安全,但是隐式转换对客户比较方便。

十六、成对使用new和delete时要采取相同形式

        new:1、内存被分配出来(通过名为operator new的函数)

                   2、针对此内存会有一个(或更多)构造函数被调用

        delete:1、针对此内存会有一个(或更多)析构函数被调用,然后内存才被释放(通过名为operator delete的函数)

std::string* stringPtr1 = new std::string;
std::string* stringptr2 = new std::string[100];
...
delete stringPtr1;        // 删除一个对象
delete [] stringPtr2;     // 删除一个由对象组成的数组

        尽量不要对数组形式做typdefs动作。

        注意:如果调用new时使用[],必须在调用delete时也使用[]。如果new表达式中不使用[],也不要在相应delete中使用[]。

十七、以独立语句将newed对象置入只能指针
int priority();
void processWidget(std::tr1::shared_ptr pw, int priority);

processWidget(std::tr1::shared_ptr(new Widget), priority());

        上述调用有可能产生资源泄漏,编译器在产出processWidget调用码之前,必须首先核算即将被传递的各个实参。上述第二实参知识一个单纯的对priority函数的调用,但第一个实参std::tr1::shared_ptr(new Widget)由两部分组成:

        1、执行“new Widget”表达式。

        2、调用tr1::shared_ptr构造函数。

        于是在调用processWidget之前,编译器必须创建代码,做一下三件事。

        1、调用priority

        2、执行“new Widget”

        3、调用tr1::shared_ptr构造函数。

        而C++编译器以什么样的次序完成这些事情d性很大。他可以以2,1,3这样的次序调用,如果对priority的调用导致异常,这样“new Widget”返回的指针将会遗失,因为它尚未被置入tr1::shared_ptr内。因此发生资源泄漏,因为在资源被创建和资源被转换为资源管理对象两个时间点之间有可能发生异常干扰。

        解决方法:1、创建Widget

                          2、将它置入一个只能指针内,然后再把那个只能指针传给processWidget。

std::tr1::shared_ptr pw(new Widget); // 在单独语句内以只能指针存储newed对象
processWidget(pw, priority());

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

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/5579640.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-14
下一篇 2022-12-14

发表评论

登录后才能评论

评论列表(0条)

保存