1、1编程精粹 Microsoft 编写优质无错 C 程序秘诀Writing Clean Code Microsoft Techniques for Developing Bug-free CProgramsSteve Maguire 著姜静波 佟金荣 译麦中凡 校电子工业出版社2这份电子书籍由 PC Home 俱乐部、C+ Bulider 讨论区数位网友分别整理完成,基本上完全按照所据译本原貌,极少数文字为通顺起见稍作修改。由于并非一人整理完成,书中例程各章节代码书写风格可能稍有不同,如指针声明,以下两种写法都存在:void* pv; / *号与类型说明符相连void *pv; / *号与变量名
2、相连抱歉为阅读带来了麻烦。如果各位在阅读这份电子书籍时发现错误,请 E-mail 至 ,我们会尽快加以修正。原文书名:Writing Clean Code Microsoft Techniques for Developing Bug-free C ProgramsSteve maguire 著Microsoft Press 出版所据译本:编程精粹 Microsoft 编写优质无错 C 程序秘诀姜静波、佟金荣 译,麦中凡 校电子工业出版社 出版整理:Solmyr:序、某些背景、命名约定、引言、第 1、2、 3、8 章、后记、参考文献iliad: 第 4、5 章lavos: 第 6 章、附录
3、Awarz: 第 7 章chief: 附录 B、C校对、格式编排: Solmyr3目 录序 .4命名约定 .6某些背景 .7引言 .8第 1 章 假想的编译程序 .12第 2 章 自己设计并使用断言 .20第 3 章 为子系统设防 .45第 4 章 对程序进行逐条跟踪 .68第 5 章 糖果机界面 .76第 6 章 风险事业 .92第 7 章 编码中的假象 .116第 8 章 剩下来的就是态度问题 .134附录 A 编码检查表 .149附录 B 内存登录例程 .152附录 C 练习答案 .160后记 走向何方 .183序 某些背景 命名约定 引 言 第 1 章 假想的编译程序 1第 2 章 自
4、己设计并使用断言 8第 3 章 为子系统设防 31第 4 章 对程序进行逐条跟踪 53第 5 章 糖果机界面 60第 6 章 风险事业 75第 7 章 编码中的假象 98第 8 章 剩下的就是态度问题 115后 记 走向何方 129附录 A 编码检查表 130附录 B 内存登录例程 133附录 C 练习答案 140参考文献 160(注:上述页码是以原书为基准,与本电子版本没有什么关系)4献给我的妻子 Beth,以及我的双 亲 Joseph 和 Julia Maguire为了他们的爱和支持序1986 年,在为几家小公司咨询和工作了 10 年之后为了获得编写 Macintosh 应用程序的经验,我
5、特意到 Microsoft 公司工作,参加了 Macintosh 开发小组。这个小组负责Microsoft 的图形电子表格应用程序的开发。当时,我还不能肯定想象的代码是什么样子的,我想也许应该既引入入胜又雅致吧!但我看到的代码却很平常,与我以往见到的其它代码没有什么不同。要知道,Excel 有一个相当漂亮的用户界面 它比当时其它基于字符的电子表格软件更容易使用,更加直观。但使我感受更深的是产品中包含的一个多功能调试系统。该系统旨在自动地问程序员和测试者进行错误报警。其工作方式非常象波音 747 驾驶仓内向驾驶员报告故障的报警灯。该调试系统主要用来对代码进行监视,它并不过多地对代码进行测试。虽然
6、现在该调试系统采用的概念已不再新鲜了,但当时它们的广泛使用程度以及该系统有效的查错能力还是吸引了我,使我深受启发。没过多久,我就发现Microsoft 的大部分项目都有多功能的内部调试系统,而 Microsoft 的程序员都高度重视代码中的错误及其产生原因。在做了两年 Macintosh Excel 之后,我离开了该开发小组,去帮助另一个代码错误数目超常的小组。在开发 Excel 的两年中,我发现 Microsoft 虽然壮大了两倍,但许多老项目组熟知的概念并没有随着公司的壮大而传到新项目组。新程序员不象我加入 Microsoft之前时的老程序员一样对容易引起错误的编码习惯特别重视,而只有一般
7、的注意。在我转到新项目组六个月之后,有一次我对一个程序员伙伴提到:“应该把编写无错代码的某些概念写成文字,使那些原理能在新项目组传开” 。这时另一位程序员对我说:“你不要总是想着写文档,为什么你不把这一切都写下来?为什么你不写本书,问问Microsoft 出版社是否愿意出版呢?毕竟这些信息不是谁的专利,其作用不过是为了使程序员更加重视错误。 ”当时我对这个建议并没有多想,主要原因是没有时间,而且以前我也没有写过书。以前我所做过与写书最有关系的事情,不过是在 80 年代初协助别人主办 Hi-Res 杂志的程序设计专栏,这与写书毕竟不是一回事。正如您所见到的,这本书还是写出来了。理由很简单:199
8、0 年,由于错误越来越多,Microsoft 取消了一个尚未公布的产品。现在,错误越来越多已经不是什么新鲜事情,Microsoft 的几个竞争者都因此取消过一些项目。但 Microsoft 因为这种原因取消项目,5还是头一次。最近,随着接连不断地出现产品错误。管理人员终于开始叫嚷“受不了啦” ,并采取了一系列的措施企图将错误率下降到原先的水平。尽管如此,仍然没有人将这些错误因由记录下来。现在,Microsoft 已经比我刚进公司时大了九倍。很难设想,倘若没有准确的指南,公司怎样才能将出错率降低到原来的水平。尤其是在 Windows 和 Macintosh 的应用越来越复杂的情况下,更是如此。以
9、上就是我最终决定写这本书的原因Microsoft 出版社已经同意出版这本书。情况就是这样。我希望您会喜欢这本书,我力图使本书不那么枯燥并尽量有趣。Steve Maguire西雅图,华盛顿1992.10.22致谢我要感谢 Microsoft 出版社中帮助本书问世的所有人,尤其是在我写作过程中始终手把手地教我的两个人。首先我要感谢我的约稿编辑 Mike Halvorson,他使我能够按照自己的进度完成了这本书,并且耐心地解答了我这个新作者提出的许多问题。我还要特别感谢我的责任编辑 Erin OConnor 女士,她用了许多额外时间及早地对我写出的章节提出了反馈意见。没有他们的帮助,就不会有这本书。
10、Erin 还鼓励我以幽默的风格写这本书。她对文中的小俏皮话发笑当然不会使我不快。我还要感谢我的父亲 Joseph Maguire 是他在 70 年代中期把我引入早期的微机世界:Altair、IMSAI 和 Sol-20,使我与这一行结缘。我要感谢我于 198183 年在 Valpar International 工作时的伙伴 Evan Rosen,他是对我职业生涯最有影响的几个人之一,他的知识以及洞察力在本书中都有所体现。还有 Paul Davis,在过去的 10 年里,我与他在全国各地的许多项目中都有过愉快的合作,他也无疑的塑造了我的思维方式。最后感谢花时间阅读本书草稿,并提供技术反馈意见的
11、所有人。他们是:Mark Gerber、Melissa Glerum、Chris Mason、Dave Moore、 John Rae-Grant 和 Alex Tilles。特别感谢 Eric Schlegel 和 Paul Davis,他们不仅是本书草稿的评审者,而且在本书内容的构思上对我很有帮助。6命名约定本书采用的命名约定和 Microsoft 所使用的“匈牙利式”命名约定差不多。该约定是由生于匈牙利布达佩斯的 Charles Simonyi 开发的,它通过在数据和函数名中加入额外的信息以增进程序员对程序的理解。例如:char ch; /* 所有的字符变量均以 ch 开始 */byte
12、 b; /* 所有的字节均冠以 b */long l; /* 所有的长字均冠以 l */对于指向某个数据类型的指针,可以先象上面那样建立一个有类型的名字,然后给该名字加上前缀字母 P:char* pch; /* 指向 ch 的指针以 p 开始 */byte* pb; /* 同理 */long* pl;void* pv; /* 特意显用的空指针 */char* ppch; /* 指向字符指针的指针 */byte* ppb; /* 指向字节指针的指针 */匈牙利式名字通常不那么好念,但在代码中读到它们时,确实可以从中得到许多的信息。例如,当你眼看到某个函数里有一个名为 pch 的变量时,不用查看声
13、明就立即知道它是一个指向字符的指针。为了使匈牙利式名字的描述性更强或者要区分两个变量名,可以在相应类型派生出的基本名字之后加上一个以大写字母开头的“标签” 。例如,strcpy 函数有两个字符指针参数:一个是源指针,另一个是目的指针。使用匈牙利式命名约定,其相应的原型是:char* strcpy(char* pchTo, char* pchFrom); /* 原型 */在上面的例子中,两个字符指针有一个共同的特点 都指向以 0 为结尾的 C 的字符串。因此在本书中,每当用字符指针指向字符串时,我们就用一个更有意义的名子 str来表示。因此,上述 strcpy 的原型则为:char* strcp
14、y(char* strTo, char* strFrom) /* 原型 */本书用到另一个类型是 ANSI 标准中的类型 size_t。下面给出该类型的一些典型用法:size_t sizeNew, sizeOld; /* 原型 */void* malloc(size_t size); /* 原型 */void* realloc(void* pv, size_t sizeNew); /* 原型 */函数和数组的命名遵循同样的约定,名字由相应的返回类型名开始,后跟一个描述的标签。例如:ch = chLastKeyPressed; /* 由变量得一字符 */ch = chInputBuffer; /
15、* 由数组得一字符 */ch = chReadKeyboard; /* 由函数得一字符 */7如果利用匈牙利式命名方法,mall和 reali可以写成如下形式:void* pvNewBlock(size_t size); /* 原型 */void* pvResizeBlock(void* pv, size_t sizeNew); /* 原型 */由于匈牙利式命名方法旨在增进程序员对程序的理解,所以大多数匈牙利式名字的长度都要超过 ANSI 严格规定 6 个字母的限制。这就不妙,除非所用的系统是几十年前设计的系统,否则这 6 个字母的限制只当是历史的遗迹。以上内容基本上没有涉及到匈牙利式命名约定
16、的细节,所介绍的都是读者理解本书中所用变量和函数名称意义的必需内容。如果读者对匈牙利式命名约定的详细内容感兴趣,可以参考本书末尾参考文献部分列出的 Simonyi 的博士论文。某些背景本书用到了一些读者可能不太熟悉的软件和硬件系统的名称。下面对其中最常见的几个系统给出简单的描述Macintosh Macintosh 是 Apple 公司的图形窗口计算机,公布于 1984 年。它是最先支持“所见即所得”拥户界面的流行最广的计算机。Windows Windows 是 Microsoft 公司的图形窗口操作系统。Microsoft 公司1990 年公布了 Windows 3.0,该版本明显好于其早期
17、版本。Excel Excel 是 Microsoft 公司的图形电子表格软件,1985 年首次在Macintosh 上公布,随后在进行了大量的重写和排错工作后,被移植到 Windows 上。多年来 Macintosh Excel 和 Windows Excel 共用一个名字,但程序所用的代码并不相同。在本书中,找多次提到曾经当过 Macintosh Excel 程序员这一经历。但应该说明的是,我的大部分工作是将 Windows Excel 的代码移到 Macintosh Excel 上或者是实现与 Windows Excel 相似的特征。我与该产品现在的惊人成功并没有特别的关系。我为 Maci
18、ntosh Excel 所做的唯一真正重要的贡献是说服Microsft 放弃掉 Macintosh Excel,而直接利用 Windows Excel 的源代码构造 Macintosh 的产品。Macintosh 2.2 版是第一个基于Windows Excel 的版本,它享用了 Windows Excel 80%的源代码。这对 Macintosh Excel 的用户意义重大,因为用了 2.2 版以后他们会感至 Macintosh Excel 的功能和质量都有了一个很大的飞跃。Word Word 是 Microsoft 公司的字处理应用软件。实际上,Word 有三种版本:基于字符并在 MSDO
19、S 上运行的 DOS 版;Macintosh 版;Windows 版。到目前为止,这三种版本的产品虽然是用不同的源代码做出的,但它们非常相象,用户改用任何一个都不会有什么困难。8由于 Excel 利用共享代码获得了成功,Microsoft 已经决定 Word 的高版本也将采用共享代码进行开发。80x86 80x86 是 MS-DOS 和 Windows 机器常用的 Intel CPU 系列。680x0 680x0 是各种 Macintosh 所用的 Motorola CPU 系列。引言几年前在一次偶然翻阅 Donald Knuth 所著TEX:The Program一书时,序言中的一段话深深触
20、动了我:我确信 TEX 的最后一个错误已经在 1985 年 11 月 27 日被发现并排除掉了。但是如果出于目前尚不知道的原因,TEX 仍然潜伏有错误,我非常愿意付给第一个发现者$20.48 元。(这一金额已是以前的两倍。我打算在本年内再增加一倍。你看我是多么自信!)我对 Knuth 是否曾经付给某人$20.48 甚至$40.96 元不感兴趣,这并不重要。重要的是他对他的程序所具有的那种自信。那么据你所知,究竟有多少程序员会严肃地声称他们的程序完全没有错误?又有多少敢把这一声称印刷在书上,并准备为错误的发现者付钱呢?如果程序员确信测试组已经发现了所有的错误,那么他也许敢作这种声明。但这本身就是
21、一个问题。每当代码被打包装送给程序经销商之前,人们在胸前划着十字带着最好的愿望说:“希望测试已经发现了所有的错误” 。这一情景我已见过多次了。由于现代的程序员已经放弃了对代码进行彻底测试的职责,他们没法知道代码中是否有错。管理人员也不会公布测试情况,只是说:“别操那个心,测试人员会为你作好测试的” 。更为微妙的是,管理人员希望程序员自己进行代码的测试。同时,他们希望测试员作得更彻底些,因为毕竟这是他们的本职工作。正如你在本书中将会看到的那样,编写无错代码的技术或许有几百种,程序员能用,但测试人员却无法使用,因为这些技术和代码的编写直接相关。两个关键的问题本书介绍的所有决窍是当发现错误时,不断地
22、就以下两个问题追问自己的结果: 怎样才能自动地查出这个错误? 怎样才能避免这个错误?第一个问题可能使读者认为本书是有关测试的书,其实不是。当编辑程序发现语法错误时,它是在做测试吗?不,不是。编辑程序只是在自动地检查代码中的错误。语法错误只是程序员可以使用的自动查错方法查出的一种最基本的错误类型。本书将详尽介绍自动9向程序员提示错误的方法。编写无错代码的最好方法是把防上错误放在第一位。关于这个问题,同样也有许多的技巧。某些技巧与常用的编码惯例有关,但它们不是象“每个人都违背原则”或“没有人违背该原则”这样泛泛地考虑问题,而是对相应的细节进行详细的讨论。要记住,在任何时候跟在大多数人的后面常常是所
23、能选择的最坏一条路。因此在成为别人的追随者之前一定要确定这样做确实有意义,而且不要仅仅因为其它的什么人如此自己也如此。本书的最后一章讨论编写无错代码应持正确态度的重要性。如果没有正确的态度,发现错误和防止错误就好比在冬季大开着窗户给房间加热,虽然也能达到目的,但要浪费大量的能量。本书除第 4 章和第 8 章以外都配有练习。但要注意,这些练习并不是要测验读者对相应内容的理解。实际上更多的是作者想在该章的正文中阐述却没能放进去的要点。其它的练习为的是让读者思考与该章内容有关的一些问题,打开思路,琢磨一下以前未曾考虑过的概念。无论哪种情况,都是想通过练习再补充一些新的技巧和信息,因此值得一读。为使读
24、者了解作者的意图,本书在附录 C 中提供了所有练习的答案。大部分章节还给出了一些课题,但这些课题没有答案,因为这种课题通常是任务,而不是问题。规则或者建议本书的编排类似于 Brian Kernighan 和 P. J. Plauger 所写的程序设计经典著作The Elements of Programming Sytle 。该书出于 William Strunk Jr.和 E. B. White 所写的重要经典著作The Elements of Style 。这两本书采用同样的基本概念表达方法: 给出一个例子; 指出该例子中的某些问题所在; 用一般的准则改进该例子。确实,这是个程式,而且是使
25、读者感到舒服的程式,因此本书同样采用了这一程式。作者力图使本书读起来是一种享受,尽管它有着公式的性质。希望读者会觉得本书很有趣。本书还给出一些似乎不应违背的“一般准则” 。我们的第一条准则是:由于准则是用来说明一般情况的,所以本书一般并不指明准则的例外情况,而把它留给读者。我相信,当读者读到某个准则时,肯定会怀疑道:“噢,当时,不是这样的” 。如果某个人对你说:“不能闯红灯” ,虽然这是一条准则,但你肯定能够举出一种特殊情况,在这种情况下闯红灯倒是个正确的行动。这里关键是要记住准则只是在一般每条准则都有例外10情况下才有意义,因此只有理由十分充足时,才可以违背准则。关于本书代码的说明本书的所有
26、代码都是按 ANSI C 写的,并且通过了 MS-DOS、Microsoft Windows 和Apple Macintosh 上五个流行编译程序的测试:Microsoft C/C+ 7.0 Microsoft 公司Turbo C/C+ 3.0 Borland 国际公司Aztec 5.2 Manx 软件系统公司MPW C 3.2 Apple 计算机公司THINK C 5.0 Symantec 公司还有一个问题:如果读者想从本书中摘取代码用在自己的程序中,那要小心。因为为了说明书中的论点,许多例子都有错误。另外,书中用到的函数虽然名字和功能都与 ANSI C 的标准库函数相同,但已对相应的界面进
27、行了一些小的修改。例如 ANSI 版 memchr 函数的界面是:void* memchr(const void* s, int c, size_t n);这里 memchr 的内部将整数 c 当作 unsigned char 来处理。在本书的许多地方,读者都会看到字符类型被显式地声明为 unsigned char,而不是 int:void* memchr(const void* pv, unsigned char ch, size_t size);ANSI 标准将所有的字符变元都声明为 int,是为了保证其库函数同样可以用于 ANSI 标准之前编写的非原型程序,这时程序使用 extern 声
28、明函数。由于在本书中只使用 ANSI C,所以不必考虑这些向下兼容的细节而可以用更加精确的类型声明以改进程序的清晰程度并用原型进行强类型检查(详见第 1 章) 。“提到 Macintosh 了吗?”出于某种原因,一本书如果没有提到 PDP11,Honeywell 6000,当然还有 IBM 360,就不会被认真对待。因此,我在这里也提到了它们。仅此而已,读者在本书中再也不会见到这些字眼、读者见到最多的是 MS-DOS,Microsoft Windows,特别还有 Apple Macintosh,因为近年来我一直为这些系统编写代码。但是应该注意,本书中的任何代码都不受这些特定的系统约束。它们都是用通用的 C 编写的,应该能够工作于任何的 ANSI C 编译程序下。因此即使读者使用的系统本书没有提及,也不必担心这些操作系统的细节会产生障碍。应该提到的是在大多数的微机系统中,用户都可以通过 NULL 指针进行读写,破坏栈框架并在内存甚至是其它应用程序的内存区中留下许多的无用信息,而硬件并没有什么反应,听任用户为所欲为。之所以提到这一点,是因为如果读者习惯于认为通过 NULL 指针进行写