章节回顾:
《Effective C++》第1章 让自己习惯C++-读书笔记
《Effective C++》第2章 构造/析构/赋值运算(1)-读书笔记
《Effective C++》第2章 构造/析构/赋值运算(2)-读书笔记
《Effective C++》第3章 资源管理(1)-读书笔记
《Effective C++》第3章 资源管理(2)-读书笔记
《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对象存储于智能指针内。如果不这样做,一旦异常被抛出,有可能造成难以察觉的资源泄露。