《Effective C++》第3章 资源管理(2)-读书笔记

章节回顾:

《Effective C++》第1章 让自己习惯C++-读书笔记

《Effective C++》第2章 构造/析构/赋值运算(1)-读书笔记

《Effective C++》第2章 构造/析构/赋值运算(2)-读书笔记

《Effective C++》第3章 资源管理(1)-读书笔记

《Effective C++》第3章 资源管理(2)-读书笔记

《Effective C++》第4章 设计与声明(1)-读书笔记

《Effective C++》第4章 设计与声明(2)-读书笔记

《Effective C++》第5章 实现-读书笔记

《Effective C++》第8章 定制new和delete-读书笔记


 

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

许多API直接指涉资源,所以除非你永远不用它们,否则都会绕过资源管理对象直接访问原始资源。假设使用tr1::shared_ptr管理对象。

std::tr1::shared_ptr<Investment> pInv(createInvestment());

函数daysHeld的声明是这样的:

int daysHeld(const Investment *pi);

下面这种调用方式,肯定是错误的:

int days = daysHeld(pInv);        //错误

因为函数需要的是指针,你传递是一个tr1::shared_ptr<Investment>对象。所以你需要一个函数将RAII对象转换为所内含的原始资源。有两种方法:隐式转换和显示转换。

(1)显示转换

tr1::shared_ptr和auto_ptr都提供了一个成员函数get返回内部的原始指针,这是显式转换。

int days = daysHeld(pInv.get());    //好的,没有问题

(2)隐式转换

tr1::shared_ptr和auto_ptr都重载了操作符operator->和operator*,这样就允许隐式转换到原始指针。举例:假设Investment类有个成员函数bool isTaxFree() const;那么下面的调用是OK的:

bool taxable1 = !(pInv->isTaxFree());        //好的,没有问题
bool taxable2 = !((*pInv).isTaxFree());        //好的,没有问题

现在的问题是,需要原始指针的地方(例如,函数形参),如何以智能指针代替。解决方法是:提供一个隐式转换函数。下面举个字体类的例子:

FontHandle getFont();                    //取得字体句柄
void releaseFont(FontHandle fh);        //释放句柄
class Font
{
public:
    explicit Font(FontHandle fh) : f(fh){}
    ~Font()
    {
        releaseFont(f);
    }
private:
    FontHandle f;
};

如果C API处理的是FontHandle而不是Font对象,当然你可以像tr1::shared_ptr和auto_ptr那样提供一个get()函数:

FontHandle get() const { return f; }    //显示转换函数

这样是可以的,但客户还是觉得麻烦,这时候定义一个隐式转换函数是必须的。

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

注意:假设你已经知道了隐式转换函数的用法。例如:必须定义为成员函数,不允许转换为数组和函数类型等。

完成了以上工作,对于下面这个函数的调用是OK的:

void changeFontSize(FontHandle f, int newSize);
Font f(getFont());
int newFontSize;
changeFontSize(f, newFontSize);        //好的,Font隐式转换为FontHandle了。

隐式类型转换也增加了一种风险。例如有以下代码:

Font f1(getFont());
FontHandle f2 = f1;        //将Font错写成FontHandle了,编译仍然通过。

f1被隐式转换为FontHandle,这时f1和f2共同管理某个资源,f1被销毁,字体释放,这时候你可以想象f2的状态(原谅我这个词我不会说),再销毁f2,必然会造成运行错误。通常提供一个显示转换get函数是比较好的,因为它可以避免非故意的类型转换的错误,这种错误估计会耗费你很长的调试时间(我遇到过的情况)。

请记住:

(1)有些API要求访问原始资源,所以每一个RAII class应该提供一个“取得其所管理的资源”的办法。

(2)对原始资源的访问可以通过显式转换或隐式转换。一般显式转换比较安全,但隐式转换对客户比较方便。


 

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

我相信你肯定一眼看出以下代码的问题:

std::string *stringArray = new std::string[100];
delete stringArray;

程序行为是未定义的。stringArray所含的100个string对象,99个可能没被删除,因为它们的析构函数没被调用。

delete必须要知道的是:删除的内存有多少个对象,决定了调用多少个析构函数。

单一对象和数组的内存布局肯定是不同的,数组占用的内存也许包含一个“数组大小”的记录。(编译器干的事)你可以告诉编译器删除的是数组还是单一对象:

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

这个规则很简单,但有一点需要注意:

typedef std::string AddressLines[4];
std::string *pal = new AddressLines;

delete pal;                //不好,行为未定义。
delete [] pal;            //很好。

所以,最好不要对数组做typedef。

请记住:如果new表达式中使用了[],必须在相应的delete表达式中使用[];如果new表达式中不使用[],一定不要在相应的delete表达式中使用[]。


 

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

假设有以下函数,具体含义我们可以先忽略:

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

先说明一点的是,当你以下面形式调用processWidget时,肯定是错误的:

processWidget(new Widget, priority());

编译出错。tr1::shared_ptr构造函数需要一个原始指针,但该构造函数是explicit,即禁止隐式类型转换的。所以,正常情况你应该这样调用:

processWidget(std::tr1::shared_ptr<Widget>(new Widget), priority());    //好的,没有问题

以这种调用方式,执行函数体之前,有三个工作要做:

(1)调用priority。

(2)执行"new Widget"。

(3)调用tr1::shared_ptr构造函数

可以肯定的是(2)在(3)前面执行,但(1)的执行次序不能确定。假设(1)在第二个被执行,则如果调用priority出现异常,new Widget返回的指针将会遗失,因为还未执行tr1::shared_ptr的构造函数。所以,发生了资源泄露。

避免这类问题很简单:使用分离语句。

std::tr1::shared_ptr<Widget> pw(new Widget);
processWidget(pw, priority());                //绝不会造成泄露

请记住:以独立语句将newd对象存储于智能指针内。如果不这样做,一旦异常被抛出,有可能造成难以察觉的资源泄露。

posted @ 2015-04-23 13:08  QingLiXueShi  阅读(936)  评论(0编辑  收藏  举报