More Effecitve C++
1. 译序(侯捷)........................................................................................................4
2. 导读........................................................................................................................5
2.1 本书所谈的 C++...........................................................................................6
2.2 惯例与术语....................................................................................................7
2.3 臭虫报告,意见提供,内容更新................................................................9
3. 基础议题............................................................................................................. 10
3.1 ITEM M1:指针与引用的区别 ...................................................................... 10
3.2 ITEM M2:尽量使用 C++风格的类型转换.................................................. 13
3.3 ITEM M3:不要对数组使用多态 .................................................................. 17
3.4 ITEM M4:避免无用的缺省构造函数 .......................................................... 20
4. 运算符................................................................................................................. 25
4.1 ITEM M5:谨慎定义类型转换函数 .............................................................. 26
4.2 ITEM M6:自增(INCREMENT)、自减(DECREMENT)操作符前缀形式与后缀形
式的区别................................................................................................................. 33
4.3 ITEM M7:不要重载“&&”,“||”,
或“,”.............................................................. 36
4.4 ITEM M8:理解各种不同含义的 NEW 和 DELETE........................................ 39
5. 异常..................................................................................................................... 45
5.1 ITEM M9:使用析构函数防止资源泄漏 ...................................................... 46
5.2 ITEM M10:在构造函数中防止资源泄漏 .................................................... 51
5.3 ITEM M11:禁止异常信息(EXCEPTIONS)传递到析构函数外 ................ 61
5.4 ITEM M12:理解“抛出一个异常”与“传递一个参数”或“调用一个虚函数”间的
差异......................................................................................................................... 64
5.5 ITEM M13:通过引用(REFERENCE)捕获异常......................................... 71
5.6 ITEM M14:审慎使用异常规格(EXCEPTION SPECIFICATIONS) .................... 76
5.7 ITEM M15:了解异常处理的系统开销 ........................................................ 82
6. 效率..................................................................................................................... 84
6.1 ITEM M16:牢记 80-20 准则(80-20 RULE)....................................... 85
6.2 ITEM M17:考虑使用 LAZY EVALUATION(懒惰计算法)........................... 87
6.3 ITEM M18:分期摊还期望的计算 ................................................................ 96
6.4 ITEM M19:理解临时对象的来源 ..............................................................101
6.5 ITEM M20:协助完成返回值优化 ..............................................................104
6.6 ITEM M21:通过重载避免隐式类型转换 ..................................................108
6.7 ITEM M22:考虑用运算符的赋值形式(OP=)取代其单独形式(OP)110
6.8 ITEM M23:考虑变更程序库 ...................................................................... 113
6.9 ITEM M24:理解虚拟函数、多继承、虚基类和 RTTI 所需的代价 ....... 116
7. 技巧(TECHNIQUES,又称 IDIOMS 或 PATTERN)............................125
7.1 ITEM M25:将构造函数和非成员函数虚拟化 ..........................................125
7.2 ITEM M26:限制某个类所能产生的对象数量 ..........................................130
7.3 ITEM M27:要求或禁止在堆中产生对象 ..................................................146
7.4 ITEM M28:灵巧(SMART)指针 ...............................................................156
7.5 ITEM M29:引用计数 ..................................................................................173
7.6 ITEM M30:代理类 ......................................................................................205
7.7 ITEM M31:让函数根据一个以上的对象来决定怎么虚拟 ......................219
8. 杂项...................................................................................................................243
8.1 ITEM M32:在未来时态下开发程序 ..........................................................243
8.2 ITEM M33:将非尾端类设计为抽象类 ......................................................248
8.3 ITEM M34:如何在同一程序中混合使用 C++和 C..................................260
8.4 ITEM M35:让自己习惯使用标准 C++语言..............................................266
9. 附录...................................................................................................................273
9.1 推荐读物...................................................................................................273
9.2 一个 AUTO_PTR 的实现实例....................................................................277
9.3 在 C++ 中计算物件个数(OBJECTS COUNTING IN C++)译者:陈崴
9.4 为智能指标实作 OPERATOR->*(IMPLEMENTING OPERATOR->* FOR SMART
POINTERS)译者:陈崴......................................................................................293
281
1. 译序(侯捷)
C++ 是一个难学易用的语言!
C++ 的难学,不仅在其广博的语法,以及语法背後的语意,以及语意背後
的深层思维,以及深层思维背後的物件模型;C++ 的难学,还在於它提供了四
种不同(但相辅相成)的程式设计思维模式:procedural-based,object-based,
object-oriented,generic paradigm。
世上没有白吃的午餐。又要有效率,又要有弹性,又要前瞻望远,又要回
溯相容,又要能治大国,又要能烹小鲜,学习起来当然就不可能太简单。
在如此庞大复杂的机制下,万千使用者前仆後续的动力是:一旦学成,妙
用无穷。C++ 相关书籍之多,车载斗量;如天上繁星,如过江之鲫。广博如四
库全书者有之(The C++ Programming Language、C++ Primer),深奥如重山
复水者有之(The Annotated C++ Reference Manual, Inside the C++ Object
Model),细说历史者有之(The Design and Evolution of C++, Ruminations on
C++),独沽一味者有之(Polymorphism in C++, Genericity in C++),独树一帜
者有之(Design Patterns,Large Scale C++ Software Design, C++ FAQs),程
式 库 大 全 有 之 ( The C++ Standard Library), 另 辟 蹊 径 者 有 之 (Generic
Programming and the STL),工程经验之累积亦有之(Effective C++, More
Effective C++, Exceptional C++)。
这其中,「工程经验之累积」对已具C++ 相当基础的程式员而言,有著致
命的吸引力与立竿见影的帮助。Scott Meyers 的 Effective C++ 和 More
Effective C++ 是此类佼佼,Herb Sutter 的 Exceptional C++ 则是後起之秀。
这类书籍的一个共通特色是轻薄短小,并且高密度地纳入作者浸淫於
C++/OOP 领域多年而广泛的经验。它们不但开展读者的视野,也为读者提供各
种 C++/OOP 常见问题或易犯错误的解决模型。某些小范围主题诸如「在 base
classes 中使用 virtual destructor」、「令operator= 传回*this 的 reference」,可
能在百科型 C++ 语言书籍中亦曾概略提过,但此类书籍以深度探索的方式,让
我们了解问题背後的成因、最佳的解法、以及其他可能的牵扯。至於大范围主题,
例如 smart pointers, reference counting, proxy classes,double dispatching, 基
本上已属 design patterns 的层级!
这些都是经验的累积和心血的结晶。
我很高兴将以下三本极佳书籍,规划为一个系列,以精装的形式呈现给您:
1. Effective C++ 2/e, by Scott Meyers, AW 1998
2. More Effective C++, by Scott Meyers, AW 1996
3. Exceptional C++, by Herb Sutter, AW 1999
不论外装或内容,中文版比其英文版兄弟毫不逊色。本书不但与原文本页页
对译,保留索引,并加上精装、书签条、译注、书籍交叉参考 1、完整范例码 2、
读者服务 3。
这套书对於您的程式设计生涯,可带来重大帮助。制作这套书籍使我感觉非
常快乐。我祈盼(并相信)您在阅读此书时拥有同样的心情。
侯捷 2000/05/15 于新竹.台湾
jjhou@ccca.nctu.edu.tw
http://www.jjhou.com
1 Effective C++ 2/e 和 More Effective C++ 之中译,事实上是以 Scott Meyers
的另一个产品 Effective C++ CD 为本,不仅资料更新,同时亦将 CD 版中两书
之交叉参考保留下来。这可为读者带来旁徵博引时的莫大帮助。
2 书 中 程 式 多 为 片 段 。 我 将 陆 续 完 成 完 整 的 范 例 程 式 , 并 在 Visual
C++,C++Builder,GNU C++ 上测试。请至侯捷网站(http://www.jjhou.com)下
载。
3 欢迎读者对本书范围所及的主题提出讨论,并感谢读者对本书的任何误失提出
指正。来信请寄侯捷电子信箱(jjhou@ccca.nctu.edu.tw)。
2. 导读
对 C++ 程式员而言,日子似乎有点过於急促。虽然只商业化不到 10 年,
C++ 却俨然成为几乎所有主要电算环境的系统程式语言霸主。面临程式设计方
面极具挑战性问题的公司和个人,不断投入 C++ 的怀抱。而那些尚未使用 C++
的人,最常被询问的一个问题则是:你打算什么时候开始用 C++。C++ 标准化
已经完成,其所附带之标准程式库幅员广大,不仅涵盖 C 函式库,也使之相形
见绌。这么一个大型程式库使我们有可能在不必牺牲移植性的情况下,或是在不
必从头撰写常用演算法和资料结构的情况下,完成琳琅满目的各种复杂程式。
C++ 编译器的数量不断增加,它们所供应的语言性质不断扩充,它们所产生的
码品质也不断改善。C++ 开发工具和开发环境愈来愈丰富,威力愈来愈强大,
稳健强固(robust)的程度愈来愈高。商业化程式库几乎能够满足各个应用领域
中的写码需求。
一旦语言进入成熟期,而我们对它的使用经验也愈来愈多,我们所需要的
资讯也就随之改变。1990 年人们想知道 C++ 是什么东西。到了 1992 年,他
们想知道如何运用它。如今 C++ 程式员问的问题更高级:我如何能够设计出适
应未来需求的软体?我如何能够改善程式码的效率而不折损正确性和易用性?
我如何能够实作出语言未能直接支援的精巧机能?
这本书中我要回答这些问题,以及其他许多类似问题。
本书告诉你如何更具实效地设计并实作 C++ 软体:让它行为更正确;面对
异常情况时更稳健强固;更有效率;更具移植性;将语言特性发挥得更好;更优
雅地调整适应;在「混合语言」开发环境中运作更好;更容易被正确运用;更不
容易被误用。简单地说就是如何让软体更好。
本书内容分为 35 个条款。每个条款都在特定主题上精简摘要出 C++ 程式
设计社群所累积的智慧。大部份条款以准则的型式呈现,附随的说明则阐述这条
准则为什么存在,如果不遵循会发生什么後果,以及什么情况下可以合理违反该
准则。所有条款被我分为数大类。某些条款关心特定的语言性质,特别是你可能
罕有使用经验的一些新性质。例如条款 9~15 专注於 exceptions(就像 Tom
Cargill, Jack Reeves, Herb Sutter 所发表的那些杂志文章一样)。其他条款解释
如何结合语言的不同特性以达成更高阶目标。例如条款 25~31 描述如何限制物
件的个数或诞生地点,如何根据一个以上的物件型别产生出类似虚拟函式的东
西,如何产生smart pointers 等等。其他条款解决更广泛的题目。条款16~24 专
注於效率上的议题。不论哪一条款,提供的都是与其主题相关且意义重大的作法。
在 More Effective C++一书中你将学习到如何更实效更精锐地使用 C++。大部份
C++ 教科书中对语言性质的大量描述,只能算是本书的一个背景资讯而已。
这种处理方式意味,你应该在阅读本书之前便熟悉 C++。我假设你已了解
类别(classes)、保护层级(protection levels)、虚拟函式、非虚拟函式,我也
假设你已通晓 templates 和 exceptions 背後的概念。我并不期望你是一位语言
专家,所以涉及较罕见的 C++ 特性时,我会进一步做解释。
2.1 本书所谈的 C++
我在本书所谈、所用的 C++,是 ISO/ANSI 标准委员会於 1997 年 11 月
完成的 C++国际标准最後草案(Final Draft International Standard)。这暗示了
我所使用的某些语言特性可能并不在你的编译器(s) 支援能力之列。别担心,我
认为对你而言唯一所谓「新」特性,应该只有 templates,而 templates 如今几
乎已是各家编译器的必备机能。我也运用 exceptions,并大量集中於条款 9~15。
如果你的编译器(s) 未能支援 exceptions,没什么大不了,这并不影响本书其他
部份带给你的好处。但是,听我说,纵使你不需用到 exceptions,亦应阅读条款
9~15,因为那些条款(及其相关篇幅)检验了某些不论什么场合下你都应该了
解的主题。
我承认,就算标准委员会授意某一语言特性或是赞同某一实务作法,并非
就保证该语言特性已出现在目前的编译器上,或该实务作法已可应用於既有的开
发环境上。一旦面对「标准委员会所议之理论」和「真正能够有效运作之实务」
间的矛盾,我便两者都加以讨论,虽然我其实比较更重视实务。由於两者我都讨
论,所以当你的编译器(s) 和 C++ 标准不一致时,本书可以协助你,告诉你如
何使用目前既有的架构来模拟编译器(s) 尚未支援的语言特性。而当你决定将一
些原本绕道而行的解决办法以新支援的语言特性取代时,本书亦可引导你。
注意当我说到编译器(s) 时,我使用复数。不同的编译器对 C++ 标准的满
足程度各不相同,所以我鼓励你在至少两种编译器(s) 平台上发展程式码。这么
做可以帮助你避免不经意地依赖某个编译器专属的语言延伸性质,或是误用某个
编译器对标准规格的错误阐示。这也可以帮助你避免使用过度先进的编译器技
术,例如独家厂商才做得出的某种语言新特性。如此特性往往实作不够精良(臭
虫多,要不就是表现迟缓,或是两者兼具),而且C++ 社群往往对这些特性缺
乏使用经验,无法给你应用上的忠告。雷霆万钧之势固然令人兴奋,但当你的目
标是要产出可靠的码,恐怕还是步步为营(并且能够与人合作)得好。
本书用了两个你可能不甚熟悉的 C++ 性质,它们都是晚近才加入 C++ 标
准之中。某些编译器支援它们,但如果你的编译器不支援,你可轻易以你所熟悉
的其他性质来模拟它们。
第一个性质是型别 bool,其值必为关键字 true 或 false。如果你的编译器
尚未支援 bool,有两个方法可以模拟它。第一个方法是使用一个 global enum:
enum bool { false, true };
这允许你将参数为 bool 或 int 的不同函式加以多载化(overloading)。缺点是,
内建的「比较运算子( comparison operators)」如==, <, >=, 等等仍旧传回 ints。
所以以下程式码的行为不如我们所预期:
void f(int);
void f(bool);
int x, y;
...
f( x < y ); // 呼叫 f(int),但其实它应该呼叫 f(bool)
一旦你改用真正支援 bool 的编译器,这种 enum 近似法可能会造成程式行
为的
改变。
另一种作法是利用 typedef 来定义 bool,并以常数物件做为 true 和 false:
typedef int bool;
const bool false = 0;
const bool true = 1;
这种手法相容於传统的 C/C++ 语意。使用这种模拟法的程式,在移植到一
个支援有 bool 型别的编译器平台之後,行为并不会改变。缺点则是无法在函式
多载化(overloading)时区分 bool 和 int。以上两种近似法都有道理,请选择最
适合你的一种。
第 二 个 新 性 质 , 其 实 是 四 个 转 型 运 算 子 : static_cast, const_cast,
dynamic_cast,和 reinterpret_cast。如果你不熟悉这些转型运算子,请翻到条款
2 仔细阅读其中内容。它们不只比它们所取代的 C 旧式转型做得更多,也更好。
书中任何时候当我需要执行转型动作,我都使用新式的转型运算子。
C++ 拥有比语言本身更丰富的东西。是的,C++ 还有一个伟大的标准程式
库(见条款E49)。我尽可能使用标准程式库所提供的string 型别来取代 char* 指
标,而且我也鼓励你这么做。string objects 并不比 char*-based 字串难操作,
它们的好处是可以免除你大部份的记忆体管理工作。而且如果发生exception 的
话(见条款 9 和 10),string objects 比较没有 memory leaks(记忆体遗失)的
问题。
实作良好的 string 型别甚至可和对应的 char* 比赛效率,而且可能会赢(条
款 29 会告诉你个中故事)。如果你不打算使用标准的string 型别,你当然会使
用类似 string 的其他 classes,是吧?是的,用它,因为任何东西都比直接使用
char* 来得好。
我将尽可能使用标准程式库提供的资料结构。这些资料结构来自 Standard
Template Library("STL" — 见条款 35)。STL 包含 bitsets, vectors, lists,
queues,stacks, maps, sets, 以及更多东西,你应该尽量使用这些标准化的资料
结构,不要情不自禁地想写一个自己的版本。你的编译器或许没有附 STL 给你,
但不要因为这样就不使用它。感谢 Silicon Graphics 公司的热心,你可以从 SGI
STL 网站下载一份免费产品,它可以和多种编译器搭配。
如果你目前正在使用一个内含各种演算法和资料结构的程式库,而且用得
相当愉快,那么就没有必要只为了「标准」两个字而改用STL。然而如果你在「使
用 STL」和「自行撰写同等功能的码」之间可以选择,你应该让自己倾向使用
STL。记得程式码的重用性吗?STL(以及标准程式库的其他组件)之中有许多
码是十分值得重复运用的。
2.2 惯例与术语
任何时候如果我谈到 inheritance(继承),我的意思是public inheritance(见
条款 E35)。如果我不是指public inheritance,我会明白地指明。绘制继承体系
图时,我对 base-derived 关系的描述方式,是从 derived classes 往 base
classes 画箭头。
例如,下面是条款 31 的一张继承体系图:
GameObject
SpaceShip
Asteroid
SpaceStation
这样的表现方式和我在 Effective C++ 第一版(注意,不是第二版)所采用
的习惯不同。现在我决定使用这种最广被接受的继承箭头画法:从 derived
classes 画往 base classes,而且我很高兴事情终能归於一统。此类示意图中,
抽象类别(abstract classes,例如上图的 GameObject)被我加上阴影而具象类
别(concrete classes,例如上图的 SpaceShip)未加阴影。
Inheritance(继承机制)会引发「pointers(或 references)拥有两个不同
型别」的议题,两个型别分别是静态型别(static type)和动态型别(dynamic
type)。Pointer 或 reference 的「静态型别」是指其宣告时的型别,「动态型别」
则由它们实际所指的物件来决定。下面是根据上图所写的一个例子:
GameObject *pgo = // pgo 的静态型别是 GameObject*,
new SpaceShip; // 动态型别是 SpaceShip*
Asteroid *pa = new Asteroid; // pa 的静态型别是 Asteroid*,
// 动态型别也是 Asteroid*。
pgo = pa; // pgo 的静态型别仍然(永远)是 GameObject*,
// 至於其动态型别如今是 Asteroid*。
GameObject& rgo = *pa; // rgo 的静态型别是 GameObject,
// 动态型别是 Asteroid。
这 些 例 子 也 示 范 了 我 喜 欢 的 一 种 命 名 方 式 。 pgo 是 一 个
pointer-to-GameObject ; pa 是 一 个 pointer-to-Asteroid ; rgo 是 一 个
reference-to-GameObject。我常常以此方式来为 pointer 和 reference 命名。
我很喜欢两个参数名称:lhs 和 rhs,它们分别是"left-hand side" 和"right-hand
side" 的缩写。为了了解这些名称背後的基本原理,请考虑一个用来表示分数
(rational numbers)的 class:
class Rational { ... };
如果我想要一个用以比较两个 Rational objects 的函式,我可能会这样宣
告:
bool operator==(const Rational& lhs, const Rational& rhs);
这使我得以写出这样的码:
Rational r1, r2;
...
if (r1 == r2) ...
在呼叫 operator== 的过程中,r1 位於"==" 左侧,被系结於 lhs,r2 位於
"=="右侧,被系结於 rhs。
我 使 用 的 其 他 缩 写 名 称 还 包 括 : ctor 代 表 "constructor" , dtor 代 表
"destructor",RTTI 代表 C++ 对 runtime type identification 的支援(在此性质
中,dynamic_cast 是最常被使用的一个零组件)。
当你配置记忆体而没有释放它,你就有了 memory leak(记忆体遗失)问
题。Memory leaks 在 C 和 C++ 中都有,但是在 C++ 中,memory leaks 所
遗失的还不只是记忆体,因为 C++ 会在物件被产生时,自动呼叫 constructors,