您的位置:首页 > 编程语言 > C语言/C++

读书笔记_Effective C++_资源管理

2015-05-05 09:57 218 查看
这个章节主要讲的是资源管理相关的知识,C++程序中最常使用的资源就是动态分配内存,但内存只是必须管理的众多资源之一,其他常见的资源还有文件描述器、互斥锁、图形界面的字型和笔刷、数据库连接、以及网络sockets。无论是哪一种资源,重要的是,当你不再使用它时,必须将它还给系统。

条款十三

[code]Investment* createInvestment();     //返回指针,指向Investment继承体系内的动态分配对象,调用者有责任删除它


通常调用这种factory函数,都需要在恰当的时候释放了pInv所指对象。

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


当很多意外的情况就会导致pInv无法释放,为了确保这一点,需要将资源放进对象中,当控制流离开f,该对象的析构函数会自动释放那些资源(把资源放进对象内,可以依赖C++的析构函数确保资源被释放)。

[code]void f()
{
    std::auto_ptr<Investment> pInv(createInvestment());
    ...
}


以对象管理资源的关键:

(1)获得资源后立刻放进管理对象内;

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

auto_ptr被销毁的时候会自动销毁所指的对象,所以应该要注意别让多个auto_ptr同时指向同一个对象。为了预防这个问题,auto_ptrs有一个不寻常的性质:若通过copy构造函数或copy assignment操作符复制它们,它们就会变成null,而复制所得的指针将获得资源的唯一拥有权。

[code]std::auto_ptr<Investment> pInv1(createInvestment());
std::auto_ptr<Investment> pInv2(pInv1); // 现在pInv2指向对象,pIn1被设为null
pInv1 = pInv2;  // 现在pInv1指向对象,pInv2被设为null


受auto_ptrs管理的资源必须绝对没有一个以上的auto_ptr同时指向它,这样的话STL容器就不能存放auto_ptr(STL容器要求其元素发挥正常的复制行为)。

而TR1的tr1::shared_ptr就是个RCSP:

[code]void f()
{
    ...
    std::tr1::shared_ptr<Investment> pInv1(createInvestment());

    std::tr1::shared_ptr<Investment> pInv2(pInv1); // pInv1和pInv2指向同一个对象
    pInv1(pInv2);   // 同理
    pInv1 = pInv2;  // 同理
    ...
}


所以tr1::shared_ptr可被用于STL容器等语境上。

auto_ptr和tr1::shared_ptr两者都在其析构函数内做delete而不是delete[]动作,意味着动态分配而得的array是无法使用的。一般对于动态分配数组都是使用vector和string,当然使用了Boost程序库的话,可以用boost::scoped_array和boost::shared_array。

对于C++动态分配数组,没有类似auto_ptr或tr1::shared_ptr那样的东西,甚至TR1中也没有。因为vector和string几乎总是可以取代动态分配而得的数组,在Boost里面有boost::scoped_array和boost::shared_array_classes。

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

auto_ptr和tr1::shared_ptr可以很好地管理heap-based的资源,但是对于非heap-based的资源就需要自己建立管理类来进行管理。

例如我们使用C API函数处理类型为Mutex的互斥器对象,共有lock和unlock两函数可用:

[code]void lock(Mutex* pm);   //锁定pm所指的互斥器
void unlock(Mutex* pm); //将互斥器解除锁定


为了确保不会忘记把一个被锁住的Mutex解锁,我们需要建立一个类来管理锁。

[code]class Lock {
public:
    explicit Lock(Mutex* pm) : mutexPtr(pm) {
        lock(mutexPtr); //获得资源
    }

    ~Lock() { unlock(mutexPtr); };  //释放资源
private:
    Mutex* mutexPtr;
};


目前的写法符合前一条的RAII方式,但如果Lock对象被复制要怎么处理?通常大家采用两种解决方案:

禁止复制:许多时候允许RAII对象被复制并不合理,所以应该像条款6一样直接禁止;

对底层资源使用引用计数:可以使用自己的引用计数器,也可以使用标准库中的tr1::shared_ptr,tr1::shared_ptr可以指定deleter,可以是一个函数或函数对象,当引用次数为0时便被调用(这个机制并不存在auto_ptr)。

[code]class Lock {
public:
    explicit Lock(Mutex* pm) : mutexPtr(pm, unlock) {
        lock(mutexPtr.get());
    }
private:
    std::tr1::shared_ptr<Mutex> mutexPtr;
};


这里就不要声明析构函数了,class析构函数会自动调用non-static成员变量的析构函数。

