More Effective C++:35个改善编程与设计的有效方法(中文版)
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

导读(Introduction)导读

Introduction

对 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++特性时,我会进一步解释。

本书所谈的 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:

这允许你将参数为 bool 或 int 的不同函数加以重载(overloading)。缺点是,内建的“比较操作符(comparison operators)”如==,<,>=,等仍旧返回 ints。所以以下代码的行为不如我们所预期:

一旦你改用真正支持 bool 的编译器,这种 enum 近似法可能会造成程序行为的改变。

另一种做法是利用 typedef 来定义 bool,并以常量对象作为 true 和 false:

这种手法与传统的 C/C++语义兼容。使用这种仿真法的程序,在移植到一个支持有 bool 类型的编译器平台之后,行为并不会改变。缺点则是无法在函数重载(overloading)时区分 bool 和 int。以上两种近似法都有道理,请选择最适合你的一种。

第二个新性质,其实是 4 个转型操作符: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(以及标准程序库的其他组件)之中有许多代码是十分值得重复运用的。

惯例与术语

任何时候如果我谈到 inheritance(继承),我的意思是 public inheritance(见条款 E35)。如果我不是指 public inheritance,我会明确地指出。绘制继承体系图时,我对 base-derived 关系的描述方式,是从 derived classes往 base classes 画箭头。例如,下面是条款 31的一张继承体系图。

这样的表现方式和我在 Effective C++ 第一版(注意,不是第二版)所采用的习惯不同。现在我决定使用这种最被广泛接受的箭头画法:从 derived classes 画往base classes,而且我很高兴事情终能归于一统。此类示意图中,抽象类(abstract classes,例如上图的 GameObject)被我加上阴影而具体类(concrete classes,例如上图的 SpaceShip)未加阴影。

Inheritance(继承)机制会引发“pointers(或 references)拥有两个不同的类型”的议题,两个类型分别是静态类型(static type)和动态类型(dynamic type)。Pointer或 reference 的“静态类型”是指其声明时的类型,“动态类型”则由它们实际所指的对象来决定。下面是根据上图所写的一个例子:

这些例子也示范了我喜欢的一种命名方式。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:

如果我想要一个“用来比较两个 Rational objects”的函数,我可能会这样声明:

这使我得以写出这样的代码:

在调用 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,而 constructors 本身可能亦配有资源(resources)。举个例子,考虑以下代码:

这段代码会泄漏内存,因为 pw 所指的 Widget 对象从未被删除。如果 Widget

constructor 分配了其他资源(例如 file descriptors,semaphores,window handles,

database locks),这些资源原本应该在 Widget 对象被销毁时释放,而现在也像内存一样都泄漏掉了。为了强调在 C++中 memory leaks 往往也会泄漏其他资源,我

在书中常以 resource leaks 一词取代 memory leaks。

你不会在本书中看到许多 inline 函数。并不是我不喜欢 inlining,事实上我相信 inline 函数是 C++的一项重要性质。然而决定一个函数是否应被 inlined,条件十分复杂、敏感,而且与平台有关(见条款 E33)。所以我尽量避免 inlining,除非其中有个关键点非使用 inlining 不可。当你在本书中看到一个 non-inline 函数,并不意味我认为把它声明为 inline 是个坏主意,而只是说,它“是否为 inline”与当时讨论的主题无关。

有一些传统的 C++性质已明确地被标准委员会排除。这样的性质被列于语言的最后撤除名单,因为新性质已经加入,取代那些传统性质的原本工作,而且做得更好。这本书中我会找出被撤除的性质,并说明其取代者。你应该避免使用被撤除的性质,但是过度在意倒也不必,因为编译器厂商为了挽留其客户,会尽力保存向下兼容性,所以那些被撤除的性质大约还会存活好多年。

所谓 client,是指你所写代码的客户。或许是某些人(程序员),或许是某些物(classes 或 functions)。举个例子,如果你写了一个 Date class(用来表现生日、最后期限、耶稣再次降临日等),任何使用了这个 class 的人,便是你的 client。任何一段使用了 Date class 的代码,也是你的 clients。Clients 是重要的,事实上clients 是游戏的主角。如果没有人使用你写的软件,你又何必写它呢?你会发现我很在意如何让 clients 更轻松,通常这会导致你的行事更困难,因为好的软件“以客为尊”。如果你讥笑我太过滥情,不妨反躬自省一下。你曾经使用过自己写的 classes或 functions 吗?如果是,你就是你自己的 client,所以让 clients 更轻松,其实就是让自己更轻松,利人利己。

当我讨论 class templates或 function templates,以及由它们所产生出来的 classes或 functions 时,请容我保留偷懒的权利,不一一写出 templates 和其 instantiations (实例)之间的差异。举个例子,如果 Array 是个 class template,有个类型参数 T,我可能会以 Array 代表此 template 的某个特定实例(instantiation)——虽然其实

Array<T> 才是正式的 class 名称。同样道理,如果 swap 是个 function template,

有个类型参数 T,我可能会以 swap 而非 swap<T> 表示其实例。如果这样的简短表示法在当时情况下不够清楚,我便会在表示 template 实例时加上 template参数。

臭虫报告,意见提供,内容更新

我尽力让这本书技术精准、可读性高,而且有用,但是我知道一定仍有改善空间。如果你发现任何错误——技术性的、语言上的、错别字,或任何其他问题——请告诉我,我会试着在本书重印时修正。如果你是第一位告诉我的人,我会很高兴将你的大名记录到本书致谢文(acknowledgments)内。如果你有改善建议,我也非常欢迎。

我将继续收集 C++程序设计的实效准则。如果你有任何这方面的想法并愿意与我分享,我会十分高兴。请将你的建议、你的见解、你的批评,以及你的臭虫报告,寄至:

或者你也可以发送电子邮件到 mec++@awl.com。

我整理了一份本书第一次印刷以来的修订记录,其中包括错误修正、文字修润,以及技术更新。你可以从本书网站取得这份记录及与本书相关的其他信息。你也可以通过 anonymous FTP,从 ftp.awl.com 的 cp/mec++目录中取得它。如果你希望拥有这份数据,但又无法上网,请寄申请函到上述地址,我会邮寄一份给你。

这篇序文够长的了,让我们开始正题吧。