1、1 编程精粹 Microsoft 编写优质无错 C 程序秘诀 Writing Clean Code Microsoft Techniques for Developing Bug-free C Programs Steve Maguire 著 姜静波 佟金荣 译 麦中凡 校 电子工业出版社 这份电子书籍由 PC Home 俱乐部、C+ Bulider 讨论区数位网友分别整理完成,基本上完全按照所据译本原 貌,极少数文字为通顺起见稍作修改。由于并非一人整理完成,书中例程各章节代码书写风格可能稍有不同, 如指针声明,以下两种写法都存在: void* pv; / *号与类型说明符相连 void *p
2、v; / *号与变量名相连 抱歉为阅读带来了麻烦。 如果各位在阅读这份电子书籍时发现错误,请 E-mail 至 wizard ,我们会尽快加以修正。 原文书名: Writing Clean Code Microsoft Techniques for Developing Bug-free C Programs Steve maguire 著 Microsoft Press 出版 所据译本: 编程精粹 Microsoft 编写优质无错 C 程序秘诀 2 姜静波、佟金荣 译,麦中凡 校 电子工业出版社 出版 整理: Solmyr:序、某些背景、命名约定、引言、第 1、2、3、8 章、后记、参考文献
3、 iliad: 第 4、5 章 lavos: 第 6 章、附录 A warz: 第 7 章 chief: 附录 B、C 校对、格式编排: Solmyr 目 录 序 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 序
4、某些背景 命名约定 引 言 第 1 章 假想的编译程序 1 第 2 章 自己设计并使用断言 8 第 3 章 为子系统设防 31 第 4 章 对程序进行逐条跟踪 53 第 5 章 糖果机界面 60 第 6 章 风险事业 75 第 7 章 编码中的假象 98 第 8 章 剩下的就是态度问题 115 后 记 走向何方 129 附录 A 编码检查表 130 附录 B 内存登录例程 133 3 附录 C 练习答案 140 参考文献 160 (注:上述页码是以原书为基准,与本电子版本没有什么关系) 献给我的妻子 Beth, 以及我的双亲 Joseph 和 Julia Maguire 为了他们的爱和支持 序
5、 1986 年,在为几家小公司咨询和工作了 10 年之后为了获得编写 Macintosh 应用程序的经验,我特意到 Microsoft 公司工作,参加了 Macintosh 开发小组。这个小组负责 Microsoft 的图形电子表格应用程序的开发。 当时,我还不能肯定想象的代码是什么样子的,我想也许应该既引入入胜又雅致吧!但我看到的代码却很平 常,与我以往见到的其它代码没有什么不同。要知道,Excel 有一个相当漂亮的用户界面 它比当时 其它基于字符的电子表格软件更容易使用,更加直观。但使我感受更深的是产品中包含的一个多功能调试系 统。 该系统旨在自动地问程序员和测试者进行错误报警。其工作方式
6、非常象波音 747 驾驶仓内向驾驶员报告故障 的报警灯。该调试系统主要用来对代码进行监视,它并不过多地对代码进行测试。虽然现在该调试系统采用 的概念已不再新鲜了,但当时它们的广泛使用程度以及该系统有效的查错能力还是吸引了我,使我深受启发。 没过多久,我就发现 Microsoft 的大部分项目都有多功能的内部调试系统,而 Microsoft 的程序员都高度重视 代码中的错误及其产生原因。 在做了两年 Macintosh Excel 之后,我离开了该开发小组,去帮助另一个代码错误数目超常的小组。在开发 Excel 的两年中,我发现 Microsoft 虽然壮大了两倍,但许多老项目组熟知的概念并没有
7、随着公司的壮大而传 到新项目组。新程序员不象我加入 Microsoft 之前时的老程序员一样对容易引起错误的编码习惯特别重视, 而只有一般的注意。 在我转到新项目组六个月之后,有一次我对一个程序员伙伴提到:“应该把编写无错代码的某些概念写成文 字,使那些原理能在新项目组传开” 。这时另一位程序员对我说:“你不要总是想着写文档,为什么你不把 这一切都写下来?为什么你不写本书,问问 Microsoft 出版社是否愿意出版呢?毕竟这些信息不是谁的专利, 其作用不过是为了使程序员更加重视错误。 ” 当时我对这个建议并没有多想,主要原因是没有时间,而且以前我也没有写过书。以前我所做过与写书最有 关系的事
8、情,不过是在 80 年代初协助别人主办 Hi-Res 杂志的程序设计专栏,这与写书毕竟不是一回事。 正如您所见到的,这本书还是写出来了。理由很简单:1990 年,由于错误越来越多,Microsoft 取消了一个 尚未公布的产品。现在,错误越来越多已经不是什么新鲜事情,Microsoft 的几个竞争者都因此取消过一些项 目。但 Microsoft 因为这种原因取消项目,还是头一次。最近,随着接连不断地出现产品错误。管理人员终 于开始叫嚷“受不了啦” ,并采取了一系列的措施企图将错误率下降到原先的水平。尽管如此,仍然没有人 将这些错误因由记录下来。 现在,Microsoft 已经比我刚进公司时大了
9、九倍。很难设想,倘若没有准确的指南,公司怎样才能将出错率降 低到原来的水平。尤其是在 Windows 和 Macintosh 的应用越来越复杂的情况下,更是如此。 以上就是我最终决定写这本书的原因 Microsoft 出版社已经同意出版这本书。 情况就是这样。 我希望您会喜欢这本书,我力图使本书不那么枯燥并尽量有趣。 4 Steve Maguire 西雅图,华盛顿 1992.10.22 致谢 我要感谢 Microsoft 出版社中帮助本书问世的所有人,尤其是在我写作过程中始终手把手地教我的两个人。 首先我要感谢我的约稿编辑 Mike Halvorson,他使我能够按照自己的进度完成了这本书,并
10、且耐心地解答了 我这个新作者提出的许多问题。我还要特别感谢我的责任编辑 Erin OConnor 女士,她用了许多额外时间及 早地对我写出的章节提出了反馈意见。没有他们的帮助,就不会有这本书。Erin 还鼓励我以幽默的风格写这 本书。她对文中的小俏皮话发笑当然不会使我不快。 我还要感谢我的父亲 Joseph Maguire 是他在 70 年代中期把我引入早期的微机世界:Altair 、IMSAI 和 Sol- 20,使我与这一行结缘。我要感谢我于 198183 年在 Valpar International 工作时的伙伴 Evan Rosen,他是对 我职业生涯最有影响的几个人之一,他的知识以
11、及洞察力在本书中都有所体现。还有 Paul Davis,在过去的 10 年里,我与他在全国各地的许多项目中都有过愉快的合作,他也无疑的塑造了我的思维方式。 最后感谢花时间阅读本书草稿,并提供技术反馈意见的所有人。他们是:Mark Gerber、Melissa Glerum、Chris Mason、Dave Moore、John Rae-Grant 和 Alex Tilles。特别感谢 Eric Schlegel 和 Paul Davis,他 们不仅是本书草稿的评审者,而且在本书内容的构思上对我很有帮助。 命名约定 本书采用的命名约定和 Microsoft 所使用的“匈牙利式”命名约定差不多。该
12、约定是由生于匈牙利布达佩斯 的 Charles Simonyi 开发的,它通过在数据和函数名中加入额外的信息以增进程序员对程序的理解。例如: char ch; /* 所有的字符变量均以 ch 开始 */ byte b; /* 所有的字节均冠以 b */ long l; /* 所有的长字均冠以 l */ 对于指向某个数据类型的指针,可以先象上面那样建立一个有类型的名字,然后给该名字加上前缀字母 P: char* pch; /* 指向 ch 的指针以 p 开始 */ byte* pb; /* 同理 */ long* pl; void* pv; /* 特意显用的空指针 */ char* ppch;
13、/* 指向字符指针的指针 */ byte* ppb; /* 指向字节指针的指针 */ 匈牙利式名字通常不那么好念,但在代码中读到它们时,确实可以从中得到许多的信息。例如,当你眼看到 某个函数里有一个名为 pch 的变量时,不用查看声明就立即知道它是一个指向字符的指针。 为了使匈牙利式名字的描述性更强或者要区分两个变量名,可以在相应类型派生出的基本名字之后加上一 个以大写字母开头的“标签” 。例如,strcpy 函数有两个字符指针参数:一个是源指针,另一个是目的指针。 使用匈牙利式命名约定,其相应的原型是: char* strcpy(char* pchTo, char* pchFrom); /*
14、 原型 */ 在上面的例子中,两个字符指针有一个共同的特点 都指向以 0 为结尾的 C 的字符串。因此在本书中, 每当用字符指针指向字符串时,我们就用一个更有意义的名子 str 来表示。因此,上述 strcpy 的原型则为: char* strcpy(char* strTo, char* strFrom) /* 原型 */ 本书用到另一个类型是 ANSI 标准中的类型 size_t。下面给出该类型的一些典型用法: size_t sizeNew, sizeOld; /* 原型 */ void* malloc(size_t size); /* 原型 */ 5 void* realloc(void*
15、 pv, size_t sizeNew); /* 原型 */ 函数和数组的命名遵循同样的约定,名字由相应的返回类型名开始,后跟一个描述的标签。例如: ch = chLastKeyPressed; /* 由变量得一字符 */ ch = chInputBuffer; /* 由数组得一字符 */ ch = chReadKeyboard; /* 由函数得一字符 */ 如果利用匈牙利式命名方法,mall和 reali可以写成如下形式: void* pvNewBlock(size_t size); /* 原型 */ void* pvResizeBlock(void* pv, size_t sizeNew)
16、; /* 原型 */ 由于匈牙利式命名方法旨在增进程序员对程序的理解,所以大多数匈牙利式名字的长度都要超过 ANSI 严格 规定 6 个字母的限制。这就不妙,除非所用的系统是几十年前设计的系统,否则这 6 个字母的限制只当是历 史的遗迹。 以上内容基本上没有涉及到匈牙利式命名约定的细节,所介绍的都是读者理解本书中所用变量和函数名称意 义的必需内容。如果读者对匈牙利式命名约定的详细内容感兴趣,可以参考本书末尾参考文献部分列出的 Simonyi 的博士论文。 某些背景 本书用到了一些读者可能不太熟悉的软件和硬件系统的名称。下面对其中最常见的几个系统给出简单的描述 Macintosh Macinto
17、sh 是 Apple 公司的图形窗口计算机,公布于 1984 年。它是最先支持“所见即所得 ” 拥户界面的流行最广的计算机。 Windows Windows 是 Microsoft 公司的图形窗口操作系统。 Microsoft 公司 1990 年公布了 Windows 3.0,该版本明显好于其早期版本。 Excel Excel 是 Microsoft 公司的图形电子表格软件,1985 年首次在 Macintosh 上公布,随后在进行了 大量的重写和排错工作后,被移植到 Windows 上。多年来 Macintosh Excel 和 Windows Excel 共用一个名字, 但程序所用的代码并
18、不相同。 在本书中,找多次提到曾经当过 Macintosh Excel 程序员这一经历。但应该说明的是,我的大部分工作是将 Windows Excel 的代码移到 Macintosh Excel 上或者是实现与 Windows Excel 相似的特征。我与该产品现在的 惊人成功并没有特别的关系。 我为 Macintosh Excel 所做的唯一真正重要的贡献是说服 Microsft 放弃掉 Macintosh Excel,而直接利用 Windows Excel 的源代码构造 Macintosh 的产品。 Macintosh 2.2 版是第一个基于 Windows Excel 的版本,它享 用了
19、 Windows Excel 80%的源代码。这对 Macintosh Excel 的用户意义重大,因为用了 2.2 版以后他们会感至 Macintosh Excel 的功能和质量都有了一个很大的飞跃。 Word Word 是 Microsoft 公司的字处理应用软件。实际上,Word 有三种版本:基于字符并在 MSDOS 上运行的 DOS 版;Macintosh 版;Windows 版。到目前为止,这三种版本的产品虽然是用不同的 源代码做出的,但它们非常相象,用户改用任何一个都不会有什么困难。由于 Excel 利用共享代码获得了成 功,Microsoft 已经决定 Word 的高版本也将采用
20、共享代码进行开发。 80x86 80x86 是 MS-DOS 和 Windows 机器常用的 Intel CPU 系列。 680x0 680x0 是各种 Macintosh 所用的 Motorola CPU 系列。 引言 几年前在一次偶然翻阅 Donald Knuth 所著TEX : The Program一书时,序言中的一段话深深触动了我: 我确信 TEX 的最后一个错误已经在 1985 年 11 月 27 日被发现并排除掉了。但是如果出于目前尚不知道的原 因,TEX 仍然潜伏有错误,我非常愿意付给第一个发现者$20.48 元。 (这一金额已是以前的两倍。我打算在 本年内再增加一倍。你看我是
21、多么自信!) 我对 Knuth 是否曾经付给某人$20.48 甚至$40.96 元不感兴趣,这并不重要。重要的是他对他的程序所具有的 6 那种自信。那么据你所知,究竟有多少程序员会严肃地声称他们的程序完全没有错误?又有多少敢把这一声 称印刷在书上,并准备为错误的发现者付钱呢? 如果程序员确信测试组已经发现了所有的错误,那么他也许敢作这种声明。但这本身就是一个问题。每当代 码被打包装送给程序经销商之前,人们在胸前划着十字带着最好的愿望说:“希望测试已经发现了所有的错 误” 。这一情景我已见过多次了。 由于现代的程序员已经放弃了对代码进行彻底测试的职责,他们没法知道代码中是否有错。管理人员也不会
22、公布测试情况,只是说:“别操那个心,测试人员会为你作好测试的” 。更为微妙的是,管理人员希望程序 员自己进行代码的测试。同时,他们希望测试员作得更彻底些,因为毕竟这是他们的本职工作。 正如你在本书中将会看到的那样,编写无错代码的技术或许有几百种,程序员能用,但测试人员却无法使用, 因为这些技术和代码的编写直接相关。 两个关键的问题 本书介绍的所有决窍是当发现错误时,不断地就以下两个问题追问自己的结果: 怎样才能自动地查出这个错误? 怎样才能避免这个错误? 第一个问题可能使读者认为本书是有关测试的书,其实不是。当编辑程序发现语法错误时,它是在做测试吗? 不,不是。编辑程序只是在自动地检查代码中的
23、错误。语法错误只是程序员可以使用的自动查错方法查出的 一种最基本的错误类型。本书将详尽介绍自动向程序员提示错误的方法。 编写无错代码的最好方法是把防上错误放在第一位。关于这个问题,同样也有许多的技巧。某些技巧与常用 的编码惯例有关,但它们不是象“每个人都违背原则”或“没有人违背该原则”这样泛泛地考虑问题,而是 对相应的细节进行详细的讨论。要记住,在任何时候跟在大多数人的后面常常是所能选择的最坏一条路。因 此在成为别人的追随者之前一定要确定这样做确实有意义,而且不要仅仅因为其它的什么人如此自己也如此。 本书的最后一章讨论编写无错代码应持正确态度的重要性。如果没有正确的态度,发现错误和防止错误就好
24、 比在冬季大开着窗户给房间加热,虽然也能达到目的,但要浪费大量的能量。 本书除第 4 章和第 8 章以外都配有练习。但要注意,这些练习并不是要测验读者对相应内容的理解。实际上 更多的是作者想在该章的正文中阐述却没能放进去的要点。其它的练习为的是让读者思考与该章内容有关的 一些问题,打开思路,琢磨一下以前未曾考虑过的概念。 无论哪种情况,都是想通过练习再补充一些新的技巧和信息,因此值得一读。为使读者了解作者的意图,本 书在附录 C 中提供了所有练习的答案。大部分章节还给出了一些课题,但这些课题没有答案,因为这种课题 通常是任务,而不是问题。 规则或者建议 本书的编排类似于 Brian Kerni
25、ghan 和 P. J. Plauger 所写的程序设计经典著作The Elements of Programming Sytle 。该书出于 William Strunk Jr.和 E. B. White 所写的重要经典著作The Elements of Style 。这两本书采 用同样的基本概念表达方法: 给出一个例子; 指出该例子中的某些问题所在; 用一般的准则改进该例子。 确实,这是个程式,而且是使读者感到舒服的程式,因此本书同样采用了这一程式。作者力图使本书读起来 是一种享受,尽管它有着公式的性质。希望读者会觉得本书很有趣。 本书还给出一些似乎不应违背的“一般准则” 。我们的第一条准
26、则是: 由于准则是用来说明一般情况的,所以本书一般并不指明准则的例外情况,而把它留给读者。我相信,当读 每条准则都有例外 7 者读到某个准则时,肯定会怀疑道:“噢,当时,不是这样的” 。如果某个人对你说:“不能闯红 灯” ,虽然这是一条准则,但你肯定能够举出一种特殊情况,在这种情况下闯红灯倒是个正确的行动。这里 关键是要记住准则只是在一般情况下才有意义,因此只有理由十分充足时,才可以违背准则。 关于本书代码的说明 本书的所有代码都是按 ANSI C 写的,并且通过了 MS-DOS、Microsoft Windows 和 Apple Macintosh 上五个 流行编译程序的测试: Micros
27、oft 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 的标准库函数相同,但已对相应的界 面进行了一些小的修改。例如 ANSI 版 memchr 函数的界面是: void* memchr(const void* s, int c, size_t
28、n); 这里 memchr 的内部将整数 c 当作 unsigned char 来处理。在本书的许多地方,读者都会看到字符类型被显式 地声明为 unsigned char,而不是 int: void* memchr(const void* pv, unsigned char ch, size_t size); ANSI 标准将所有的字符变元都声明为 int,是为了保证其库函数同样可以用于 ANSI 标准之前编写的非原型 程序,这时程序使用 extern 声明函数。由于在本书中只使用 ANSI C,所以不必考虑这些向下兼容的细节而 可以用更加精确的类型声明以改进程序的清晰程度并用原型进行强类型检
29、查(详见第 1 章) 。 “提到 Macintosh 了吗?” 出于某种原因,一本书如果没有提到 PDP11,Honeywell 6000,当然还有 IBM 360,就不会被认真对待。因 此,我在这里也提到了它们。仅此而已,读者在本书中再也不会见到这些字眼、读者见到最多的是 MS- DOS,Microsoft Windows,特别还有 Apple Macintosh,因为近年来我一直为这些系统编写代码。但是应该注 意,本书中的任何代码都不受这些特定的系统约束。它们都是用通用的 C 编写的,应该能够工作于任何的 ANSI C 编译程序下。因此即使读者使用的系统本书没有提及,也不必担心这些操作系统
30、的细节会产生障碍。 应该提到的是在大多数的微机系统中,用户都可以通过 NULL 指针进行读写,破坏栈框架并在内存甚至是其 它应用程序的内存区中留下许多的无用信息,而硬件并没有什么反应,听任用户为所欲为。之所以提到这一 点,是因为如果读者习惯于认为通过 NULL 指针进行写操作会引起硬件故障的话,那么可能会对本书中的某 些语句感到迷惑不解。遗憾的是,目前微机上的保护型操作系统仍不普及,破坏内存的隐患必须通过硬件保 护(通常它也不能提供充足的保护)之外的方法才能发现。 有错误就有错误 不必为本书的读者定义什么叫错误,相信读者都知道什么是错误。但是错误可以分为两类:一类是开发某一 功能时产生的错误,
31、另一类是在程序员认为该功能已经开发完成之后仍然遗留在代码中的错误。 例如在 Microsoft 中,每个产品都由一些绝不应该含有错误的原版源代码构成。当程序员给产品增加新功能 时,并不直接改变相应的原版源代码,改变的是其副本。只有在这些改变已经完成,并且程序员确信相应代 码中已经没有错误时,才将其合并到原版源代码中。因此从产品质量看,在实现指定功能过程中不论产生多 少个错误都没有关系,只要这些错误在相应代码被并入原版源代码之前被删除掉就行。 所有的错误都有害,但损害产品最危险的错误是已经进入原版源代码中的错误。因此,本书中提到的错误指 的就是这些已经进入原版源代码中的错误。作者并不指望程序员在
32、键入计算机之前总是写出没有错误的代码, 但确信防止错误侵入原版源代码是完全可能的。尤其是程序员在使用了本书提供的秘诀之后,更是如此。 8 9 第 1 章 假想的编译程序 读者可以考虑一下倘若编译程序能够正确地指出代码中的所有问题,那相应程序的错误情况会怎样?这不单 指语法错误,还包括程序中的任何问题,不管它有多么隐蔽。例如,假定程序中有“差 1”错误,编译程序 可以采用某种方法将其查出,并给出如下的错误信息 - line 23: while (i line 318: strCopy = memcpy(malloc(length), str, length); Invalid argument:
33、 memcpy fails when malloc returns NULL 好了,要求编译程序能够做到这一程度似乎有点过分。但如编译程序真能做到这些,可以想象编写无错程序 会变得多么容易。那简直是小事一桩,和当前程序员的一般作法真没法比。 假如在间谍卫星上用摄像机对准某个典型的软件车间就会看到程序员们正弓着身子趴在键盘上跟踪错误; 旁边,测试者正在对刚作出的内部版本发起攻击,轮番轰炸式地输入人量的数据以求找出新的错误。你还会 发现,测试员正在检查老版本的错误是否溜进了新版本。可以推想,这种查错方法比用上面的假想编译程序 进行查错要花费大得多的工作量、确实如此,而且它还要有点运气。 运气? 是
34、的,运气。测试者之所以能够发现错误,不正是因为他注意到了诸如某个数不对、某个功能没按所期望的 方式工作或者程序瘫痪这些现象吗?再看看上面的假想编译程序给出的上述错误:程序虽然有了“差 1”错 误,但如果它仍能工作,那么测试者能看得出来吗?就算看得出来,那么另外两个错误呢? 这听起来好象很可怕但测试人员就是这样做的大量给程序输入数据,希望潜在的错误能够亮相。 “噢,不! 我们测试人员的工作可不这么简单,我们还要使用代码覆盖工具、自动的测试集、随机的“猴”程序、抽点 打印或其他什么的” 。也许是这样,但还是让我们来看看这些工具究竟做了些什么吧!覆盖分析工具能够指 明程序中哪些部分未被测试到,测试人
35、员可以使用这一信息派生出新的测试用例。至于其它的工具无非都是 “输入数据、观察结果”这一策略的自动化。 请不要产生误解,我并不是说测试人员的所作所为都是错误的。我只是说利用黑箱方法所能做的只是往程序 里填数据,并看它弹出什么。这就好比确定一个人是不是疯子一样。问一些问题,得到回答后进行判断。但 这样还是不能确定此人是不是疯子。因为我们没法知道其头脑中在想些什么。你总会这样地问自己:“我问 的问题够吗?我问的问题对吗” 。 因此,不要光依赖黑箱测试方法。还应该试着去模仿前面所讲的假想编译程序,来排除运气对程序测试的影 响,自动地抓住错误的每个机会。 考虑一下所用的语言 你最后一次看推销字处理程序
36、的广告是什么时候?如果那个广告是麦迪逊大街那伙人写的,它很可能是这么 说:“无论是给孩子们的老师写便条还是为下期的Great American Novel撰稿,WordSmasher 都能行,毫 不费劲!WordSmasher 配备了令人吃惊的 233000 字的拼写字典,足足比同类产品多 51000 个字。它可以方 便地找出样稿中的打字错误。赶快到经销商那里去买一份拷贝。WordSmasher 是从圆珠笔问世以来最革命性 的书写工具!” 。 用户经过不断地市场宣传熏陶,差不多都相信拼写字典越大越好,但事实并非如此。象 em、abel 和 si 这些 词,在任何一本简装字典中都可以查到、但在
37、me、able 和 is 如此常见的情况下您还想让拼写检查程序认为 em、abel 和 si 也是拼写正确的词吗?如果是,那么当你看到我写的 suing 时,其本意很可能是与之风马牛不 相及的 using。问题不在于 suing 是不是一个真正的词而在于它在此处确实是个错误。 幸运的是,某些质量比较高的拼写检查程序允许用户删去象 em 这类容易引起麻烦的词。这样一来,拼写检 查程序就可以把原来合法的单词看成是拼写错误。好的编译程序也应该能够这样 可以把屡次出错的 合法的 C 习惯用法看成程序中的错误。例如,这类编译程序能够检查出以下 while 循环错放了一个分号: /* memcpy 复制一
38、个不重叠的内存块 */ 10 void* memcpy(void* pvTo, void* pvFrom, size_t size) byte* pbTo = (byte*)pvTo; byte* pbFrom = (byte*)pvFrom; while(size0); *pbTo+ = *pbFrom+; return(pvTo); 我们从程序的缩进情况就可以知道 while 表达式后由的分号肯定是个错误,但编译程序却认为这是一个完全 合法的 while 语句,其循环体为空语句。由于有时需要空语句,有时不需要空语句,所以为了查出不需要的 空语句,编译程序常常在遇到空语句时给出一条可选的警告
39、信息,自动警告你可能出了上面的错误。当确定 需要用空语句时,你就用。但最好用 NULL 使其明显可见。例如: char* strcpy(char* pchTo, char* pchFrom) char* pchStart = pchTo; while(*pchTo+ = *pchFrom+) NULL; Return(pchStart); 由于 NULL 是个合法的 C 表达式,所以这个程序没有间题。使用 NULL 的更大好处在于编译程序不会为 NULL 语句生成任何的代码,因为 NULL 只是个常量。这样,编译程序接受显式的 NULL 语句,但把隐式 空语句自动地当作错误标出。在程序中只允许
40、使用一种形式的空语句,如同为了保持文字的一致性,文中只 想使用 zero 的一种复数形式 zeroes,因此要从拼写字典中删除另一种复数形式 zeros。 另一个常见的问题是无意的赋值。C 是一个非常灵活的语言,它允许在任何可以使用表达式的地方使用赋值 语句。因此如果用户不够谨慎,这种多余的灵活性就会使你犯错误。例如,以下程序就出现了这种常见的错 误: if(ch = t) ExpandTab(); 虽然很清楚该程序是要将 ch 与水平制表符作比较,但实际上却成了对 ch 的赋值。对于这种程序,编译程序 当然不会产生错误,因为代码是合法的 C。 某些编译程序允许用户在 编译程序会产生警告信息一
41、所以要写成; while( (*pchTo+ = *pchFrom+)!= 0 ) NULL; 这样做有两个好处。第一,现代的商用级编译程序不会为这种冗余的比较产生额外的代码,可以将其优化掉。 因此,提供这种警告选择项的编译程序是可以信赖的。第二,它可以少冒风险,尽管两种都合法,但这是更 安全的用法。 另一类错误可以被归入“参数错误”之列。例如,多年以前,当我正在学 C 语言时,曾经这样调用过 fputc: fprintf(stderr, “Unable to open file %s. n”, filename); fputc(stderr, n); 这一程序看起来好象没有问题,但 fput
42、c 的参数次序错了。不知道为什么,我一直认为流指针(stderr)总是 11 这类流函数的第一个参数。事实并非如此,所以我常常给这些函数传递过去许多没用的信息。幸好 ANSI C 提供了函数原型,能在编译时自动地查出这些错误。 由于 ANSI C 标准要求每个库函数都必须有原型所以在 stdio.h 头文件中能够找到 fputc 的原型。fputc 的原型 是: int fputc(int c, FILE* stream); 如果在程序中 include 了 stdio.h,那么在调用 fputc 时,编译程序会根据其原型对所传递的每个参数进行比较。 如果二者类型不同,就会产生编译错误。在上面
43、的错误例于中,因为在 int 的位置上传递了 FILE* 类型的参 数,所以利用原型可以自动地发现前一个 fputc 的错误。 ANSI C 虽然要求标准的库函数必须有原型,但并不要求用户编写的函数也必须有原型。严格地说,它们可 以有原型,也可以没有原型。如果用户想要检查出自己程序中的调用错误,必须自己建立原型,并随时使其 与相应的函数保持一致。 最近我听到程序员在抱怨他们必须对函数的原型进行维护。尤其是刚从传统 C 项目转到 ANSI C 项目时,这 种抱怨更多。这种抱怨是有一定理由的,但如果不用原型,就不得不依赖传统的测试方法来查出程序中的调 用错误。你可以扪心自问,究竟哪个更重要,是减少
44、一些维护工作量,还是在编译时能够查出错误?如果你 还不满意,请再考虑一下利用原型可以生成质量更好的代码这一事实。这是因为:ANSI C 标准使得编译程 序可以根据原型信息进行相应的优化。 在传统 C 中,对于不在当前正被编译的文件中的函数,编译程序基本上得不到关于它的信息。尽管如此,编 译程序仍然必须生成对这些函数的调用,而且所生成的调用必须奏效。 编译程序实现者解决 这个问题的办法是使用标准的调用约定。这一方法虽然奏效,但常常意味着编译程序必须生成额外的代码, 以满足调用约定的要求。但如果使用了“要求所有函数 都必须有原型”这一编译程序提供的 警告选择项,由于编译程序了解程序中每个函数的参数
45、情况,所以可以为不同的函数选择它认为最有效率的 调用约定。 空语句、错误的赋值以及原型检查只是许多 C 编译程序提供的选择项中的一小部分内容,实际上常常还有更 多的其它选择项。这里的要点是:用户可以选择的编译程序警告设施可以就可能的错误向用户发出警告信息, 其工作的方式非常类似于拼写检查程序对可能的拼写错误的处理 Peter Lynch,据说是 80 年代最好的合股投资公司管理者,他曾经说过:投资者与赌徒之间的区别在于投资 者利用每一次机会,无论它是多么小,去争取利益;而赌徒则只靠运气。用户应该将这一概念同样应用于编 程活动,选择编译程序的所有可选警告设施,并把这些措施看成是一种无风险高偿还的
46、程序投资。再不要问: “应该使用这一警告设施吗?而应该问:“为什么不使用这一警告设施呢?” 。要把所有的警告开关都打开, 除非有极好的理由才不这样做。 增强原型的能力 不幸的是,如果函数有两个参数的类型相同,那么即使在调用该函数时互换了这两个参数的位置,原型也查 不出这一调用错误。例如,如果函数 memchr 的原型是: void* memchr(const void* pv, int ch, int size); 那么在调用该函数时,即使互换其字符 ch 和大小 size 参数,编译程序也不会发出警告信息。但是如果在相 应界面和原型中使用了更加精确的类型,就可以增强原型提供的错误检查能力。例
47、如,如果有了下面的原型: void* memchr(const void* pv, unsigned char ch, size_t size); 那么在调用该函数时弄反了其字符 ch 和大小 size 参数,编译程序就会给出警告错误。 在原型中使用更精确类型的缺陷是常常必须进行参数的显式类型转换,以消除类型不匹配的错误,即使参数 的次序正确。 lint 并不那么差 另一种检查错误更详细、更彻底的方法是使用 lint,这种方法几乎不费什么事。最初,lint 这个工具用来 扫描 C 源文件并对源程序中不可移植的代码提出警告。但是现在大多数 lint 实用程序已经变得更加严密,它 不但可以检查出可
48、移植性问题,而且可以检查出那些虽然可移植并且完全合乎语法但却很可能是错误的特性, 使用编译程序所有的可选警告设施 12 上一节那些可疑的错误就属于这一类。 不幸的是,许多程序员至今仍然把 lint 看作是一个可移植性的检查程序,认为它只能给出一大堆无关的警告 信息。总之,lint 得到了不值得麻烦的名声。如果你也是这样想的程序员,那么你也许应该重新考虑你的见 解。想一想究竟是哪一种工具更加接近于前文所述的假想编译程序,是你正使用的编译程序,还是 lint? 实际上,一旦源程序变成了没有 lint 错误的形式,继续使其保持这种状态是很容易做到的。只要对所改变的 部分运行 lint,没有错误之后再把其并入到原版源代码中即可。利用这种方法,并不要进行太多的考虑,只 要经过一、二周就可以写出没有 lint 错误的代码。在
Copyright © 2018-2021 Wenke99.com All rights reserved
工信部备案号:浙ICP备20026746号-2
公安局备案号:浙公网安备33038302330469号
本站为C2C交文档易平台,即用户上传的文档直接卖给下载用户,本站只是网络服务中间平台,所有原创文档下载所得归上传人所有,若您发现上传作品侵犯了您的权利,请立刻联系网站客服并提供证据,平台将在3个工作日内予以改正。