过去十年中,有一件事最是奇特有趣:我於 1993 开始《无责任书评》专栏,介绍我所能够掌握的技术范围内的一些世界名着。这样的题材与文体,吸引了很多目光,也开创了某种先河。《无责任书评》夹杂对台湾电脑出版业的观点与评论,由於当时台湾电脑书的良窳程度极端不均(现在也是),初阶 滥而高阶贫血(现在也是),我以程序员的角度所给的评论显得尖锐不群。
1998 年网际网路兴盛,我把所有电脑散文都移到网路上发表,范围扩及学习方向与学习态度(但不涉及细节技术)。过去的书评文章也重新整理了起来。各位可从侯捷网站上看到所有这些文章。
书评之所以受人欢迎,一方面在它的知识性,一方面在它的辛辣味。通常我的原则是只评好书(该说是「荐」而不是「评」了),所以辛辣味只藏在旁徵博引的明喻暗讽之中,或偶尔忍不住的一把火。一般而言,只要有丰富的知识含量,而不是单纯地将章节照录一遍,书评专栏就够吸引人了,辛辣味只是附带红利。诸君如想尝尝真正的川辣子,看看国外期刊的书评,肯定叫温良恭俭让的中国人频频抚胸,大惊失色。
好书之於学习(尤其是自修),重要性自不待言,所以书评永远受欢迎。好书是一支钓杆,好书评则让你认识这支钓杆并告诉你到哪儿去买。单一书评固然好,如果能系列化、系统化、根据技术的演进与层次,铺陈一条学习的红地毯,就更有价值。过去我曾经分篇为台湾读者介绍过 C++/OOP 方面的许多好书。做为本专栏的第一篇,我决定将它们汇总结集,让你一次看饱买足。
●阅读之前
往下阅读之前,我想先谈一些打底的话。
第一,以下介绍的全都是外来书。各位购买这些书籍或许有经济压力,但毕竟它们都是成名已久的世界名着,我想,为读者开这扇窗绝对是很重要的。
购买这些书籍其实很方便,只要你有信用卡,连上亚马逊网路书店(www.amazon.com)爱怎麽买就怎麽买。我们的困难可能在於信用卡和书价。唔,加上运费真的很贵。
第二,在我少不更事的时候,读了一本好书并不会回头特别记下作者姓名。这是个绝对错误的态度。茫茫书海中该如何选书?第一次当然是到书店去乱枪打鸟,浪费一点子弹。但是你不能老停留在少不更事的阶段,你的子弹还有你的书架空间都很宝贵,你的时间更宝贵。牢记优秀作家的名字,是找好书的捷径。这其实也是写阅环境的一个进步表徵:让好作家有自己的品牌。
第三,OO(Object-Oriented,物件导向)领域,从编程到设计,可概分为 OOP(Progrmming)、OOA(Analysis)、OOD(Design)。目前国外十分成熟的 UML(Unified Modeling Language)属於OO 领域里头用来将设计概念表现出来的一种 notation(符号表现法)。本篇文章只介绍到 OOP 这个层次(唯 [Gamma95]稍属例外),这比较具体,也比较贴近大部份程序员。愈往上去愈抽象,愈接近软体工程或方法论。
第四,以下介绍的这些 C++/OOP 书籍,几乎成为我初步判断一个人在这方面水平的基准。一个具备数年经验的 C++ 程序员,或许自己能够摸索出「总是让 base class 拥有 virtual destructor」这样的准则,但初出茅庐的程序员,恐怕连 virtual destructor 是什麽都不甚有概念,更别说该如何正确运用它。如果他说他看过 [Meyers98],我会比较放心他的水平。
有趣的是,我曾经在自己班上(学生从大二到研究生都有)做了一个调查。拥有这些书籍的学生人数并不多,而且老是同样几位。这让我感觉,强者恒强弱者恒弱,悲夫。就我和业界的广泛接触经验,我也发现,许多程序员离开学校後就不太看书了,或者因为忙碌,或者因为安於现况。专案做了不少,技术却没有精进太多。三两下招数一再用老,人特别容易空乏。请你打开这扇窗,你会发现巨着之所以为巨着,专家经验之所以为专家经验,是有道理的。愈是看了这些书,你愈会发现这些书的价值,并觉醒过去的一些愚蠢行为。
第五,下面开出来的书单都是我熟读过的,其中甚且不少繁体中文版是我翻译的,所以我放心推荐并接受质询。然而书海浩瀚,遗珠难免。
第六,为求方便,以下以学术界习惯的标示法,标示书籍代名。文中即使用这些代名。凡有中文版者,我会特别加注。
[Ellis90]: The Annotated C++ Reference Manual, by Margaret A. Ellis and Bjarne Stroustrup, Addison-Wesley, 1990. 447 pages.
[Gamma95]: Design Patterns: Elements of Reusable Object-Oriented Software,
by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides, Addison-Wesley, 1995. 395 pages
简体中文版:《设计模式》,李英军等译,机械工业出版社,2000. 254 页
[Lippman98]: C++ Primer, 3rd Editoin, by Stanley Lippman and Josee Lajoie,
Addison Wesley Longman, 1998. 1237 pages.
繁体中文版:《C++ Primer 中文版》,侯捷译, 峰 1999. 1237 页
[Lippman96]: Inside the C++ Object Model, by Stanley Lippman, Addison Wesley Longman, 1996. 280 pages
繁体中文版:《深度探索 C++ 物件模型》,侯捷译, 峰 1998. 320 页
[Meyers96]: More Effective C++, by Scott Meyers, Addison-Wesley, 1996. 318 pages
繁体中文版:《More Effective C++ 中文版》,侯捷译,培生 2000. 318 页
[Meyers98]: Effective C++, Second Edition, by Scott Meyers. Addison Wesley Longman, 1998. 256 pages
繁体中文版:《Effective C++ 2/e 中文版》侯捷译,培生 2000. 256 页
[Struostrup97]: The C++ Programming Language, 3rd Editoin, by Bjarne Stroustrup, Addison Wesley Longman, 1997. 910 pages
繁体中文版:《C++ 程式语言经典本》,叶秉哲译,儒林 1999.(未录总页数)
[Sutter99]: Exceptional C++, by Herb Sutter, Addison Wesley Longman, 2000. 208 pages
繁体中文版:《Exceptional C++ 中文版》侯捷译,培生 2000. 248 页
●层级一:语法/语意(C++)
学习语言,当然首先从语法开始。初学者究竟要从轻松小品出发,或一开始就接触巨着,殊无定论,因为初学者有很多种,「初学者」一词却无法反映他们的真实状态。我的学生群中有 13 岁的,也有 31 岁的(年纪更大的当然也有),有人连电脑基本概念都尚未建立,有人已是经验丰富的软体工程师。不同背景、不同年龄、不同领悟力、不同学习速度的人,需要不同层级的教材来满足他们。同一个人在不同阶段,也需要不同层面的教材来提升其功力与视野。
但,不论新生或老手(新生有一天会变成老手),任何一位C++ 程序员,我都强烈建议你的书架上要有[Lippman98] 和 [Struostrup97] 这两本书。它们是 C++ 语法/语意层面的百科全书;所有相关问题,这两本书都是最後仲裁,说了算!它们不适合连电脑基本概念都缺乏的人,但颇为适合已有编程经验的人。
这两本书都已经在 C++ 领域驰逞十年。最新版本都是第三版,印映 1998 定案的 C++ 标准规格。由於 C++ 标准规格带入一个十分庞大的标准程式库,所以这两本书也都比其前一版有巨幅的改变。如果要拿这两本书做特性比较,我的个人观感是,[Lippman98] 适合做为教本,教学自修叁考皆宜,[Struostrup97]比较生涩难读,学术味重,叁考性浓厚,权威性最高(毕竟 Struostrup 创造了 C++)。
这两本书的每一版寿命大约是五年。C++ 标准规格定案後,国际标准组织(ISO)每五年开会覆审一次,所以第三版至少也是五年寿命。也许有人以为,做为疑问辩论的最终裁判,还是以 C++ 规格书为准(1998/09/01 出版,编号 ISO/IEC 14882),而且网路下载只需 18 美元,PDF 格式,索引极为方便。这当然是很好的一份工具,但是这份文件绝不适合做为学习材料,太硬了。
目前全世界还没有任何一个 C++ 编译器支援完整的 C++ 标准规格,互有长短,所以晚近新增的语言特性,不见得能够在你手上的编译器演练。关於 Visual C++, Borland C++, CYGNUS C++ 三套工具在C++ 标准规格上的表现,我个人有一些经验,整理於http://www.jjhou.com/qa-cpp-primer-27.txt。为什麽迟迟未有完全支援标准规格的 C++ 编译器问世呢?因为这已经不成为市场竞争重点;C++ 开发工具市场已经转到对视窗介面的支援以及对企业的完整解决方案。
这两本书深具工具叁考价值,因此索引格外重要。两本繁体版译本皆用心地制作了索引(仍以英文术语来排列),此可为大陆借镜。中文电脑书带有索引,在台湾亦不常见,两本译本皆因译者的特别用心才得如此。索引采用英文术语,导出一个问题:如果书内文本没有保留英文术语,怎麽办?索引的制作与配套办法,是科技翻译亟需深思的一个问题。我的作法是,把诸多科技术语保留原文不译,并努力维持中英页页对照,这麽一来原书索引就可以完整而轻松地保留下来。保留原文术语不译,不完全是为了索引的制作,而是因为某些字眼强译为中文,不但与业界习惯脱节,也与世界脱节。我所采行的这种作法受到很多读者的喜爱,但是哪些原文术语要保留,哪些要中译,又是见仁见智。大凡如果译者真正是业内人士,他的选择不会脱离业界习惯太远。
●层级二:专家经验(C++/OOP)
能够在学习语法并开始练习编程的同时,就接触专家的经验,最是理想,但实际上很难如此。一方面,每一条经过淬炼的编程规则,其来龙去脉可能牵涉到多方面的知识,甚至可能涉及底层技术,这对新手的负担过重。另一方面,初学者往往只顾眼前半亩田,眼光没太高远。不过,如果有良师带引,依样画葫芦不失为一种初期的权宜学习方式。
无论如何,为了提升自己的 OOP 功力,专家经验是一条终南捷径,让你一次吸取高手十数年功力。[Meyers96] 和[Meyers98] 是我极为推荐的两本专家经验书。以下试摘书中条款数例,诸君可掂掂自己的斤两,看看自己平时实践了多少,从各条款中又联想了多少。
⊙以下摘自 [Meyers98]:
条款1:尽量以 const 和 inline 取代 #define
条款2:尽量以 <iostream> 取代 <stdio.h>
条款3:尽量以 new 和 delete 取代 malloc() 和 free()
条款5:使用相同型式的 new 和 delete
条款6:记得在 destructors 中以 delete 对付 pointer member
条款7:为记忆体不足的状况预做准备
条款8:撰写 operator new 和 operator delete 时,应奉行惯常行为
条款9:避免遮掩了 new 的正规型式
条款10:如果你写了 operator new,请对应写一个 operator delete
条款11:classes 内如果动态配置记忆体,请为它宣告一个 copy constructor 和一个 assignment 运算子
条款12:在 constructor 中尽量以 initialization 动作取代 assignment 动作
条款13:initialization list 中的 members 初始化排列次序应该和其在 class 内的宣告次序相同
条款14:总是让 base class 拥有 virtual destructor
条款15:令 operator= 传回 *this 的 reference
条款16:在 operator= 中为所有的 da
条款17:在 operator= 中检查是否「自己赋值给自己」
条款19:区分 member functions, non-member functions 和 friend functions 三者
条款20:避免将 da
条款21:尽可能使用 const
条款22:尽量使用 pass-by-reference(传址),少用 pass-by-value(传值)
条款23:当你必须传回一个 object 时,不要尝试传回一个 reference
条款29:避免传回内部资料的 handles
条款30:避免写出「传回 non-const pointers 或 references 并以之指向较低存取层级之 members」的 member functions
条款31:千万不要传回「函式内的 local 物件的 reference」,或是「函式中以 new 获得的指标的所指物件」。
条款32:尽可能延缓变数定义式的出现
条款33:明智地运用 inlining
条款34:将档案之间的编译相依关系(compilation dependencies)降至最低
条款35:确定你的 public inheritance 模塑出 "isa" 的关系
条款36:区分「介面继承(interface inheritance)」和「实作继承(implementation inheritance)」
条款37:绝对不要重新定义一个继承而来的非虚拟函式
条款38:绝对不要重新定义一个继承而来的预设叁数值
条款39:避免在继承体系中做 cast down(向下转型)动作
条款40:透过 layering(分层技术)来模塑 has-a 或 is-implemented-in-terms-of 的关系
条款41:区分 inheritance 和 templates
条款42:明智地运用 private inheritance(私有继承)
条款43:明智地运用多重继承(multiple inheritance,MI)
条款45:知道 C++(编译器)默默为我们完成和呼叫哪些函式
条款47:使用 non-local static objects 之前,确定它已有初值
条款49:尽量让自己熟悉 C++ 标准程式库
⊙以下摘自 [Meyers96]:
条款1:仔细区别 pointers 和 references
条款2:最好使用 C++ 转型运算子
条款3:绝对不要以 polymorphically(多型)方式来处理阵列
条款4:非必要不使用 default constructor
条款5:对自定的型别转换函式保持警觉
条款6:区别 increment/decrement 运算子的前序(prefix)和後序(postfix)型式
条款7:千万不要多载化 &&, ||, 和 , 运算子
条款8:了解各种不同意义的 new 和 delete
条款9:利用 destructors 避免遗失资源
条款10:在 constructors 内阻止资源遗失(resource leaks)
条款11:禁止异常讯息(exceptions)流出 destructors 之外
条款12:了解「丢出一个 exception」与「传递一个叁数」或「呼叫一个虚拟函式」之间的差异
条款13:以 by reference 方式捕捉 exceptions
条款15:了解异常处理(exception handling)的成本
条款17:考虑使用 lazy evaluation
条款18:分期摊还预期的计算成本
条款19:了解暂时物件的来源
条款20:协助完成「传回值最佳化(RVO)」
条款21:利用多载化技术(overload)避免隐式型别转换
条款22:考虑以运算子的复合型式(op=)取代其独身型式(op)
条款24:了解 virtual functions、multiple inheritance、virtual base classes、
runtime type identification 所需的成本
条款25:将 constructor 和 non-member functions 虚拟化
条款26:限制某个 class 所能产生的物件数量
条款27:要求(或禁止)物件产生於 heap 之中
条款28:Smart Pointers(精灵指标)
条款29:Reference counting(叁用计数)
条款30:Proxy classes(替身类别、代理人类别)
条款31:让函式根据一个以上的物件型别来决定如何虚拟化
条款33:将非尾端类别(non-leaf classes)设计为抽象类别(abstract classes)
其中条款
25~31 层次甚高,用来解决 C++ 软体开发过程中一再出现的问题,作者把这类问题及其解法称为 idioms (惯用法)或 patterns (样式),与着名的 23 个精典 patterns (见 [Gamma95] )相呼应。虽然这里所谈的规模格局部都比较小,但正因为如此,作者得以完成比较具体的实现,反而比 [Gamma95] 容易阅读。 [Meyers96] 和 [Meyers98] 这两本书成名已久,获得极大的声誉。以下的赞美可以使你更了解这两本书的性质和价值:
◆在你开始着手第一个真正的 C++ 专案之前,你应该阅读本书;在你获得一些实务经验之後,你应该再读一遍。-- comp.lang.c++
◆作者不只提供你撰写 C++ 码时应该遵循的明白规则,也提供了深入的解释与范例。-- Sun Expert
◆每一位 C++ 程式员不只应该拥有这本书,而且应该确实运用这本书。书中文字极易拿来实际运用,交叉叁考与索引的功夫做得很好。-- Computer Language
◆这本绝妙好书提供的招数,帮助我们把 C++ 运用得更好。每一位 C++ 程式员桌上都应该有这本书。在提升 C++ 程式设计的整体品质上, Scott Meyers 这份珍贵的礼物或许比业内任何人士的贡献都大。-- Jesse Liberty, C++ Report
[Sutter99] 是另一本专家经验谈。作者是 C++ Report 期刊主编,并主持网路上一个名为每周之星(a Guru of the Week,GotW)的 C++ 特别节目。以他的背景和经历,接触的疑难杂症自然是又多又猛。这本书整理了 47 个条款,由於条款名称无法表现某种具体准则,所以我不条列於此。本书主要分为八大项:
1. 泛型程式设计与 C++ 标准程式库
2. Exception-Safety(异常发生时仍安全)的主题与相关技术
3. Class 的设计与继承
4. 编译器防火墙(Firewalls)及 Pimpl 惯用手法
5. 名称查询、命名空间、介面原则
6. 记忆体管理
7. 陷阱、易犯错误与有害作法
8. 杂项主题
Scott Meyers 为此书所写的序,点出了这本书的特质:
『从语言的特性到标准程式库内的组件,再到程式编写技术,本书在不同的主题之间跳跃,总是使你稍稍失去平衡,总是使你必须付出全然的注意力。...我把 GotW 发音为 "Gotcha"(意思是「这下可逮到你了」),或许很适当。当我把书中测验的(我的)答案拿来和 Sutter 的答案比较,我掉进他(和 C++)铺设的陷阱中 ─ 虽然我实在不想承认这点。我几乎可以看见 Herb 微笑并温柔地对我所犯的每一个错误说 "Gotcha!"。...当你选择 C++ 做为工具,你必须小心地思考你正在做些什麽。C++ 是一个威力强大的语言,用来协助解决吃力的问题,其重要性使你必须尽可能面对语言本身、程式库、程式惯用手法来磨炼你的知识。』
就我的英文程度而言,[Sutter99] 读起来不若 [Meyers96] 和 [Meyers98] 那般平顺,原因是其中用了很多厘语、口语、典故。举个例子,Morphy law 是什麽,大家知道吗?(莫菲定律说:会出错的,一定会出错。)Machiavelli 又代表了什麽意思?(意大利政治家,以诈术闻名。)
这类专家经验谈,多半薄而贵,但贵得有价值。好消息是,[Meyers96] 和 [Meyers98] 已经集结为电子书,以光碟呈现,采用 HTML 格式,可使用任何支援 Java(以便进行全文检索)的浏览器阅读。多少钱一片?请上亚马逊瞧瞧。
●层级三:底层机制(C++ Object Model)
如果对於迥异传统编程方式的 C++ 特性,诸如 virtual functions、constructors、destructors┅等特异功能一直无法心领神会,可能有必要到内部机制去深度游历一番。不要以为钻到这麽深层的技术,会愈搞愈糊涂,愈搞愈恍忽。很多人,包括我自己,是在游历过底层机制一遍之後,才彻底觉悟并接受了 C++。
所谓底层机制主要是指 (1) object 的记忆体布局:da
知道了这些底层机制,你便能够对自己在 C++ 程式中的每一个动作所引发的影响,了如指掌。学习这些底层技术,不是为了自行开发一套编译器,而是为了彻底掌握 C++ 语言;底层技术的学习,只是过程,不是目标。这种情况和《深入浅出 MFC》(侯捷着,松岗 1997)的情况很像,数万名读者不是为了自行开发 framework 而欢喜阅读该书对 MFC 的剖析,是为了彻底掌握自己在撰写 MFC 应用程式时的一言一行。
底层机制方面的专论书籍非常稀少。我所仅见的两本,一是 [Ellis90],一是 [Lippman96]。前者被昵称为 ARM(带注解的叁考手册),是早期 C++ 编译器的实作依循准则,但因年代过远,我宁愿更推荐後者。[Lippman96] 笔误非常多,我翻译此书的过程中至少修正了100 个以上的笔误。
了解事务的本质,到底有没有必要?这个问题太简单了:如果你必须走那麽一遭,才能接受事务的表徵,那麽於你就有必要。如果你天生是个 OO 奇才,或你一开始接触的第一个语言就是OO 语言,以至於有可能认为其中的一切都是理所当然,可以完全领受各种特性的运用,那麽底层机制於你就不需要。
我个人是如此地真正第一线面对大量的学习者,就我的教学经验(乃至於我个人的学习经验),我要说,了解事务的本质,对绝大多数人都有极正面的帮助。关於这一点,我最喜欢引用林语堂先生在《朱门》一书里头的一句话:『只用一样东西,不明白它的道理,实在不高明。』
●层级四:设计观念的复用(C++/Patterns)
软体工程的所有努力,无非是为了美好的复用性(reusibility)。从早期的subroutines, procedures, functions, 到後来的 classes, templates。在在为了相同的目标。如今我们已经能够将「资料,以及处理资料的动作」封装得很好,甚至能够把资料型别都抽取出来成为叁数,甚至更进一步将资料本体和处理资料的各种演算法独立开来,各自发展而又能够藉着某种「黏胶」彼此作用(注)。
注:这便是所谓泛型编程(generic programming)的精神。下个月我为大家介绍这个主题。
很好,很好。但是长久以来我们却无法将设计概念以规格化的方式传承下去。面对资料结构(da
如果这些一再被反覆大量运用、并且早经众人淬炼出极佳作法的设计(一整组解决方案),能够系统化地分类整理,给定标准名称、定义、效果、实作法、甚至示例代码,我们就不必每次都从轮子造起(还造得不比专家圆呢)。如果程序员之间只要说Singleton, Proxy, Iterator, Flyweight,闻者马上知道其背後代表的是某种特定设计,有着特定的逻辑,用以解决某种特定问题,可多好。这正是将设计观念及其实作逻辑的宝贵经验,以简洁而可复用的形式表达出来。
[Gamma95] 一书内含精心整理的 23 个 design patterns。四位作者的主要贡献不在於 patterns 的创建,而在於 patterns 的整理形式与发扬光大。书中所提的 patterns 名称,几乎已经成为 OO 设计领域里头的标准辞汇。《程序员》去年 11 月份有一份蒋涛先生针对 [Gamma95] 的评论,其中对於 patterns 的比喻,令人激赏:『patterns 需要反复练习体会,才能应用自如。这有点像围棋中的定式,围棋定式是百年来高手下法的总结,但不能简单地应用,要看场合选择合适的定式,还要按棋理变通下法。』
幸运,真幸运,[Gamma95] 也有电子书出版,以光碟呈现,采用 HTML 格式,可使用任何支援 Java(以便进行全文检索)的浏览器阅读。
程式设计究竟是一门工匠技艺还是一门艺术?都可以是!看你从哪个角度出发。有人说连设计概念都可以以 patterns 规格化地传承,还谈什麽艺术?如果你是这样想,我说三件事给你听。建筑是一门技术还是艺术?很多人都认为建筑是一门艺术。然而 patterns 的概念正是滥觞於建筑设计领域。目前软体界所使用的 pattern 一词源自建筑理论大师 Christopher Alexander 的着作,他的书探讨的虽然都是建筑设计与都市规划的课题,但其精神与本质却适用其它领域,包括软体开发。另一件事是,软体界开始流行 framework 工具时,也有人认为程式主框架都被限死了,谈什麽设计与弹性?我说:只要馒头好吃,我从不在乎是机器馒头还是纯手工精制。你的设计精力应该放在专业领域如绘图、影像处理、统计、数学分析┅,而不是放在共通的基础框架上。如果你真的对基础共通的事务感兴趣,你不应走应用软体开发之路,应该将设计天份用来研究更新更好的资料结构,更新更好的演算法,更新更好的框架。
●线性学习?没的事!
虽然我把 C++/OOP 的学习阶段分为四层,但除了第四层得万事俱备才能水到渠成,其他三层的学习并不是那麽泾渭分明。通常你要你的C++ 坚轫锋利,你得让它历经多次回火,在高热和骤冷之间 炼,在学术与实用之间震荡。我无法为你画出一条单行道,你势必得走些回头路,时而品味一下曾经忽略的小花,时而啜饮一口被你遗忘的甘泉,填实了某种缝隙之後,才能神清气爽充实盈满地再出发。
-- the end