C++ 99 个常见错误

n344

贡献于2013-11-30

字数:0 关键词: C/C++开发 C/C++

HEWLETT-PACKARD C++:99 个常见错误 避免编码和设计中的常见问题 高博;Stephen C. Dewhurst; 最近修订日期 6/28/2007 本文档仅供审阅用户参考,在知识共享 署名-非商业性使用-禁止演绎 2.5 中国大陆许 可 前提下分发(详见 http://creativecommons.org/licenses/by-nc-nd/2.5/cn)。审阅用 户请发送审阅意见至 gaobo@cplusplus.cn,或致电(86)159-218-19896。 目录 常见错误 1:过分积极的注释 第 2 页 目录 目录................................................................................................................................................... 2 译者前言 ........................................................................................................................................... 5 前言................................................................................................................................................... 6 致谢................................................................................................................................................... 9 第一章 基础问题 ......................................................................................................................... 11 常见错误 1:过分积极的注释 .............................................................................................. 11 常见错误 2:幻数 .................................................................................................................. 13 常见错误 3:全局变量 .......................................................................................................... 15 常见错误 4:未能区分函数重载和形式参数默认值 .......................................................... 17 常见错误 5:对引用的认识误区 .......................................................................................... 19 常见错误 6:对常量(性)的认识误区 .............................................................................. 23 常见错误 7:无视基础语言的精妙之处 .............................................................................. 24 常见错误 8:未能区分可访问性和可见性 .......................................................................... 29 常见错误 9:使用糟糕的语言 .............................................................................................. 33 常见错误 10:无视(久经考验的)习惯用法 .................................................................... 35 常见错误 11:聪明反被聪明误 ............................................................................................ 38 常见错误 12:嘴上无毛,办事不牢 .................................................................................... 40 第二章 语法问题 ......................................................................................................................... 43 常见错误 13: ........................................................................................................................ 43 常见错误 14: ........................................................................................................................ 43 常见错误 15: ........................................................................................................................ 43 常见错误 16: ........................................................................................................................ 43 常见错误 17: ........................................................................................................................ 43 常见错误 18: ........................................................................................................................ 43 常见错误 19: ........................................................................................................................ 43 常见错误 20: ........................................................................................................................ 43 常见错误 21: ........................................................................................................................ 44 常见错误 22: ........................................................................................................................ 44 常见错误 23: ........................................................................................................................ 44 常见错误 24: ........................................................................................................................ 44 第三章 预处理器问题 ................................................................................................................. 44 常见错误 25: ........................................................................................................................ 44 常见错误 26: ........................................................................................................................ 44 目录 常见错误 1:过分积极的注释 第 3 页 常见错误 27: ........................................................................................................................ 44 常见错误 28: ........................................................................................................................ 44 第四章 强制型别转换问题 ......................................................................................................... 45 常见错误 29: ........................................................................................................................ 45 常见错误 30: ........................................................................................................................ 45 常见错误 31: ........................................................................................................................ 45 常见错误 32: ........................................................................................................................ 45 常见错误 33: ........................................................................................................................ 45 常见错误 34: ........................................................................................................................ 45 常见错误 35: ........................................................................................................................ 45 常见错误 36: ........................................................................................................................ 45 常见错误 37: ........................................................................................................................ 46 常见错误 38: ........................................................................................................................ 46 常见错误 39: ........................................................................................................................ 46 常见错误 40: ........................................................................................................................ 46 常见错误 41: ........................................................................................................................ 46 常见错误 42: ........................................................................................................................ 46 常见错误 43: ........................................................................................................................ 46 常见错误 44: ........................................................................................................................ 46 常见错误 45: ........................................................................................................................ 46 常见错误 46: ........................................................................................................................ 47 第五章 初始化问题 ..................................................................................................................... 47 常见错误 47: ........................................................................................................................ 47 常见错误 48: ........................................................................................................................ 47 常见错误 49: ........................................................................................................................ 47 常见错误 50: ........................................................................................................................ 47 常见错误 51: ........................................................................................................................ 47 常见错误 52: ........................................................................................................................ 47 常见错误 53: ........................................................................................................................ 47 常见错误 54: ........................................................................................................................ 48 常见错误 55: ........................................................................................................................ 48 常见错误 56: ........................................................................................................................ 48 常见错误 57: ........................................................................................................................ 48 常见错误 58: ........................................................................................................................ 48 常见错误 59: ........................................................................................................................ 48 第六章 内存和资源管理问题 ..................................................................................................... 48 常见错误 60: ........................................................................................................................ 48 常见错误 61: ........................................................................................................................ 48 常见错误 62: ........................................................................................................................ 49 常见错误 63: ........................................................................................................................ 49 常见错误 64: ........................................................................................................................ 49 目录 常见错误 1:过分积极的注释 第 4 页 常见错误 65: ........................................................................................................................ 49 常见错误 66: ........................................................................................................................ 49 常见错误 67: ........................................................................................................................ 49 常见错误 68: ........................................................................................................................ 49 第七章 多态问题 ......................................................................................................................... 49 常见错误 69: ........................................................................................................................ 49 常见错误 70: ........................................................................................................................ 50 常见错误 71: ........................................................................................................................ 50 常见错误 72: ........................................................................................................................ 50 常见错误 73: ........................................................................................................................ 50 常见错误 74: ........................................................................................................................ 50 常见错误 75: ........................................................................................................................ 50 常见错误 76: ........................................................................................................................ 50 常见错误 77: ........................................................................................................................ 50 常见错误 78: ........................................................................................................................ 50 常见错误 79: ........................................................................................................................ 51 第八章 类设计问题 ..................................................................................................................... 51 常见错误 80: ........................................................................................................................ 51 常见错误 81: ........................................................................................................................ 51 常见错误 82: ........................................................................................................................ 51 常见错误 83: ........................................................................................................................ 51 常见错误 84: ........................................................................................................................ 51 常见错误 85: ........................................................................................................................ 51 常见错误 86: ........................................................................................................................ 51 常见错误 87: ........................................................................................................................ 52 常见错误 88: ........................................................................................................................ 52 第九章 继承相关的设计问题 ..................................................................................................... 52 常见错误 89: ........................................................................................................................ 52 常见错误 90: ........................................................................................................................ 52 常见错误 91: ........................................................................................................................ 52 常见错误 92: ........................................................................................................................ 52 常见错误 93: ........................................................................................................................ 52 常见错误 94: ........................................................................................................................ 52 常见错误 95: ........................................................................................................................ 53 常见错误 96: ........................................................................................................................ 53 常见错误 97: ........................................................................................................................ 53 常见错误 98: ........................................................................................................................ 53 常见错误 99: ........................................................................................................................ 53 译者前言 常见错误 1:过分积极的注释 第 5 页 译者前言 前言 常见错误 1:过分积极的注释 第 6 页 前言 本书之渊薮乃是近二十年的小小挫折、大错特错、不眠之夜和在键盘的敲击中不觉而过的无 数周末。里面收集了普遍的、严重的或有意思的 C++常见错误,共计九十有九。其中的大多 数,(实在惭愧地说)都是我个人曾经犯过的。 术语“Gotcha”(译注:本书通译为“常见错误”,固然较之原文失之神韵,倒也算得通俗易 懂)有其云谲波诡的形成历史和汗牛充栋的不同定义。但在本书中,我们定义它在 C++范畴 里的含义为既普遍存在、又能加以防范的 C++编码和设计问题。这些常见错误涵盖了从无关 大局的语法困扰,到基础层面上的设计瑕疵,到源自内心的离经叛道等诸方面。 大约十年前,我开始在我教授的 C++课程的相关材料中添加个别常见错误的心得笔记。我的 感觉是,指出这些普遍存在的误解和误用,配合以正确的用法指导就像给学生打了预防针, 让他们自觉地与这些错误作斗争,更可以帮助新入门的 C++软件工程师避免重蹈他们前辈的 覆辙。大体而言,这种方法行之有效。我也深受鼓舞,于是又收集了一些互相关联的常见错 误的集子,在会议上作演讲用。未想这些演讲大受欢迎(或是同病相怜之故也未可知?), 于是就有人鼓励我写一本“常见错误之书”。 任何有关规避或修复 C++常见错误的讨论都涉及了其它的议题,最多见的是设计模式、习惯 用法,以及 C++语言特征的技术细节。 这并非一本讲设计模式的书,但我们经常在规避或修复 C++常见错误时发现设计模式是如此 管用的方法。习惯上,设计模式的名字我们把每个单词的首字母大写,比如模板方法设计模 式(Template Method)或桥接设计模式(Bridge)。 当我们提及一种设计模式的时候,若它 不是很复杂,则简介其工作机制,而详细的讨论则放在它们和实际代码相结合的时候才进行。 除非特别说明,本书不提供设计模式的完全描述或极为详尽的讨论,这些材料可以参考 Erich Gamma et al.,Design Patterns。无环访问者(Acyclic Visitor)、单态(Monostate)和 空件(Null Object)等设计模式的描述请参见 Robert Martin,Agile Software Development。 从常见错误的视角来看,设计模式有两个可贵的特质。首先,它们描述了已经被验证成功的 设计技术,这些技术在特定的软件环境中可以采用自定义的手法搞出很多新的设计花样。其 次,或许更重要的是,提及设计模式的应用,对于文档的贡献不仅在于使运用的技术一目了 然,同时也使应用设计模式的原因和效果一清二楚了。 举例来说,当我们看到在一个设计里应用了桥接设计模式时,我们就知道在一个机制层里, 一个抽象数据型别的实现并分解成了一个接口类和一个实现类。犹有进者,我们知道这样做 是为了强有力地把接口部分同底层实现剥离,是故底层实现的改变将不会影响到接口的用户。 我们还知道这种剥离会带来运行时的开销、还知道此抽象数据型别的源代码应该怎么安排, 还知道很多其它细节。 一个设计模式的名字是关于某种技术极为丰富的信息和经验之高效、无疑义的代号。在设计 和撰写文档时仔细而精确地运用设计模式及其术语会使代码洗练,也会阻止常见错误的发生。 前言 常见错误 1:过分积极的注释 第 7 页 C++是一门复杂的软件开发语言,而一种语言愈是复杂,习惯用法在软件开发中之运用就愈 是重要。对一种软件开发语言来说,习惯用法就是常用的、由低阶语言特征构成的高阶语言 结构的特定用法组合。总的来说,这和设计模式与高阶设计的关系差不多。是故,在 C++ 语言里我们可以直接讨论复制操作、函数对象、智能指针以及抛出异常等概念而不需要一一 指出他们在语言层面上的最低阶实现细节。 有一点要特别强调一下,那就是习惯用法并不仅仅是一堆语言特征的常见组合,它更是一组 对此种特征组合之行为的期望。复制操作是什么意思呢?当异常被抛出的时候,我们能指望 发生什么呢?大多数本书中的建议都是在提请注意以及建议应用 C++编码和设计中的习惯 用法。很多这里列举的常见错误常常可以直接视作对某种 C++习惯用法的背离,而这些常见 错误对应的解决方案则常常可以直接视作对某种 C++习惯用法的皈依(参见常见错误 10)。 本书在 C++语言的犄角旮旯里普遍被误解的部分着了重墨,因为这些语言材料也是常见错误 的始作俑者。这些材料中的某些部分可能让人有武林秘笈的感觉,但如果不熟悉它们,就是 自找麻烦,在通往 C++语言专家的阳关大道上也会平添障碍。这些语言死角本身研究起来就 是其乐无穷,而且产出颇丰。它们被引入 C++语言总有其来头,专业的 C++软件工程师经 常有机会在进行高阶的软件开发和设计时用到它们。 另一个把常见错误和设计模式联系起来的东西是,描述相对平凡的实例对于两者来说是差不 多同等重要的。平凡的设计模式是重要的。在某些方面,它们也许比在技术方面更艰深的设 计模式更为重要,因为平凡的设计模式更有可能被普遍应用。所以从对平凡设计模式的描述 中获得的收益就会以杠杆方式造福更大范围的代码和设计。 差不多以完全相同的方式,本书中描述的常见错误涵盖了很宽范围内的技术困难,从 如何成 为一个负责的专业软件工程师的循循善诱(常见错误 12)到避免误解虚拟继承下的支配原 则的苦口良言(常见错误 79)。 不过,就与设计模式类似的情况看,表现得负责专业当然比 懂得什么支配原则要对日复一日的软件开发工作来得受用。 本书有两个指导思想。第一个是有关习惯用法的极端重要性。这对于像 C++这样的复杂语言 来说尤为重要。对业已形成的习惯用法的严格遵守使我们能够既高效又准确地和同行交流。 第二个是对“其他人迟早会来维护我们写的代码”这件事保持清醒头脑。这种维护可能是直 截了当的,所以这就要求我们把代码写得很洗练,以使那些称职的维护工程师一望即知;这 种维护也可能是拐了好几道弯的,在那种情况下我们就得保证即使远在天边的某个变化影响 了代码的行为,它仍然能够给出正确的结果。 本书中的常见错误以一组小的论说文章的形式呈现,其中每一组都讨论了一个常见错误或一 些相互关联的常见错误,以及有关如何规避或纠正它们的建议。由于常见错误这个主题内廪 的无政府倾向,我不敢说哪本书可以特别集中有序地讨论它。然而,在本书中,所有的常见 错误都按照其错误本质或应用(误用)所涉的领域归类到相应的章节。 还有,对一个常见错误的讨论无可避免地会牵涉到其它的常见错误。当这种关联有它的意义 时——通常确实是有的——我就显式地作出链接标记。其实,这种每个常见错误的为了增强 关联性的描述本身也是有其讨厌之处的。比方说经常遇到一种情况就是还没来得及描述一个 前言 常见错误 1:过分积极的注释 第 8 页 常见错误自己,倒先把为什么会犯这个错误的前因后果交代了一大篇。要说清这些个前因后 果呢,好家伙,又非得扯上某种技术啦、习惯用法啦、设计模式啦或是语言细节什么的,结 果在言归正传之前要兜更大的圈子。我已经尽力把这种发散式的跑题减到最少了,但要是说 完全消除了这种现象,那我就没说实话。要把 C++程序设计做到很高效的境界,那就得在非 常多水火不容的方面作出如履薄冰的协调,想在研究大量相似的主题前就对语言作出像样的 病理学分析,那只能说是不现实的。 把这本书从第 1 个常见错误到第 99 个常见错误这么挨个地读下去,不仅是毫无必要的,而 且也谈不上明智。一气儿服下这么一剂虎狼之药恐怕会让你一辈子再也学不成 C++了。比较 好的阅读方法应该是拣一条你不巧犯过的,或是你看上去有点儿意思的常见错误开始看,再 沿着里面的链接看一些相关的。另一种办法就是你干脆由着性子,想看哪儿看哪儿,也行。 本书的文本里也使用了一些固定格式来阐明内容。首先,错误的和不提倡的代码以灰色背景 来提示,而正确和适当的代码却没有任何背景。其次,这里作示意用的代码为了简洁和突出 重点,都经过了编辑。这么做的一个结果是,这里示例用的代码若是没有额外的支撑代码往 往不能单独通过编译。那些并非平凡无用的示例源代码则可以在作者的网站里找到: www.semantics.org。所有这样的代码都由一个相对路径引出,像这样: gotcha00/somecode.cpp 最后,提个忠告:你不要把常见错误的重要性提升到和习惯用法、设计模式一样。(译注: 作者用心良苦,怕读者“近墨者黑”,好的没记住反而坏的学会了。所以特意提醒所有读者, 常见错误有些奇技淫巧,但毕竟不登大雅之堂。)一个你已经学会正确地使用习惯用法和设 计模式的标志是,当某个习惯用法或是设计模式正好是你手头的设计或编码对症时,它就“神 不知鬼不觉地”在你最需要时从你的潜意识里冒出来了。 对常见错误的清醒意识就好比是对危险的条件反射:一回错,二回过。就像对待火柴和荷枪 实弹一样,你不必非得烧伤或是走火打中了脑袋才学乖。总之,只要加强戒备就行了。把我 这本小书当作是你面对 C++常见错误时自我保护的武器吧。 Stephen C. Dewhurst Carver, Massachusetts 2002 年 7 月 致谢 常见错误 1:过分积极的注释 第 9 页 致谢 编辑们经常在书的“致谢”一章里落得个 get short shrift 的下场,有时用一句“„„其实我 也挺感谢我那编辑的,我琢磨着我在拼了命爬格子的时候此人大概肯定也是出过一点什么力 的吧”就打发了。Debbie Lafferty,也就是本人的编辑,负责本书的问世。有一次,我拿着 一本不足为道的介绍性的程序设计教材去找她搞个不足为道的合作提案,结果她反而建议我 把其中一个有关常见错误的章节扩展成一本书。我不肯。她坚持。她赢了。值得庆幸的是, Debbie 在胜利面前表现得特别有风度,只是淡淡了说了一句站在编辑立场上的“你看看, 我叫你写的吧。”当然不止于此,在我拼了命爬格子的时候,她是颇出了一些力的。 我也感谢那些无私奉献了他们的时间和专业技能来使本书变得更好的审阅者们。审阅一本未 精化的稿本是相当费时的,常常也是枯燥乏味的,有时甚至会气不打一处来,而且几乎肯定 是讨不着什么好的(常见错误 12),这里要特别赞美一下我的审阅者们入木三分而又深中肯 綮的修改意见。Steve Clamage、Thomas Gschwind、Brian Kernighan、Patrick McKillen、Jeffrey Oldham、Dan Saks、Matthew Wilson 和 Leor Zolman 对书中的技术问题、行业规矩、清出校 样、代码片断和一些偶然出现的冷嘲热讽都提出了自己的宝贵意见。 Leor 在稿本出来之前很久就开始了对本书的“审阅”,书中的一些常见错误的原始版本只是 我在互联网论坛里发的一些帖子,他针对这些帖子回复了不少逆耳忠言。Sarah Hewins,是 我最好的朋友同时也是最不留情的批评家,不过这俩头衔都是在审阅我一改再改的稿本时获 称的。David R. Dewhurst 经常为整个写作项目提供难得的远见。Greg Comeau 慷慨地让我有 幸使用他堪称最牛的标准 C++编译器来校验书里的代码(译注:这应该就是著名的 Comeau C/C++ Front/End 编译器)。 就像所有关于 C++的并非平淡无奇的工作那样,本书也是集体智慧的结晶。这些年来,很多 我的学生、客户和同事们为我在 C++常见错误面前表现的呆头呆脑和失足跌跤可没少数落过 我,并且他们中的好多人都帮我找到了问题的解决之道。当然,这些特别可贵的贡献者中的 大部分都没法在这里一一谢过,不过有些提供了直接贡献的人还是可以列举如下的: 常见错误 11 中的 Select 模板,和常见错误 70 中的 OpNewCreator 策略都取自 Andrei Alexandrescu,Modern C++ Design。 我在常见错误 44 中描述了有关返回一个常量形参的引用带来的问题(译注:是个有关临时 对象生命期的问题),此问题我初见于 Cline et al.,C++ FAQs(我客户的代码中在此之后马 上就用上了这个解决方案)。此书还描述了我在常见错误 73 中提到的用于规避重载虚函数的 技术。 常见错误 83 中的那个 Cptr 模板,其实是 Nicolai Josuttis,The C++ Standard Library 中 CountedPtr 模板的一个变形。 Scott Meyers 在他的 More Effective C++中,对运算符&&、||、和,的重载之不恰当性提出了 比我在常见错误 14 的描述更深入的见解。他也在他的 Effective C++中,对我在常见错误 58 致谢 常见错误 1:过分积极的注释 第 10 页 中讨论的二元运算符以值形式返回的必要性作了更细节的描述,还在 Effective STL 中描述了 我在常见错误 68 里说的对 auto_ptr 的误用。在后置自增、自减运算符中返回常量值的技 术,也在他的 More Effective C++中提到了。 Dan Saks 对我在常见错误 8 中描述的前置声明文件技术提出了最有说服力的论据,他也是区 别出常见错误 17 中提及的“Sergeant operator”的第一人, 他也说服了我在 enum 型别的自 增和自减中不去做区间校验,这一点被我写在了常见错误 87 中。 Herb Sutter 的 More Exceptional C++中条款 36 促使我去重读了 C++标准的 8.5 节,然后修正 了我对形参初始化的理解(见常见错误 57)。 常见错误 10、27、32、33、38~41、70、72~74、89、90、98 和 99 中的一些材料出自我先 是在 C++ Report、后来在 The C/C++ Users Journal 撰写的 Common Knowledge 专栏。 第一章 基础问题 常见错误 1:过分积极的注释 第 11 页 第一章 基础问题 一个问题说它是基础的,并不就是说它不是严重的或不是普遍存在的。事实上,本章所讨论 的基础问题的共同特点比起在以后章节讨论的技术复杂度而言,可能更侧重于使人警觉。这 里讨论的问题由于它们的基础性,在某种程度上可以说它们存在于几乎所有的 C++代码中。 常见错误 1:过分积极的注释 很多注释都是画蛇添足。它们只是让源代码更难读、更难维护,并经常把维护代码的工程师 引入歧途。考虑下面的简单语句: a = b; // 将 b 赋值给 a 这个注释难道比代码本身更能说明这个语句的意义吗?因而它是完全无用的。事实上,它比 完全无用还要坏。它是害人精。首先,这条注释转移了阅读代码的人注意力,增加了阅读量 因而使代码更费解。其次,要维护的东西更多了,因为注释也是要随着它描述的代码的更改 而更改的。第三,这个对注释的更改常常就没有做。(译注:因为注释没有随着代码更改, 这个语句看起来就成了下面这个样子。) c = b; // 将 b 赋值给 a 一个仔细的维护工程师不会简单地断言注释是错的(译注:说不定是代码错了呢),所以他 就被迫要去检视整个程序以确定注释到底是错了的呢,还是好意的呢(c 可能是 a 的一个引 用),还是微妙的呢(赋值给 c 可能引发一些传播效应以使 a 的值也发生相应变化)等等, 总之这一行就应该根本不带注释。 a = b; 还是这代码本来的样子最清楚地表明了其意义,也没有额外的注释需要维护。这在精神上也 符合老生常谈,亦即“最有效率的代码就是根本不存在的代码”。这条经验对于注释也适用: 最好的注释就是根本用不着写的注释,因为要注释的代码已经“自注释”了。 另一些常见的非必要的注释的例子经常可以在类的定义里见到,它们要么是病态的编码标准 的怪胎,要么就是出自 C++嫩手: class C { 第一章 基础问题 常见错误 1:过分积极的注释 第 12 页 // 公开接口 public: C(); // 默认构造函数 ~C(); // 析构函数 // ... }; 你会觉得别人拿你当二傻子。要是一个维护工程师连“public:”是什么意思都需要教, 你还会让他动你的代码吗?对于任何有经验的 C++软件工程师而言,这些注释除了把代码搞 乱、增加需要维护的文本数量以外没有任何用处。 class C { // 公开接口 protected: C( int ); // 默认构造函数 public: virtual ~C(); // 析构函数 // ... }; 软件工程师还有一种强烈的心理趋势就是尽量不要“平白无故”地在源文件文本中多写哪怕 一行。这里公布一个有趣的本行业秘密:如果某种结构(函数啦、类的公开接口啦什么的) 能被塞在一“页”里,也就在三四十行左右(译注:也就是在普通的屏幕上能一页显示得下) 的话,它就很容易理解。假如有些内容跑到第二页去了,它理解起来就难了一倍。如果三页 才塞得下,据估计理解难度就成原来的四倍了。(译注:源文件文本长度与理解难度成指数 关系,所以能少写一行非必要的注释就少写一行。) 一种特别声名狼藉的编码实践就是把更改日志作为注释插入到源文件的头部或尾部: /* 6/17/02 SCD 把一个该死的 bug 干掉了 */ 这到底是有用的信息,抑或是仅仅是维护工程师的自吹自擂?在这行注释被写下以后的一两 个星期,它怎么看也不再像是有用的了,但它却也许要在代码里粘上很多年,不懈地戏弄着 一代又一代的维护工程师。顶好是用你的版本控制软件来做这种无用注释真正想做的事, C++的源代码文件里可没有闲地方来放这些劳什子。 想不用注释却又要使代码意义明确、容易维护的最好办法就是遵循一个简单易行的、定义良 好的命名习惯来为你使用的实体(函数啦、类啦、变量啦等等)取一个清晰的、反映其抽象 含义的名字。(函数)声明中形式参数的名字尤其重要。考虑一个带有三个同一型别参数的 函数: /* 从源到目的执行一个动作 第一个参数是动作编码(action code),第二个参数是源(source),第三个参数是目 第一章 基础问题 常见错误 2:幻数 第 13 页 的(destination) */ void perform( int, int, int ); 这也不算太坏吧,不过如果参数是七八个而不是三个你又该写多少东西呢?我们明明可以做 得更好: void perform( int actionCode, int source, int destination); // 译注:这很明显是 Herb Sutter 倡导的命名规则(原谅我又多写了一行注释,) 这就好多了。按理,我们还需要写一行注释来说明这个函数是干什么的(而不是如何实现的)。 形式参数的一个最引人入胜之处就是,不像注释,它们是随着余下的代码一起更改的,即使 改了也不影响代码的意义。话虽然这么说,但我不能想像任何一个软件工程师在参数意义改 变了的时候,会不给它取一个新名字。(译注:请参考 Brian W. Kernighan、Rob Pike,The Practice of Programming,§8.7),但我能举出一串软件工程师来,他们改了代码但老是忘记 维护注释。 Kathy Stark 在 Programming in C++中说得好:“如果在程序里用意义明确、脱口而出的名字, 那么注释只是偶尔才需要。如果不用意义明确的名字,即使加上了注释也不能让代码更好懂 一些。” 另一种最大程度地减少注释书写的办法是采用标准的、或公认的组件: printf( "Hello, World!" ); // 在屏幕上打印“Hello, World” 上面这个注释不但是无用的,而且只在某些情况下正确。标准组件不仅是“自注释”的,而 且有关它们的文献汗牛充栋,并广为人知。 swap( a, a+1 ); sort( a, a+max ); copy( a, a+max, ostream_iterator(cout,"\n") ); 因为 swap、sort 和 copy 都是标准组件,对它们加上任何注释都是一种捣乱,而且给定 义得好好的标准操作规格描述带来了(非必要的)不确定性。 注释之害并非与生俱来,并且注释常常是必不可少的。但它们必须(和代码一起)被维护。 维护注释常常比维护它们注解的代码要难。注释不应该描述显而易见的事,或是把在别的地 方已经说清楚的东西再聒噪一遍。我们的目标不是要消灭注释,而是在代码容易理解和维护 的前提下,尽可能少写注释。 常见错误 2:幻数 第一章 基础问题 常见错误 2:幻数 第 14 页 幻数,用在这里时其含义是上下文里出现的字面常量(raw numeric literal),本来它们应该 是具名常量(named constant)才对。(译注:比如下面这个类的声明里出现的“10”。) class Portfolio { // ... Contract *contracts_[10]; char id_[10]; }; 幻数带来的主要问题是它们没有语义,它们只是个量罢了。一个“10”就是一个“10”,你 看不出它的意思是“合同的数量”或是“标识符的长度”。这就是为什么当我们阅读和维护 带有幻数的代码时,不得不一个个地去搞清楚每个光秃秃的量到底代表的是什么意思。没错, 这样也能活下去,但带来的是不必要的精力浪费以及准确性的牺牲。 就拿上面这个设计得很差的公文包(portfolio)类来说,它能够管理最多 10 个合同。当合同 数越来越多的时候(10 个不够用了),我们决定把合同数增加至 32 个。(如果你对安全性和 正确性很挑剔,你顶好是用标准模板库提供的 vector。)我们立刻陷入了困境,因为我们 必须一个个去检查那些用了 Portfolio 类的源文件里出现的每一个字面常量“10”,并逐 个甄别每个“10”是不是代表“最多合同数”这个意思。 实际情况可能会更糟。在一些很大的、长期的项目里,有时“最多合同数是 10”这件事成 了一个临时的军规,这个(远非合理的)知识被硬编码在某些根本没有包含 Portfolio 类 头文件的代码中: for( int i = 0; i < 10; ++i ) // ... 上面这个“10”是不是代表“最大合同数”的意思呢?还是“标识符的最大长度”?抑或是 毫不相干的其它意思? 一堆臭味相投的字面常量要是不巧凑在了一块儿,软件工程师能写出的史上最有碍观瞻的代 码就这么诞生了: if( Portfolio *p = getPortfolio() ) for( int i = 0; i < 10; ++i ) p->contracts_[i] = 0, p->id_[i] = '\0'; 现在维护工程师可有事做了。他们不得不在 Portfolio 类中出现的毫不相关的、但正好值 相同的两个“10”之间费劲地识别出它们各自的意思并分别处理。(译注:如果“最大合同 数”和“标识符的最大长度”变成了不同的数,上述的初始化循环就要从一个变成两个了。 而事实上就应该是两个毫不相关的初始化循环,由于值的巧合而粘连了它们,这也是幻数背 后的临时观念导致的不良编码实践习惯。)当这一切头疼的事有一个如此简单的解决方案时, 我们真的没有理由不去做: 第一章 基础问题 常见错误 3:全局变量 第 15 页 class Portfolio { // ... enum { maxContracts = 10, idlen = 10 }; Contract *contracts_[maxContracts]; char id_[idlen]; }; 一个在它所在范围有着清楚含义的枚举常量同时还有着不占空间,也没有任何执行期成本的 巨大优点。 幻数的一个不那么显而易见的坏处是它会以意想不到的方式降低它所代表的型别的精度,它 们也不占有相应的存储空间(译注:具名常量则理论上占有存储空间,尽管有常量折叠的可 能)。拿字面常量 40000 来说,它的实际型别是平台相关的。如果一个 int 型别能把它塞下, 它就是 int 型别的。要是塞不下呢,它就成了 long 型别的。要是我们不想在平台移植的 当口引狼入室(想想根据型别进行的函数重载解析规则在这里能把我们逼疯的情形!),我们 还是老老实实地自己指定一个型别吧,这比让编译器或平台替我们做这件事要好得远: const long patienceLimit = 40000; 另一个字面变量带来的潜在威胁来源于它们没有地址这件事。好吧,就算这不是一个天天发 生的问题,但是有的时候将一个引用绑定到一个常量是有其作用的。 const long *p1 = &40000; // 错误!(译注:字面常量无法取址,它们没有地址。) const long *p2 = &patienceLimit; // 没问题。 const long &r1 = 40000; // 合法,不过常见错误 44 会告诉你另一些精彩故事。 const long &r2 = patienceLimit; // 没问题。 幻数有百害而无一利。请使用枚举常量或初始化了的具名常量。 常见错误 3:全局变量 很难找到一个理由去硬生生地声明一个突兀的全局变量。全局变量阻碍了代码重用,而且使 代码变得更难维护。它们阻碍重用是因为任何使用了全局变量的代码就立刻与之耦合,这使 得全局变量一改它们也非得跟着改,从而使任何重用都不可能了。它们使代码变得更难维护 的原因是很难甄别出哪些代码用了某个特定的全局变量,因为任何代码都有访问它们的权限。 全局变量增加了模块间的耦合,因为它们往往是作为一种幼稚的模块间消息传递机制的设施 而存在的。就算它们能担此重任,从实践角度来说,(译注:如果要改变机制,不再使用某 些全局变量的话)要从一个大软件上去掉任何一个全局变量都几乎不可能。(译注:此类全 局变量会分散在各个源文件里并在各个地方使用,块间耦合度因而就变得很高了。)这还是 说他们能正常工作的情况。不过可不要忘了,全局变量是不设防的。随便哪个维护你代码的 C++嫩手都能让你对全局变量有强烈依赖的软件所玩的把戏随时坍台。 第一章 基础问题 常见错误 3:全局变量 第 16 页 全局变量的辩护者们经常拿它的“方便”来说事。这真是自私自利之徒的无耻之争。要知道, 软件的维护常常比它的初次开发要花费更多时间,而使用全局变量就意味着把烂摊子扔给了 维护工程师。假设我们有一个系统,它有一个全局可访问的“环境”,并且(我们按需求保 证)确实只有一个。不幸的是,我们选择了使用一个全局变量: extern Environment * const theEnv; 我们的需求一时如此,但马上就行不通了。在软件就要分发之前,我们会发现,可能同时存 在的环境要增加到两个、或三个、或是在系统启动时指定的某个数、或根本就是完全动态的。 这种在软件发布的最后时刻发生的变更实属家常便饭。在备有无微不至的源代码控制过程的 大项目里,这个变更会引发极费时间、涉及所有源文件的更改,即使在最细小的和最直截了 当的那些地方也不例外。整个过程预计要几天到几星期不等。假如我们不用全局变量这个灾 星,只要五分钟我们就能搞定这一切: Environment *theEnv(); 仅仅是把对于一个值的访问加了一个函数的包装,我们就获得了可贵的可扩充性。要是再加 上函数重载,或是给予函数形式参数一个默认值,我们就根本不要怎么改源代码了。 Environment *theEnv( EnvCode whichEnv = OFFICIAL ); 另一个全局变量引起的问题不是一眼能看出来的。此问题的来源是全局变量经常要求(延迟 到)执行期(才进行的)静态初始化。C++语言里如果一个静态变量用来初始化的值不能在 编译期就计算妥当,那么这个初始化的动作就会被拖到执行期。这是许多致命后果的始作俑 者(此问题非常重要,常见错误 55 专门来讨论此问题): extern Environment * const theEnv = new OfficialEnv; 如果改用一个函数或类来充当访问全局信息的掮客,初始化动作就会被延后,从而也就变得 安全无虞了: gotcha03/environment.h class Environment { public: static Environment &instance(); virtual void op1() = 0; // ... protected: Environment(); virtual ~Environment(); 第一章 基础问题 常见错误 4:未能区分函数重载和形式参数默认值 第 17 页 private: static Environment *instance_; // ... }; gotcha03/environment.cpp // ... Environment *Environment::instance_ = 0; Environment &Environment::instance() { if( !instance_ ) instance_ = new OfficialEnv; return *instance_; } 在上述例子中,我们采用了称为单件设计模式(Singleton Pattern)的一个简单实现(译注: 请参考 Scott Meyers,Effective C++,3rd Edition,条款 4,那里有一个更漂亮的 Singleton Pattern 的实现。),以所谓“缓式求值”形式完成静态指针的初始化动作(如果一定要在技术上钻钻 牛角尖的话,好吧,这是赋值,不是初始化)。是故,我们能够保证 Environment 对象的 数量不会超过一个。请注意,Environment 类没有给予其构造函数 public 访问层级,所以 使用 Environment 类的工程师只能用它公开出来的 instance()成员函数来取得这个静 态指针。而且,我们在不必在第一次访问 Environment 对象之前就创建它(译注:这符 合 C++“不为用不到的东西付出成本”的哲学): Environment::instance().op1(); 更重要的是,这种受控的访问为使用了单件设计模式的类适应未来的变化带来了灵活性,并 且消除了对现有代码的影响。以后当我们要切换到一个多线程的环境,或是要改成允许一种 以上的环境并存的设计,或是随便要求怎么变时,我们都可以通过更改使用了单件设计模式 的类的实现来搞定这一切,而这就像我们先前更改包装全局变量的那个函数一样随心所欲。 常见错误 4:未能区分函数重载和形式参数默认值 函数重载其实和形式参数默认值之间并无干系。不过,这两个独立的语言特征有时会被搞混。 因为它们会模塑出语法上非常相像的函数用法接口。当然啦,看似一样的接口其背后的意义 大相径庭: 第一章 基础问题 常见错误 4:未能区分函数重载和形式参数默认值 第 18 页 gotcha04/c12.h class C1 { public: void f1( int arg = 0 ); // ... }; gotcha04/c12.cpp // ... C1 a; a.f1(0); a.f1(); 类 C1 的设计者决定给予函数 f1()一个形式参数的默认值。这样一来,C1 的使用者就有了 两个选择:要么显式地给函数 f1()一个实际参数,要么(通过不指定任何参数的方式)隐 式地给函数 f1()一个实际参数“0”。所以,上述两个函数调用产生的动作序列(译注:即 机器代码)是完全相同的。 gotcha04/c12.h class C2 { public: void f2(); void f2( int ); // ... }; gotcha04/c12.cpp // ... C2 a; 第一章 基础问题 常见错误 5:对引用的认识误区 第 19 页 a.f2(0); a.f2(); C2 这个类的实现则有很大不同。其使用者的选择是根据给予的参数数目调用两个虽然名字 都叫 f2(),却是完全不同的函数中的某一个。在我们早先那个 C1 类的例子里,两个函数 调用产生的动作序列是完全相同的,但在这个例子里它们产生的却是完全不同的动作序列了。 这是因为两个函数调用的结果是调用了不同的函数。 通过对成员函数 C1::f1()和 C2:f2()取址,我们就拿到了这两种接口最大不同点的证据: gotcha04/c12.cpp void (C1::*pmf)() = &C1::f1; // 错误! void (C2::*pmf)() = &C2::f2; 我们实现 C2 类的方法决定了指向成员函数的指针 pmf 指向了没有带任何形式参数的那个 f2()函数。因为 pmf 是一个指向没有带任何形式参数的成员函数的指针,编译器能够正确 地选择第一个 f2()作为它应该指向的函数。而对于 C1 类来说,我们将收到一个编译期错 误,因为唯一的一个名叫 f1()的成员函数带有一个 int 型别的形式参数。(译注:因而编 译器找不到不带形式参数的 f1()。) 函数重载主要用于一组抽象意义相同,但实现不同的函数。而形式参数默认值主要用于方便, 为函数提供一个更简洁的接口。(译注:也就是能让函数在被调用时少指定几个参数,不用 老是反复地指定几个相同值的参数。)函数重载和形式参数默认值是两个毫不相干的语言特 征,它们出于不同的目的而设计,行为也完全不同。请仔细地区分它们。(更详细的信息请 参见常见错误 73 和 74)(译注:请参考 Scott Meyers,Effective C++,2nd Edition,条款 24, 它用更具体的例子说明了本章讨论的问题。) 常见错误 5:对引用的认识误区 对于引用的使用,主要存在两个常见的问题。首先,它们经常和指针搞混。其次,它们被穿 了小鞋。好多在 C++工程里使用的指针实际上只是 C 阵营那些老顽固的杰作,该是引用翻 身的时候了。 一个引用不是一个指针。一个引用只是它的初始化物的一个别名。记好了,你能对引用做的 唯一的事情就是初始化它。初始化完了以后,你使用一个引用就是使用其初始化物的另一种 写法罢了。(凡事皆有例外,请看常见错误 44)一个引用是没有地址的,甚至有可能它们不 占任何存储。 第一章 基础问题 常见错误 5:对引用的认识误区 第 20 页 int a = 12; int &ra = a; int *ip = &ra; // ip 指向 a 的地址。 a = 42; // ra 的值现在也成 42 了。 由于这个原因(引用没有地址),声明引用的引用、指向引用的指针或引用的数组都是不合 法的(尽管 C++标准委员会已经在讨论至少在某些上下文环境里允许引用的引用)。 int &&rri = ra; // 错误! int &*pri; // 错误! int &ar[3]; // 错误! 引用不能是常量性的或挥发性的,因为别名不能是常量性的或挥发性的。尽管引用可以是一 个常量性的或挥发性的实体的引用。如果用 const 或 volatile 来修饰引用,就会收到一 个错误: int &const cri = a; // 错误! const int &rci = a; // 没问题。 不过,比较诡异的是,如果把一个 const 或 volatile 修饰符加在一个引用型别上面并不 被 C++判定为非法。编译器不会为此报错,而是简单地忽略这些修饰符。 typedef int *PI; typedef int &RI; const PI p = 0; // p 是常量指针。 const RI r = a; // 没有常量引用,r 就是个平凡的引用 没有空引用,也没有型别为 void 的引用。 C *p = 0; // p 是空指针。 C &rC = *p; // 把引用绑定到空指针上,其结果未有定义。 extern void &rv; // 试图声明型别为 void 的引用会引起编译期错误。 一个引用就是一个(不可更改的、初始化物的)别名,既然是别名,总得是“某个东西”的 别名,这“某个东西”一定要实际存在才成。 不管怎样你要记住,我可没说引用只能是一个简单变量名的别名。其实,任何一个能作为左 值的(如果你不清楚什么是左值,请看常见错误 6)复杂表达式都能作为一个引用的初始化 物。 int &el = array[n-6][m-2]; el = el*n-3; string &name = p->info[n].name; if( name == "Joe" ) 第一章 基础问题 常见错误 5:对引用的认识误区 第 21 页 process( name ); 如果一个函数的返回值是一个引用,这就意味着可以把这个函数的返回值重新赋值。一个经 常被津津乐道的典型例子是一个数组抽象数据型别的索引函数(index function)(译注:即 非 const 的那个 operator []())。 gotcha05/array.h template class Array { public: T &operator [](int i){ return a_[i]; } const T &operator [](int i) const{ return a_[i]; } // ... private: T a_[n]; }; 那个引用返回值就使得对数组元素的赋值在语法上颇为自然了: Arrayia; ia[3] = ia[0]; 引用还有一个作用,就是可以让函数在其返回值之外多传递几个值: Name *lookup( const string &id, Failure &reason ); // ... string ident; // ... Failure reasonForFailure; if( Name *n = lookup( ident, reasonForFailure ) ) { // 查找成功则执行的例程 } else { // 如果查找失败,那么由 reasonForFailure 的值返回错误代号 } 对一个对象用引用型别进行强制转型操作的话,其效果与用非引用的相同型别进行的强制转 第一章 基础问题 常见错误 5:对引用的认识误区 第 22 页 型有殊为不同的效果: char *cp = reinterpret_cast(a); reinterpret_cast(a) = cp; 在上述代码的第一行里,我们把一个 int 型变量的值转换成了一个指针值。(我们在这里使 用了 reinterpret_cast 运算符,这好过使用形如“(char *) a”的旧式强制转换操 作。要想知道这是出于何种考量,请看常见错误 40。)这个操作的详细情况分解如下:一个 int 型变量的值被存储于一个拷贝中,并随即被当作一个指针来解释。(译注:作者想强调 的是,a 的值首先被拷贝到一个临时对象中,reinterpret_cast 的操作数并非 a 本身, 而是 a 的这个拷贝,也就这个临时对象。) 而第二个强制转型操作则是完全另外一个样子。转换成引用型别的强制转型操作的意义是把 int 型变量本身解释成一个指针,成为左值的是这个变量本身(译注:而不是什么拷贝而成 的临时对象了),我们继而可以对它赋值。也许这个操作会引发一个核心转储(dump core 俗称“吐核”,也就是操作系统级的崩溃),不过那不是我们现在谈论的主题,再说,使用 reinterpret_cast 本身也就暗示着该操作没把可移植性纳入考量。和上述形式差不多的、 没有转换成引用型别的强制转型操作的一次赋值尝试则会可耻地失败,因为这样一个强制转 型操作的结果是一个右值而不是一个左值。(译注:这一段非常重要,请仔细阅读以真正领 会它的意思。它主要说了这么一件事:转换成引用型别的强制转型操作的操作数是对象本身, 因而它是个左值。否则它的操作数就是一个临时对象,而对临时对象赋值是没有任何意义的, 只能引起概念混乱,所以 C++语言把这样的结果规定为右值,禁止进行赋值。这就像 int 型变量 a、b 相加的表达式 a+b 的结果也是一个临时对象,因而不能对它赋值是一个道理。) reinterpret_cast(a) = 0; // 错误! 对数组的引用保留了数组边界(译注:也就是能记住数组的大小),而指向数组的指针则不 保留。 int ary[12]; int *pary = ary; // pary 指向数组 ary 的第一个元素。 int (&rary)[12] = ary; // rary 是整个数组 ary 的引用。 int ary2[3][4]; int (*pary2)[4] = ary2; // pary2 指向数组 ary2 的第一个元素。 int (&rary2)[3][4] = ary2; // rary2 是整个数组 ary2 的引用。 引用的这个性质有时在数组作为参数被传递给函数时有用。(欲知详情,请看常见错误 34。) 同样可以声明一个函数的引用: int f( double ); int (* const pf)(double) = f; // pf 是指向函数 f()的常量指针 int (&rf)(double) = f; // rf 是函数 f()的引用 第一章 基础问题 常见错误 6:对常量(性)的认识误区 第 23 页 指向函数的常量指针和函数的引用从编码实践角度来看,并无很大不同。除了一点,那就是 指针可以显式地使用提领语法。对引用是不能使用显式的提领语法的,除非它被隐式转换成 指向函数的指针。(译注:以下这六行代码主要想说明,C++的函数调用语法很灵活,无论 是通过使用函数名本身、指向函数的指针还是函数的引用来调用一个函数,都既可以用名字 本身,也可以使用提领语法。尽管后两行在语法上其实是经过了一个隐式型别转换,因而会 带来效率上的损失。) a = pf( 12.3 ); // 直接用函数指针名调用函数。 a = (*pf)(12.3); // 使用提领语法也是可以的。 a = rf( 12.3 ); // 通过引用调用函数。 a = f( 12.3 ); // 直接调用函数本身。 a = (*rf)(12.3); // 把引用(隐式)转换成指向函数的指针,再使用提领语法。 a = (*f)(12.3); // 把函数本身(隐式)转换成指向函数的指针,再使用提领语法。 请注意区别引用和指针。 常见错误 6:对常量(性)的认识误区 在 C++中的常量性概念是平凡的,但是这和我们先入为主的对 const 的观念不太符合。 首先我们要特别注意一个以 const 饰词修饰的变量声明和一个字面常量的区别: int i = 12; const int ci = 12; 字面常量 12 不是 C++概念中的常量。它是一个字面常量。字面常量没有地址,永远不可能 改变其值。i 是一个对象,有自己的地址,其值可变。用 const 关键字修饰声明的 ci 也是 一个对象,有自己的地址,尽管(在本例中)其值不可变。 我们说 i 和 ci 可以作为左值使用,而字面常量 12 却只能作为右值。这两个术语来源于伪 表达式 L=R,说明只有左值能出现在赋值表达式左侧,右值则只能出现在赋值表达式右侧。 但这种定义对 C++和标准 C 来说并不成立,因为在本例中 ci 是一个左值,但不能被赋值, 因为它是一个不可修改的左值。 如果把左值想成一个能放置值的地方,那末右值就是没有与之相关的地址的值。 int *ip1 = &12; // 错误! 12 = 13; // 错误! const int *ip2 = &ci; // 没问题。 ci = 13; // 错误! 最好仔细考虑一下上面 ip2 的声明中出现的 const,这个饰词描述了对我们通过 ip2 对 第一章 基础问题 常见错误 7:无视基础语言的精妙之处 第 24 页 ci 的操作的一个约束,而不是对于 ci 的一般操作的一个约束。(译注:也就是说,变量本 身不具常量性,具有常量性的是通过指针提领的那个能够作为左值的表达式。虽然这两者从 观念上来看,是同一个对象。这就让我们理解,C++的常量性不是根据低级地址绑定的,而 是富有高级的对象观念的。)如果我们想声明指向常量的一个指针,我们应该这么办: const int *ip3 = &i; i = 10; // 没问题,约束的不是 i 的一般操作而是通过 ip3 对 i 的操作。 *ip3 = 10; // 错误! 这里我们就有了一个指向 const int 型别的指针,而这个 const int 型别对象又是一个 普通 int 型别对象的引用。这里的常量性仅仅限制了我们能通过 ip3 做什么事。这不表明 i 不会变,只是对它的修改不能通过 ip3 进行。如果我们再把问题说细一点,请看下面这 个把 const 和 volatile 结合使用的例子: extern const volatile time_t clock; 这个 const 饰词的存在表明我们是不允许修改变量 clock 的值的,但是 volatile 饰词 的存在说明 clock 的值肯定还是会通过其它途径变掉的。(译注:这个例子说明了 C++里的 常量性的观念只是限制了在代码中对 const 修饰的变量显式的直接修改,对于其它方式的 修改,并不是 C++里的常量性观念所要求的。总体来看,本文指出了常量性是高级的操作。) 常见错误 7:无视基础语言的精妙之处 大多数 C++软件工程师都自信满满地认为自己对所谓 C++的“基础语言”,也就是 C++继承 自 C 语言的那部分了如指掌。实际情况是,即使是 C++前辈有的时候也对最基础的 C/C++ 语句和运算符的某些曲奥妙用一无所知。 逻辑运算符不能说是难懂的吧,但刚入行的 C++工程师总是不能让它们物尽其用。你看到下 面的代码时是不是会气得脸发紫? bool r = false; if( a < b ) r = true; 正解如下: bool r = a12),A,B>::Result::value: cout << "group 2" << endl; goto theDefault; } 如果是有意去利用直下式计算的话——更多的人可能是由于疏忽才不小心让直下式计算引 起了错误的执行流——我们习惯上要在适当的地方加一个注释,提醒将来的维护工程师,我 们这里使用直下式计算是有意为之的。不然,维护工程师就像有职业病一样以为我们是漏掉 了 break 语句,并给我们添上,这样就错了。 记住,case 语句的标签必须是整型常量性的表达式。换句话说,编译器必须能在编译期就 算出这些表达式的值来。不过从上面这个例子你也应该能够看出,这些常量性的表达式能够 用多么丰富多彩的写法来书写。而 switch 表达式本身一定是整型,或者能转型到整型的 其它对象也可以。比如上面这个 e 就可以是一个带有型别转换运算符的、能够转型到整型 的类的对象。 同样要记住,switch 语句的平凡语法暗示着我们能够把语句块写成比上面的例子更非结构 化的形式。在 switch 语句里的任何地方都能用 case 标记,而且不一定要在同一个嵌套层 级里。 switch( expr ) default: if( cond1 ) { case 1: stmt1; case 2: stmt2; } else { if( cond2 ) case 3:stmt2; 第一章 基础问题 常见错误 7:无视基础语言的精妙之处 第 28 页 else case 0: ; } 这样的代码看起来有点傻(容我直言,确实很傻),但是这种对于基础语言曲奥部分的理解 有的时候相当有用。比如利用上述的 switch 语句的性质,就曾在 C++编译器中做出了一 个复杂数据结构内部迭代的有效实现: gotcha07/iter.cpp bool Postorder::next() { switch( pc ) case START: while( true ) if( !lchild() ) { pc = LEAF; return true; case LEAF: while( true ) if( sibling() ) break; else if( parent() ) { pc = INNER; return true; case INNER: ; } else { pc = DONE; case DONE: return false; } } } 在上面的代码里,我们使用了低级的 switch 语句最曲奥的语义来实现了树的遍历操作。 每当我使用上面这样的结构时,我总能收到强烈的、负面的甚至是骂骂咧咧的反应。我确实 同意这种代码可不适合给维护工程师中的嫩手来打理,但是这样的结构——尤其是已经封装 好的、带有文档的版本——确实在对要求甚高或非常特殊的编码中有自己的一席之地。一句 话,对基础语言难点的熟练掌握会是对你大有裨益的。 第一章 基础问题 常见错误 8:未能区分可访问性和可见性 第 29 页 常见错误 8:未能区分可访问性和可见性 C++语言压根儿没有实现什么数据隐藏,它实现了的是访问保护层级。一个类中保护访问的 成员和私有访问的成员不是不可见的,只是不能访问罢了。如同一切可见而不可及的事物一 样(经理的形象跃入脑海),他们总是惹出各种麻烦。 一个最显而易见的问题就是即使是一个类的实现仅仅更改了一些貌似不可见的部分,也会带 来必须重新编译代码的苦果。考虑一个简单的类,我们为其添加一个新的数据成员: class C { public: C( int val ) : a_( val ), b_( a_ ) // 新添加的代码 {} int get_a() const { return a_; } int get_b() const { return b_; } // 新添加的代码 private: int b_; // 新添加的代码 int a_; }; 上例中,修改造成了类的若干种变化。有些变化是可见的,有些则不然。 由于添加了新的数据成员,类的尺寸发生了变化,这一点是可见的。这个变化对给所有使用 了该类的对象的、提领成该类的对象的或是对该类的指针作了指针算术运算的代码,或是以 其它的什么方式用到了这个类的尺寸数据或是引用了成员的名字的代码,都带来了深刻的影 响。这里要特别注意的是,新的数据成员的引入所占的位置,同样也会影响旧的成员 a_在 类内的偏移量。一旦 a_在类内的偏移量真的变了,那所有 a_作为数据成员的引用,或是指 向 a_的指向数据成员的指针(译注:若不重新编译的话)将统统失效。顺便说一句,这个 类的成员初始化列表的行为是未可预期的,b_被初始化成了一个未定义的值。(欲知详情, 请参见常见错误 52。) 而最主要的不可见变化,在于编译器隐式提供的复制构造函数和赋值运算符的语义。默认地, 这些函数被定义成 inline 的。是故,它们编译后的代码就会被插入任何使用一个 C 对象 来初始化另一个 C 对象、或是使用一个 C 对象给另一个 C 对象赋值的代码中。(常见错误 49 里提及了有关这些函数的更多信息。) 这个对类 C 简单的修改带来的最主要的结果(让我们把上面提到的一个引入的缺陷暂时搁 下不提),就是几乎所有用到过类 C 的代码统统需要重新编译过。在一个大块头的项目里, 这种重新编译可能会旷日持久。如果类 C 是在一个头文件里定义的,所有包含了这个源文 件的代码都(连带地)需要重新编译过。有一个方法能够缓解此一境况,那就是使用类 C 的前置声明。具体做法倒也简明,就是当不需要除名字以外的其它信息时,像下面这样写一 第一章 基础问题 常见错误 8:未能区分可访问性和可见性 第 30 页 句非完整的类声明: class C; 就是这么一句平凡的、非 完整的声明语句,使得我们仍然可以声明基于类 C 的指针和引用, 前提是我们不进行任何需要类 C 的尺寸和成员的名称的操作(译注,如指针算术运算),包 括那些继承自类 C 的子类初始化类 C 部分子对象的操作。(可是你看看常见错误 39,凡事皆 有例外。) 这种手段可谓行之有效,不过要想免吃维护阶段的苦头,还要谨记严格区分“仅提供非完整 的类声明”和“提供完整类定义”的代码,不要把它们写到同一个源文件中去。也就是说, 想为复杂冗长的类定义提供上述轻量级替身的软件工程师,请不要忘记提供一个放置各种适 当前置声明的(译注:而不放置类定义的)专用头文件。 比如上例中,如果类 C 的完整定义是放在 c.h 这个头文件中的,我们就会考虑提供一个 cfwd.h, 里面只放置非完整的类声明。如果所有的应用都用不着 C 的完整定义,那末包含 c.h 就不如 包含 cfwd.h。这样做有什么好处呢?因为 C 这个名字的涵义在未来可能会发生变化,使得 一个简单的前置声明不容于新环境。比如,C 可能会在未来的实现中成为一个 typedef: template class Cbase{ // ... }; typedef Cbase C; 很清楚,那个头文件 c.h 的作者是在尽力避免当前类 C 的用户去修改他们的源代码(译注: 这个可怜的作者是想造出一种假象,使得像“C c;”这样的语句能够继续合法,遗憾的是 任何假象都会在某些情况下被揭穿),不过,任何在包含了 c.h 以后还想继续使用“C 的不完 整声明”的企图都会触发毫不留情的编译错误: #include "c.h" // ... class C; // 错误!C 现在不再是一个类的名字,而是一个 typedef。 因而,如果提供了一个前置声明专用头文件 cfwd.h 的话,这样问题就根本不会出现了。(译 注:那些根本用不着 C 的完整定义的代码才不管 C 是一个类还是一个 typedef。)所以, 这个锦囊妙计就在标准库中催生了 iosfwd,它就是(人尽皆知的)iostream 头文件对应的前 置声明专用头文件。 更为常见的是,由于必须对使用了类 C 的代码进行重新编译,结果就使得对已经部署了软 件打补丁这件事很难做。这么一来,也许最管用的解决方案就是把一个类的接口与其实现分 离,从而要达到真正的数据隐藏之境,而其不二法门则是运用桥接设计模式(Bridge design pattern)。 第一章 基础问题 常见错误 8:未能区分可访问性和可见性 第 31 页 桥接设计模式需要把一个目标类分为两个部分,也就是一个接口部分和一个实现部分: gotcha08/cbridge.h class C { public: C( int val ); ~C(); int get_a() const; int get_b() const; private: Cimpl *impl_; }; gotcha08/cbridge.cpp class Cimpl { public: Cimpl( int val ) : a_( val ), b_( a_ ) {} ~Cimpl() {} int get_a() const { return a_; } int get_b() const { return b_; } private: int a_; int b_; }; C::C( int val ): impl_( new Cimpl( val ) ) {} C::~C(){ delete impl_; } int C::get_a() const{ return impl_->get_a(); } int C::get_b() const{ return impl_->get_b(); } 此新接口包含了类 C 的原始接口,但类的实现则被移入了一个在一般应用中不可见的实现 类里。类 C 的新版本仅仅包含了一个指向实现类的指针,而整个实现,包括类 C 的成员函 数,现在都对使用了类 C 的代码不可见了。(译注:使用了桥接设计模式以后,有关类 C 的 代码就不再修改了,修改的只是实现类,也就是类 Cimpl 的代码。这样,使用了类 C 的代 码就不必重新编译,因为对于 C 来说,内存布局没有发生任何变化。这也就是 Herb Sutter 所谓的“编译防火墙”,参见 Herb Sutter,Exceptional C++,§4。)任何对于类 C 实现的修 第一章 基础问题 常见错误 8:未能区分可访问性和可见性 第 32 页 改(译注:亦即,对于类 Cimpl 的修改),只要不改变类 C 的接口(译注:亦即成员函数), 影响就会被牢牢地箝制在一个单独的实现文件里了。(译注:这就通过修改了实现的可见性, 给软件工程师带来了可贵的最小编译代价和变化影响的可控性,也就是让使用了类 C 的代 码“眼不见,心不烦”。) 运用桥接模式显然要付出一些运行时的成本,因为一个类 C 对象现在需要用两个对象,而 不是一个对象来表示了,而且调用所有的成员函数的动作现在由于是间接调用,也做不成 inline 的了。无论如何,它带来的好处是大幅节省了编译时间,而且不必重新编译就能发 布使用了类 C 的代码的更新。这在大多数情况下,可谓物美价廉。 此项技术已经被广泛应用多年,因而也被冠以数种趣名,如“pimpl 习惯用法”和“柴郡猫 技术”(译注:“柴郡猫”大致相当于“神龙见首不见尾”的涵义)之美誉。(译注:关于此 主题的更多信息,参见 Scott Meyers,Effective C++,3rd Edition,条款 31。) 不可访问的成员在通过继承接口访问时,会造成派生类成员和基类成员的语义发生变化。考 虑如下的基类和派生类: class B { public: void g(); private: virtual void f(); // 新添加的代码 }; class D : public B { public: void f(); private: double g; // 新添加的代码 }; 在类 B 这个基类中添加了一个私有访问的虚函数,导致了原先派生类中的非虚函数变成了 虚函数;添加在类 D 中的私有访问的数据成员则遮掩了 B 中的一个函数。这就是为什么继 承常常被视为“白盒”复用(译注:亦即源代码必须可见的情况下才能进行的复用),因为 对类的任何修改都在一个非常基本的层面同时影响着基类和派生类的语义。 一种能够削弱此类问题的方法,是采用一种简明的、根据功能划分名字的命名规范。典型的 办法是为型别的名字、私有访问的数据成员的名字或其它什么东西的名字使用不同的规范以 示区分。在本书中,我们的规范是使用全大写的型别名字,并在数据成员的后面附加一个下 划线(它们都是私有访问的!),而对于其它的名字(除一些特例外)我们用小写字母打头的 名字。如果遵守这样的规范,我们在上面的例子中就不会意外地遮掩基类中的成员函数了。 不过,最要紧的是不要建立一个极复杂的命名规范,因为如此规范往往形同具文。 此外,绝对不要让一个变量的型别成为它的名字的一部分。比如,把一个整型变量 index 第一章 基础问题 常见错误 9:使用糟糕的语言 第 33 页 命名为 iIndex 是对代码的理解和维护主动搞破坏。首先,一个名字应该描述一个实体的 抽象意义,而不是它的实现细节(数据抽象甚至在预定义的型别中就已经发挥了它的影响)。 再有,大多数的情况下,变量的型别改变的时候,它的名字不会同步地跟着其型别变化。这 么一来,变量的名字就成了迷惑维护工程师有关其型别信息的利器。 其它方法在一些别的地方时有讨论,特别在常见错误 70、73、74 和 77 中为最多。 常见错误 9:使用糟糕的语言 当一个更大的世界入侵了 C++社群原本悠然自得的乐土之时,它们带来了一些足堪天谴的语 言和编码实践。本条错误乃是为了厘清返璞归真的 C++语言所使用的正确适当、堪称典范之 用语和行为。 用语 表 1-1 列出了最常见的用语错误,以及它们对应的正确形式。 没有什么所谓“纯虚基类”。纯虚函数是有的,而包含有、或是未能改写(override)此种函 数的类,我们(并不叫它“纯虚基类”,而是)叫它“抽象类”。 C++语言里是没有“方法”的。Java 和 Smalltalk 里才有方法一说。当你颇带着一丝自命不 凡而就面向对象的话题侃侃而谈之时,你可能使用像“消息”和“方法”这种词汇。但如果 你开始脚踏实地,开始讨论你的设计对应的 C++实现时,最好还是使用“函数调用”或“成 员函数”来表达。 一些不足为信的 C++专家(你知道我在说谁)使用“destructed”作为“constructed”的对应 词。这明显是英语没学好(译注:destructed 是不及物动词,不可能有-ed 分词形式)。正确 的对应词是“destroyed”。 C++确实有强制转型(或曰型别转换)运算符——事实上(只)有四个(static_cast、 dynamic_cast、const_cast 以及 reinterpret_cast)。遗憾的是,“强制转型运算 符”常常被不正确地用于表达“成员型别转换运算符”,而后者指定了一个类的对象何以被 隐式地转为另一个型别。 class C { operator int *()const; // 一个(成员)型别转换运算符 //... }; 第一章 基础问题 常见错误 9:使用糟糕的语言 第 34 页 表 1-1|常见用语错误及其对应正确用语 错误用法 正确用法 (译注: 错误用法中译) (译注: 对应正确中文用法) Pure virtual base class Abstract class 纯虚基类 抽象类 Method Member function 方法 成员函数 Virtual method ??? 虚方法 ??? Destructed Destroyed ??? ??? Cast operator Conversion operator 强制转型运算符 (成员)型别转换运算符 当然用强制转换运算符来完成一个型别转换的工作也是允许的,只要你不把用语搞混就成。 请参见常见错误 31 中有关“常量指针”和“指向常量的指针”的讨论,以加深对本主题的 理解。 空指针 从前,当软件工程师使用预处理符号 NULL 来表示空指针时,他会遭遇潜在的灾难。 void doIt( char * ); void doIt( void * ); C *cp = NULL; 麻烦出在 NULL 这个符号在不同的平台上,有很多种定义的方法: #define NULL ((char *)0) #define NULL ((void *)0) #define NULL 0 这些各扫门前雪的不同定义严重损害了 C++语言的可移植性: doIt( NULL ); // 平台相关抑或模棱两可? C *cp = NULL; // 错误? 事实上,在 C++语言里是没有办法直接表示一个空指针的。但我们可以保证的是,数字字面 常量 0 可以转换成任何一种指针型别对应的空指针。那也就是传统的 C++语言保证可移植性 和正确性的用法。(译注:请参见 Bjarne Stroustrup,The Design and Evolution of C++,§11.2.3。) 现在,C++标准规定像(void *)0 这样的定义是不允许的(译注:请参见 ISO/IEC 14882, Programming languages – C++,2nd Edition,§16.3.5),可见这是个和 NULL 的使用并无多 大干系的技术问题(如若不然,NULL 岂不是成了一个格外受人青睐的预处理符号?其实它 第一章 基础问题 常见错误 10:无视(久经考验的)习惯用法 第 35 页 是普通不过的)。可是,真正属于 C++阵营的软件工程师仍然使用(字面常量)0。任何其它 用法都会给你打上无可救药的土鳖印记。 缩略词 C++软件工程师都有缩略词强迫症,不过似乎这种趋势没有蔓延到管理层。表 1-2 在你的同 事给你来上一句“ RVO 将不会应用到一个 POD 上,所以你最好自己写一个自定义的复制 ctor” 时能派上用场。 表 1-2|常用缩略词的意思 缩略词 完整形式 (译注:完整形式中译) POD Plain old data, a C struct 和 C 语言连接兼容的结构 POF Plain old function, a C function 和 C 语言连接兼容的函数 RVO Return value optimization 返回值优化 NRV Named RVO 具名的返回值优化 Ctor Constructor 构造函数 Dtor Destructor 析构函数 常见错误 10:无视(久经考验的)习惯用法 “很早就有人发现,最杰出的作家有时对修辞学的条条框框置若罔顾。不过,每当他们这 么做的时候,读者总能在这样的语句中找到一些补偿的闪光点,以抵消违反规则带来的代 价。也只有他们确信有这样的效果存在他们才这么做,否则他们会尽一切可能去因循那些 既成的规则。”(Strunk 和 White,The Elements of Style)1 以上这条被经常引用的、对于英语散文写作的经典导引却常常在指导软件开发中的文本书写 风格方面时也屡试不爽。我对本条金科玉律以及背后的深邃思想心悦诚服。不过,我对它还 有不甚满意的方面,那就是它在脱离了上下文的前提下,并未揭示为何通常情况下因循修辞 学的既成规则是事半功倍的,也未有阐明这些既成规则究竟是怎么来的。相比 Strunk 高高 在上的圣断,我倒更对 White 的朴实无华的“牛道”之比喻情有独衷。 仍在使用中的语言就像是奶牛群行经的道路:它的缔造者即奶牛群本身(译注:改编鲁迅 先生的一句话:世上本没有路,走的牛多了,也就成了路。),而这些缔造道路的奶牛 1 这段引言的实际作者是 William Strunk,在本书的原始卷宗里就已经有这段话了。但是这 本书的再次风靡是之后 1959 年 White 的功劳。 第一章 基础问题 常见错误 10:无视(久经考验的)习惯用法 第 36 页 们在踏出这条道路以后,或是一时兴起,或是实际有需,有时继续沿着它走,有时则大大 偏离。日复一日,这道路也历经沧桑变迁。对于一头特定的奶牛而言,它没有义务非得沿 着它亲身参与缔造的羊肠小道框出的轮廓行走不可。不过它如果因循而行,常常会因此得 益,而若非如此,则不免会因为不知身处何处和不知行往何方而给自己平添障碍。(E. B. White,The New Yorker 刊载文章) 软件开发的程序语言并不像自然语言那般复杂,因而我们 “撰写清晰代码” 的这一目标肯 定比“书写漂亮的(自然语言的)句子”要容易企及。当然,像 C++那样的程序语言的复杂 性已经使得用它来开发软件时的效能对遵循一套标准用法和习惯用法具有依赖性了。C++语 言的设计是不拘小节的,这给使用 C++进行软件开发的人带来了很大的弹性。但是,那些久 经考验的习惯用法为开发的效率和清晰的交流开启了方便之门。对这些习惯用法的无意忽视 甚至是有意违背,无异是对误解和误用的开门揖盗。 很多本书中的建议都包括了发掘和运用 C++编码和设计中的习惯用法。很多这里列举的常见 错误都可以直接视作对某个 C++习惯用法的背离。而针对它们提出的正解,则又经常可以看 成是向相应习惯用法的归顺。出现这种情况有一个好理由:有关 C++编码和设计中的习惯用 法乃是 C++软件工程师社群的所有人一起总结并不断地加以完善的。那些不管用的或是已经 过气的方法会逐渐失宠直至被扬弃。能够得以流传的习惯用法,都是那些持续演化以适应它 们的应用环境的。意识到,并积极运用 C++编码和设计中的习惯用法是产出清晰、有效和可 维护的 C++代码和设计的最确信无疑的坦途之一。 作为一个称职的专业软件工程师,我们可要时时刻刻告诫自己我们平常撰写的代码和设计都 是被涵盖在某个习惯用法的上下文里了。(译注:这是真正的大师经验之谈,这种时时刻刻 准备套用习惯用法的良好实践,不仅可以使我们摆脱从头再造轮子的重复之累,更能使我们 从别人擅长的技术中受益,并集中精力在自己擅长的问题上,十分符合分工互惠的经济学原 理。这种职业敏感是专业软件工程师和业余代码写手的极大不同,后者比较热衷于自己写点 新鲜的东西,自诩“创新”,但是水平却不敢恭维。)一旦我们识别出了某种编码和设计中的 习惯用法,我们既可以选择呆在它为我们营造的安乐小窝里,也可以选择在理性思考后为自 己的特殊需要而暂时背离它。无论如何,大多数情况下我们还是因循守旧一点好。有一点可 以肯定的是,如果我们连半点儿也没有意识到什么习惯用法的存在,我们的半路迷途就是注 定的了。 我并不想在不经意间给你留下“C++软件开发中的习惯用法就像一个讨厌的紧身衣一样把设 计流程的方方面面绑得死死的”这么个印象。远非如此。恰当运用习惯用法会让你的设计流 程和有关设计的交流变得极其简化,给设计师们留下了发挥其创作天赋的无尽空间。也有这 样的时候,哪怕是最合理的、最大路的软件开发中的习惯用法都会在碰到某种设计时不合用。 遇到这种情况,设计师就不得不另辟蹊径了。 最常用,也是最有用的 C++语言的习惯用法之一就是复制操作的习惯用法。所有 C++里的 抽象数据型别都需要做一个有关它的赋值运算符和复制构造函数的决定,那就是是允许编译 器自行生成它们、还是软件工程师自己手写它们,还是干脆禁止对它们的访问。(参见常见 错误 49) 如果软件工程师打算写这些操作,我们很清楚应该怎么写。当然了,编写这些操作的“标准” 第一章 基础问题 常见错误 10:无视(久经考验的)习惯用法 第 37 页 方法在过去的很多年里是不断演化的。这是习惯用法并非恣意妄为的好处之一:它们总是朝 着适应当下用法趋势的方向演化。 class X { public: X( const X & ); X &operator =( const X & ); // . . . }; 虽然 C++语言在如何定义复制操作这方面留下了很大的发挥空间,但是把它们像上面几行代 码展示的那样声明却几乎肯定是个好主意:两个操作(译注:完成复制的操作是由两个函数 来完成的,一个是复制构造函数,还有一个是赋值运算符)都以一个指涉到常量的引用为参 数,赋值运算符不是虚函数(译注:而复制构造函数不允许声明为虚函数),返回值是一个 指涉到非常量的引用(译注:应该返回一个指涉到*this 的引用,请参考 Scott Meyers, Effective C++,3rd Edition,条款 10)。显然,这些操作中的任何一个都不应该修改其操作数。 如果它们修改了,这是让人莫名其妙的。 X a; X b( a ); // a 不会被修改 a = b; // b 不会被修改 „„除了某些情况。 C++标准库里的 auto_ptr 模板就比较特立独行。这是一个资源句柄, 它能够在堆上分配的存储不再有用时,把这些存储的善后清理工作做好。 void f() { auto_ptr blob( new Blob ); // ... // 在此处把分配给 Blob 型别对象的存储自动清除 } 好极了,不过如果那些还在念书的实习生们写下这样大大咧咧的代码,可如何是好? void g( auto_ptr arg ) { // ... // 在此处把分配给 Blob 型别对象的存储自动清除 } void f() { auto_ptr blob( new Blob ); g( blob ); // 哎呀,在此处把分配给 Blob 型别对象的存储又清除了一遍! } 一个解决之道是把 auto_ptr 的复制操作彻底禁止,但这么一来就会严重限制它的用途, 第一章 基础问题 常见错误 11:聪明反被聪明误 第 38 页 也使得好多 auto_ptr 的习惯用法化为泡影。另一种做法是为 auto_ptr 加上一个引用计 数,但那么一来,使用资源句柄的代价就将膨胀。所以,标准的 auto_ptr 采取的做法是 故意地背离了复制操作的习惯用法: template class auto_ptr { public: auto_ptr( auto_ptr & ); auto_ptr &operator =( auto_ptr & ); // ... private: T *object_; }; (标准的 auto_ptr 还实现了这些非模板复制操作对应的模板成员函数,但是经验是相似 的,参见常见错误 88。)这里,操作符右边的操作数不是常量!当一个 auto_ptr 使用另一 个 auto_ptr 对象初始化或被赋值为另一个 auto_ptr 对象时,这个用于初始化或赋值的 源对象便中止了对它指涉的在堆上分配的对象的所有权,具体做法是把它内部原本指向对象 的指针置空。 就像背离了习惯用法通常所发生的那样,对于如何用好 auto_ptr 对象从一开始就引起了 不少困惑。当然,这个对已经存在的习惯用法的背离也搞出了不少多产的、围绕着所用权议 题的新用法,而将 auto_ptr 对象用作数据的“源”和“汇”看起来成为了一个获利颇丰 的新的设计领域。从效果上说,对一个已经存在的、业已成功的习惯用法采取了深思熟虑的 背离,反而产生了一个系列的新习惯用法。 常见错误 11:聪明反被聪明误 C++语言和 C 语言看起来会吸引相当多的人去显摆(你有没有听说过一个叫“Obfuscated Eiffel”的比赛?)(译注:这是一个以恶搞为能事的比赛,参赛者比的是谁能把合法的 C 语 言写得最难看懂、或是排版成各种花样。)。在这些软件工程师的思维里,两点间的最短距离 是普通欧氏空间之球面扭曲上的大圆。 试举一例:在 C++语言的圈子里(管它这个圈子是不是普通欧氏空间里的),代码的排版格 式纯粹是为了方便解读代码的人类的,而对于代码(译注:对机器而言)的意义,只要标记 块的顺序还是按原先的顺序的依次出现,就怎么都无所谓。这最后一个附加条款殊为重要, 比如,以下这两行(译注:这两行的“字面字符”是完全相同的,但这并不意味着它们表示 着同样的“标记块”)表示的是非常不同的意思(但是请看常见错误 87)。 a+++++b; // 错误!(译注:本行等价于 a++ ++ + b,而 a++不是一个左值。) a+++ ++b; // 没问题。 第一章 基础问题 常见错误 11:聪明反被聪明误 第 39 页 以下两行也是同出一辙(参见常见错误 17): ptr->*m; // 没问题。 ptr-> *m; // 错误!(译注:->*合起来才是一个运算符。) 上面的例子容易让大多数 C++软件工程师同意,只要注意不去趟标记块划分错误的浑水,代 码的排版格式就再次高枕无忧地和代码的意义无关了。因此呢,把一个声明变量的语句写在 一行里还是分成两行写,结果都是一样的。(有一些软件开发环境的调试器以及其它工具组 件是依据代码的行数,而不是其它更精确的定位逻辑来实现的。这样的工具经常强迫软件工 程师去把本来可以写在一行里的代码硬分成既不自然也不方便的数行来写,以得到更精准的 错误提示错误,或是设置更精准的断点,等等。这不是 C++语言的毛病,而是 C++软件开 发环境作者的毛病。) long curLine = __LINE__; // 取得当前行数值 long curLine = __LINE__ ; // 同样的声明(译注:但是,结果变得毫无意义了。) 绝大多数的 C++软件工程师们(译注:在这一点上)都会犯错。让我们看一个平凡的用模板 元编程实现的可以在编译期遴选一种型别的设备: gotcha11/select.h template struct Select { typedef A Result; }; template struct Select { typedef B Result; }; 具现 Select 模板的过程是先在编译期对一个条件评估求值,然后根据此表达式的布尔结 果具现此模板的两个版本之一。这相当于一个编译期的 if 语句说“如果条件为真,那么内 含的 Result 的型别就是 A,否则它的型别就是 B。” gotcha11/lineno.cpp Select< sizeof(int)==sizeof(long), int, long >::Result temp = 0; 第一章 基础问题 常见错误 12:嘴上无毛,办事不牢 第 40 页 上面这个语句声明了一个变量 temp,如果(译注:在某特定平台上)int 型别和 long 型 别占用的字节数是一样的,那末变量的 temp 的型别就是 int,否则它的型别就是 long。 再让我们看看前面声明的那个 curLine 吧。我们干嘛没事找事地写浪费那么多空格的空间 呢?不过权且让我们没什么理由地把问题复杂化好了: gotcha11/lineno.cpp const char CM = CHAR_MAX; const Select<__LINE__<=CM,char,long>::Result curLine = __LINE__; 上面这段代码是管用的(而且算他是正确的),但是这一行太长了,所以维护工程师便随后 稍稍把它重新排了一下版: gotcha11/lineno.cpp const Select<__LINE__<=CM,char,long>::Result curLine = __LINE__; 现在我们的代码里有了一个 bug,你看出来了吗? 在代码行数为 CHAR_MAX(它可能小到只有 127)的那一行里,以上的声明会导致什么结果? curLine 的型别会被声明为 char,并被初始化为 char 型别的最大值。随着我们把初始化 源放到了下一行,我们就会把 curLine 的值初始化为 char 型别的最大值还要大 1 的数。 这个结果很可能会指出,当前行数是一个负数(比如-128)(译注:在硬件采用补码编码的 机器上就会如此,比如 IBM PC 架构的机器)。多么聪明啊! 聪明反被聪明误在 C++软件工程师身上算一个常见的问题。请时刻牢记,几乎在所有的场合 下,遵循习惯用法、清晰的表达和一点点效率的折损都好过远非必要的小聪明、模棱两可和 维护便利的丧失。 常见错误 12:嘴上无毛,办事不牢 我们软件工程师在提出建议方面是巨人,但一到行动的时候就成了矮子。我们不懈地奉劝人 家不要使用全局变量、不好的变量名称、幻数等等,但在自己的代码里却常常放入这些东西。 第一章 基础问题 常见错误 12:嘴上无毛,办事不牢 第 41 页 这种现象困惑了在下多年,直到有一次我偶然读到一本描写青少年行为学的杂志时才豁然开 朗。对于青少年来说,指责别人的冒险行为是常事,但是他们常常有一种“个人幻想”,相 信他们自己对相同行为的一切负面效应都具有免疫力。那末我可以说,作为一个群体来说, 软件工程师看来是深受情感方面发展不足之苦的。 我曾经带过这么一些项目。在这些项目里有些软件工程师不仅拒绝服从编码规范,甚至会因 为被要求缩进四个空格而不是两个这样的小事而威胁要退出团队。我面临过这样的境遇:在 软件开发会议上,只要有一个派系的人参加,另一个就不参加。我见过这样的软件工程师: 他们故意地写没有文档的、令人费解的代码,这样其它人就没法去动这些代码了。我见过这 样根本不合格的软件工程师:他们拒绝接受比他们年长——或比他们年幼、或说话太直、或 太吹毛求疵——的同事的任何意见,并引起灾难性的结果。 不管情感上是不是嘴上无毛的青少年所特有的躁动性格,作为一个专业的软件工程师,我们 都有一些数量的成人的——或至少是专业的——责任。(参见美国计算机器协会在 ACM Code of Ethics and Professional Conduct 和 Software Engineering Code of Ethics and Professional Practice 对此类问题所持观点。) 首先,我们对我们自己选择的专业负有义务。从而我们应该做出有质量的工作,并在我们的 能力范围内做到最高的标准。 其次,我们对身处的社会和居住的星球负有义务。我们选择的专业在科学和实际服务的方面 都是平等一员。如果我们的工作不是为把我们身处的世界变得更好而作出贡献,我们就是在 浪费我们的才智和时间,最终浪费的,是我们自己的生命。 第三,我们对参与的社群负有义务。所以我们应该共享我们的长处,来影响公共政策。在我 们这个越来越技术化的社会里,最重要的决策都是那些受法学或政治学教育的人作出的,但 那些人在技术方面一窍不通。比如,某个州曾经一度把 π 的近似值以法律形式规定为 3。这 很滑稽(当然,那些以轮胎为基础的交通工具在这条法律寿终正寝之前只能颠簸不已),但 我们看到的许多秘而不宣的政策就不那么好玩了。我们有义务告知那些政坛精英们作为政策 基础的理性、技术和在数目字上的来龙去脉。 第四,我们对同事负有义务。所以我们应该有大度风范。这就包括我们应该遵守编码和设计 的“土政策”(如果这些“土政策”不好,我们应该变更它们而不是无视它们),写出易于维 护的代码,在表达我们自己意见的同时,也倾听别人怎么说。 这绝对不是让你系大溜、装老好人,或是为屈从团队权威和市井俗见而摇旗呐喊。我的一些 最满意的专业协作就是和一些离经叛道、身居要职、行事诡异的独行侠们共同完成的。但是 这些值得珍惜的同事中的每一个都既尊重我,也尊重我的想法(他们在我理应受嘉奖时不吝 其辞,也在我犯错时直言不讳),在和我一起工作时努力完成那些我们商议好了要完成的东 西。 第五,我们对同行负有义务。从而,我们应该共享我们的知识和经验。 最后,我们对自己负有义务。我们的工作和思想起码应该让我们自己感到满意,并让我们自 第一章 基础问题 常见错误 12:嘴上无毛,办事不牢 第 42 页 己觉得选择这一行情有可原。如果我们对我们从事的工作富有激情,如果我们从事的工作已 经融入成为我们的一部分,我们的这些义务就不再是一种负担,而是一种快乐了。 第二章 语法问题 常见错误 13: 第 43 页 第二章 语法问题 常见错误 13: 常见错误 14: 常见错误 15: 常见错误 16: 常见错误 17: 常见错误 18: 常见错误 19: 常见错误 20: 第三章 预处理器问题 常见错误 21: 第 44 页 常见错误 21: 常见错误 22: 常见错误 23: 常见错误 24: 第三章 预处理器问题 常见错误 25: 常见错误 26: 常见错误 27: 常见错误 28: 第四章 强制型别转换问题 常见错误 29: 第 45 页 第四章 强制型别转换问题 常见错误 29: 常见错误 30: 常见错误 31: 常见错误 32: 常见错误 33: 常见错误 34: 常见错误 35: 常见错误 36: 第四章 强制型别转换问题 常见错误 37: 第 46 页 常见错误 37: 常见错误 38: 常见错误 39: 常见错误 40: 常见错误 41: 常见错误 42: 常见错误 43: 常见错误 44: 常见错误 45: 第五章 初始化问题 常见错误 46: 第 47 页 常见错误 46: 第五章 初始化问题 常见错误 47: 常见错误 48: 常见错误 49: 常见错误 50: 常见错误 51: 常见错误 52: 常见错误 53: 第六章 内存和资源管理问题 常见错误 54: 第 48 页 常见错误 54: 常见错误 55: 常见错误 56: 常见错误 57: 常见错误 58: 常见错误 59: 第六章 内存和资源管理问题 常见错误 60: 常见错误 61: 第七章 多态问题 常见错误 62: 第 49 页 常见错误 62: 常见错误 63: 常见错误 64: 常见错误 65: 常见错误 66: 常见错误 67: 常见错误 68: 第七章 多态问题 常见错误 69: 第七章 多态问题 常见错误 70: 第 50 页 常见错误 70: 常见错误 71: 常见错误 72: 常见错误 73: 常见错误 74: 常见错误 75: 常见错误 76: 常见错误 77: 常见错误 78: 第八章 类设计问题 常见错误 79: 第 51 页 常见错误 79: 第八章 类设计问题 常见错误 80: 常见错误 81: 常见错误 82: 常见错误 83: 常见错误 84: 常见错误 85: 常见错误 86: 第九章 继承相关的设计问题 常见错误 87: 第 52 页 常见错误 87: 常见错误 88: 第九章 继承相关的设计问题 常见错误 89: 常见错误 90: 常见错误 91: 常见错误 92: 常见错误 93: 常见错误 94: 第九章 继承相关的设计问题 常见错误 95: 第 53 页 常见错误 95: 常见错误 96: 常见错误 97: 常见错误 98: 常见错误 99:

下载文档,方便阅读与编辑

文档的实际排版效果,会与网站的显示效果略有不同!!

需要 10 金币 [ 分享文档获得金币 ] 0 人已下载

下载文档

相关文档