复制底部资源:某些标准字符串类型是由“指向heap内存”之指针构成(那内存被用来存放字符串的组成字符)。这种字符串对象内含一个指针指向一块heap内存。当这样一个字符串对象被复制,不论指针或其所指内存都会被***出一个复件。

Copying函数有可能被编译器自动创建出来,因此除非编译器所生版本做了你想要做的事,否则你得自己重写它们。

PS:

注意“Lock(Mutex* pm) : mutexPtr(pm)”的冒号声明,表示在调用Lock(Mutex* pm)之前,先调用mutexPtr(pm),这种写法强调了调用的时序。

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

资源管理类是处理资源泄露的很好的设计模式,但是很多情况下类的APIs会直接指涉资源,这样的APIs会直接绕过资源管理对象直接访问原始资源。

例如:int daysHeld(const Investment* pi);这样一个函数,int days = daysHeld(pInv);会显示编译错误,因为传递的是一个tr1::shared_ptr对象。这个时候需要进行显示转换或隐式转换。

tr1::shared_ptr和auto_ptr都提供了一个get成员函数,用来执行显示转换,int days = daysHeld(pInv.get());

tr1::shared_ptr和auto_ptr也重载了指针取值操作符(operator->和operator*),它们允许隐式转换至底部原始指针。

我们在进行封装的时候也有类似的情况,可以提供一个显示的get转换函数,但是这也迫使了使用者每次调用API的时候都需要调用get。这是可以提供一个隐式的转换函数,使得调用更加轻松和自然。

[code]class Font {
public:
    ...
    operator FontHandle() const
    { return f; }
    ...
};


对于许多设计良好的classes,它隐藏了使用者不需要看的部分,但备妥使用者需要的所有东西。

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

对于即将删除的那个指针,所指的是单一对象或对象数组?这是个必不可缺的问题,因为单一对象的内存布局一般而言不同于数组的内存布局。更明确地说,数组所用的内存通常还包括“数组大小”的记录,以便delete知道需要调用多少次析构函数。单一对象的内存则没有这笔记录。



当你对一个指针使用delete,唯一能够让delete知道内存中是否存在一个“数组大小记录”的办法就是:由你来告诉它,即使用delete时加上中括号,delete便认定指针指向一个数组,否则它便认定指针指向单一对象。

这个规则对于喜欢使用typedef的人也很重要,因为它意味typedef的作者必须说清楚,当使用者以new创建该种typedef类型对象时,该以哪一种delete形式删除之。

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

假设我们有个函数用来揭示处理程序的优先权,另一个函数用来在某动态分配所得的Widget上进行某些带有优先权的处理:

[code]int priority();
void processWidget(std::tr1::shared_ptr<Widget>pw, int priority);


现在调用processWidget,”processWidget(new Widget, priority());”,这样的调用形式是无法通过编译的。tr1::shared_ptr构造函数需要一个原始指针,但该构造函数是个explict构造函数,无法进行隐式转换。可以写成这样”pricessWidget(std::tr1::shared_ptr(new Widget), priority());”,然后这种写法可能会泄露资源。

编译器在产出一个processWidget调用代码之前,必须首先核算即将被传递的各个实参。在调用processWidget之前,编译器必须创建代码,做一下三件事:

调用priority

执行“new Widget”

调用tr1::shared_ptr构造函数

C++编译器以什么样的次序完成这些事情呢?弹性很大,这和其他语言如Java和C#不同,那两种语言总是以特定次序完成函数参数的核算。可以确定的是“new Widget”一定执行于tr1::shared_ptr的构造函数被调用之前,因为这个表达式的结果还要被传递作为tr1::shared_ptr构造函数的一个实参,但对priority的调用则可以排在第一或第二或第三执行。如果编译器选择以第二顺位执行它,最终获得这样的操作序列。

执行“new Widget”

调用priority

调用tr1::shared_ptr构造函数

现在,如果对priority的调用导致异常,“new Widget”返回的指针将会遗失,因为它尚未被置入tr1::shared_ptr内,后者是我们用来防止资源泄露的武器。在“资源被创建”和“资源被转换为资源管理对象”两个时间点之间可能发生异常干扰。

避免这类问题的办法很简单:使用分离语句,分别写出(1)创建Widget(2)将它置入一个智能指针内,然后再把那个智能指针传给processWidget。

[code]std::tr1::shared_ptr<Widget> pw(new Widget);
processWidget(pw, priority());


编译器对于“跨越语句的各项操作”没有重新排列的自由(只有在语句内才拥有那个自由度)。

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: