前言
C++覆辙录
本书之渊薮乃是近20年的小小挫折、大错特错、不眠之夜和在键盘的敲击中不觉而过的无数周末。里面收集了普遍的、严重的或有意思的C++常见错误,共计九十有九。其中的大多数,(实在惭愧地说)都是我个人曾经犯过的。
术语“gotcha”1有其云谲波诡的形成历史和汗牛充栋的不同定义。但在本书中,我们将它定义为C++范畴里既普遍存在又能加以防范的编码和设计问题。这些常见错误涵盖了从无关大局的语法困扰,到基础层面上的设计瑕疵,再到源自内心的离经叛道等诸方面。
大约10年前,我开始在我教授的C++课程的相关材料中添加个别常见错误的心得笔记。我的感觉是,指出这些普遍存在的误解和误用,配合以正确的用法指导就像给学生打了预防针,让他们自觉地与这些错误作斗争,更可以帮助新入门的C++软件工程师避免重蹈他们前辈的覆辙。大体而言,这种方法行之有效。我也深受鼓舞,于是又收集了一些互相关联的常见错误的集合,在会议上作演讲用。未想这些演讲大受欢迎(或是同病相怜之故也未可知?),于是就有人鼓励我写一本“常见错误之书”。
任何有关规避或修复C++常见错误的讨论都涉及了其他的议题,最多见的是设计模式、习惯用法以及C++语言特征的技术细节。
这并非一本讲设计模式的书,但我们经常在规避或修复C++常见错误时发现设计模式是如此管用的方法。习惯上,设计模式的名字我们把每个单词的首字母大写,比如模板方法(Template Method)设计模式或桥接(Bridge)设计模式。当我们提及一种设计模式的时候,若它不是很复杂,则简介其工作机制,而详细的讨论则放在它们和实际代码相结合的时候才进行。除非特别说明,本书不提供设计模式的完全描述或极为详尽的讨论,这些内容可以参考Erich Gamma等人编写的Design Patterns一书。无环访问者(Acyclic Visitor)、单态(Monostate)和空件(Null Object)等设计模式的描述请参见Robert Martin编写的Agile Software Development一书。
从常见错误的视角来看,设计模式有两个可贵的特质。首先,它们描述了已经被验证成功的设计技术,这些技术在特定的软件环境中可以采用自定义的手法搞出很多新的设计花样。其次,或许更重要的是,提及设计模式的应用,对于文档的贡献不仅在于使运用的技术一目了然,同时也使应用设计模式的原因和效果一清二楚。
举例来说,当我们看到在一个设计里应用了桥接设计模式时,我们就知道在一个机制层里,一个抽象数据型别的实现并分解成了一个接口类和一个实现类。犹有进者,我们也知道了这样做是为了强有力地把接口部分同底层实现剥离,是故底层实现的改变将不会影响到接口的用户。我们不仅知道这种剥离会带来运行时的开销,还知道此抽象数据型别的源代码应该怎么安排,并知道很多其他细节。
一个设计模式的名字是关于某种技术极为丰富的信息和经验之高效、无疑义的代号。在设计和撰写文档时仔细而精确地运用设计模式及其术语会使代码洗练,也会阻止常见错误的发生。
C++是一门复杂的软件开发语言,而一种语言愈是复杂,习惯用法在软件开发中之运用就愈是重要。对一种软件开发语言来说,习惯用法就是常用的、由低阶语言特征构成的高阶语言结构的特定用法组合。总的来说,这和设计模式与高阶设计的关系差不多。是故,在C++ 语言里,我们可以直接讨论复制操作、函数对象、智能指针以及抛出异常等概念,而不需要一一指出它们在语言层面上的最低阶实现细节。
有一点要特别强调一下,那就是习惯用法并不仅仅是一堆语言特征的常见组合,它更是一组对此种特征组合之行为的期望。复制操作是什么意思呢?当异常被抛出的时候,我们能指望发生什么呢?大多数本书中的建议都是在提请注意以及建议应用C++编码和设计中的习惯用法。很多这里列举的常见错误常常可以直接视作对某种C++习惯用法的背离,而这些常见错误对应的解决方案则常常可以直接视作对某种C++习惯用法的皈依(参见常见错误10)。
本书在C++语言的犄角旮旯里普遍被误解的部分着了重墨,因为这些语言材料也是常见错误的始作俑者。这些材料中的某些部分可能让人有武林秘笈的感觉,但如果不熟悉它们,就是自找麻烦,在通往C++语言专家的阳关大道上也会平添障碍。这些语言死角本身研究起来就是其乐无穷,而且产出颇丰。它们被引入C++语言总有其来头,专业的C++软件工程师经常有机会在进行高阶的软件开发和设计时用到它们。
另一个把常见错误和设计模式联系起来的东西是,描述相对平凡的实例对于两者来说是差不多同等重要的。平凡的设计模式是重要的。在某些方面,它们也许比在技术方面更艰深的设计模式更为重要,因为平凡的设计模式更有可能被普遍应用。所以从对平凡设计模式的描述中获得的收益就会以杠杆方式造福更大范围的代码和设计。
差不多以完全相同的方式,本书中描述的常见错误涵盖了很宽范围内的技术困难,从如何成为一个负责的专业软件工程师的循循善诱(常见错误12)到避免误解虚拟继承下的支配原则的苦口良言(常见错误79)。不过,就与设计模式类似的情况看,表现得负责专业当然比懂得什么支配原则要对日复一日的软件开发工作来得受用。
第1章 基础问题常见错误
1:过分积极的注释
1.2:幻数
1.3:全局变量
1.4:未能区分函数重载和形参默认值
1.5:对引用的认识误区
1.6:对常量(性)的认识误区
1.7:无视基础语言的精妙之处
1.8:未能区分可访问性和可见性
1.9:使用糟糕的语言
1.10:无视(久经考验的)习惯用法
1.11:聪明反被聪明误
1.12:嘴上无毛,办事不牢
第2章 语法问题
2.1:数组定义和值初始化的语法形式混淆
2.2:捉摸不定的评估求值次序
2.3:(运算符)优先级问题
2.4 for语句引发的理解障碍
2.5:取大优先解析原则带来的问题
2.6:声明饰词次序的小聪明
2.7:“函数还是对象”的多义性
2.8:效果漂移的型别量化饰词
2.9:自反初始化
2.10:静态连接型别和外部连接型别
2.11:运算符函数名字查找的反常行为
2.12:晦涩难懂的operator ->