下载

2下载券

加入VIP
  • 专属下载特权
  • 现金文档折扣购买
  • VIP免费专区
  • 千万文档免费下载

上传资料

关闭

关闭

关闭

封号提示

内容

首页 编程精粹Microsoft编写优质无错C程序秘诀

编程精粹Microsoft编写优质无错C程序秘诀

编程精粹Microsoft编写优质无错C程序秘诀

king
2009-03-15 0人阅读 举报 0 0 暂无简介

简介:本文档为《编程精粹Microsoft编写优质无错C程序秘诀doc》,可适用于IT/计算机领域

编程精粹───Microsoft编写优质无错C程序秘诀WritingCleanCode───MicrosoftTechniquesforDevelopingBugfreeCProgramsSteveMaguire著姜静波佟金荣译麦中凡校电子工业出版社这份电子书籍由PCHome俱乐部、CBulider讨论区数位网友分别整理完成基本上完全按照所据译本原貌极少数文字为通顺起见稍作修改。由于并非一人整理完成书中例程各章节代码书写风格可能稍有不同如指针声明以下两种写法都存在:void*pv‘*’号与类型说明符相连void*pv‘*’号与变量名相连抱歉为阅读带来了麻烦。如果各位在阅读这份电子书籍时发现错误请Email至wizardcitiznet我们会尽快加以修正。原文书名:《WritingCleanCode──MicrosoftTechniquesforDevelopingBugfreeCPrograms》Stevemaguire著MicrosoftPress出版所据译本:《编程精粹──Microsoft编写优质无错C程序秘诀》姜静波、佟金荣译麦中凡校电子工业出版社出版整理:Solmyr:序、某些背景、命名约定、引言、第、、、章、后记、参考文献iliad:第、章lavos:第章、附录Awarz:第章chief:附录B、C校对、格式编排:Solmyr目录序命名约定某些背景引言第章假想的编译程序第章自己设计并使用断言第章为子系统设防第章对程序进行逐条跟踪第章糖果机界面第章风险事业第章编码中的假象第章剩下来的就是态度问题附录A编码检查表附录B内存登录例程附录C练习答案后记走向何方序………………………………………………………………………………Ⅰ某些背景………………………………………………………………………Ⅲ命名约定………………………………………………………………………Ⅳ引言…………………………………………………………………………Ⅵ第章假想的编译程序……………………………………………………第章自己设计并使用断言………………………………………………第章为子系统设防………………………………………………………第章对程序进行逐条跟踪………………………………………………第章糖果机界面…………………………………………………………第章风险事业……………………………………………………………第章编码中的假象………………………………………………………第章剩下的就是态度问题………………………………………………后记走向何方……………………………………………………………附录A编码检查表……………………………………………………………附录B内存登录例程…………………………………………………………附录C练习答案………………………………………………………………参考文献………………………………………………………………………(注:上述页码是以原书为基准与本电子版本没有什么关系)献给我的妻子Beth以及我的双亲Joseph和JuliaMaguire────为了他们的爱和支持序年在为几家小公司咨询和工作了年之后为了获得编写Macintosh应用程序的经验我特意到Microsoft公司工作参加了Macintosh开发小组。这个小组负责Microsoft的图形电子表格应用程序的开发。当时我还不能肯定想象的代码是什么样子的我想也许应该既引入入胜又雅致吧!但我看到的代码却很平常与我以往见到的其它代码没有什么不同。要知道Excel有一个相当漂亮的用户界面───它比当时其它基于字符的电子表格软件更容易使用更加直观。但使我感受更深的是产品中包含的一个多功能调试系统。该系统旨在自动地问程序员和测试者进行错误报警。其工作方式非常象波音驾驶仓内向驾驶员报告故障的报警灯。该调试系统主要用来对代码进行监视它并不过多地对代码进行测试。虽然现在该调试系统采用的概念已不再新鲜了但当时它们的广泛使用程度以及该系统有效的查错能力还是吸引了我使我深受启发。没过多久我就发现Microsoft的大部分项目都有多功能的内部调试系统而Microsoft的程序员都高度重视代码中的错误及其产生原因。在做了两年MacintoshExcel之后我离开了该开发小组去帮助另一个代码错误数目超常的小组。在开发Excel的两年中我发现Microsoft虽然壮大了两倍但许多老项目组熟知的概念并没有随着公司的壮大而传到新项目组。新程序员不象我加入Microsoft之前时的老程序员一样对容易引起错误的编码习惯特别重视而只有一般的注意。在我转到新项目组六个月之后有一次我对一个程序员伙伴提到:“应该把编写无错代码的某些概念写成文字使那些原理能在新项目组传开”。这时另一位程序员对我说:“你不要总是想着写文档为什么你不把这一切都写下来?为什么你不写本书问问Microsoft出版社是否愿意出版呢?毕竟这些信息不是谁的专利其作用不过是为了使程序员更加重视错误。”当时我对这个建议并没有多想主要原因是没有时间而且以前我也没有写过书。以前我所做过与写书最有关系的事情不过是在年代初协助别人主办HiRes杂志的程序设计专栏这与写书毕竟不是一回事。正如您所见到的这本书还是写出来了。理由很简单:年由于错误越来越多Microsoft取消了一个尚未公布的产品。现在错误越来越多已经不是什么新鲜事情Microsoft的几个竞争者都因此取消过一些项目。但Microsoft因为这种原因取消项目还是头一次。最近随着接连不断地出现产品错误。管理人员终于开始叫嚷“受不了啦”并采取了一系列的措施企图将错误率下降到原先的水平。尽管如此仍然没有人将这些错误因由记录下来。现在Microsoft已经比我刚进公司时大了九倍。很难设想倘若没有准确的指南公司怎样才能将出错率降低到原来的水平。尤其是在Windows和Macintosh的应用越来越复杂的情况下更是如此。以上就是我最终决定写这本书的原因Microsoft出版社已经同意出版这本书。情况就是这样。我希望您会喜欢这本书我力图使本书不那么枯燥并尽量有趣。SteveMaguire西雅图华盛顿致谢我要感谢Microsoft出版社中帮助本书问世的所有人尤其是在我写作过程中始终手把手地教我的两个人。首先我要感谢我的约稿编辑MikeHalvorson他使我能够按照自己的进度完成了这本书并且耐心地解答了我这个新作者提出的许多问题。我还要特别感谢我的责任编辑ErinO’Connor女士她用了许多额外时间及早地对我写出的章节提出了反馈意见。没有他们的帮助就不会有这本书。Erin还鼓励我以幽默的风格写这本书。她对文中的小俏皮话发笑当然不会使我不快。我还要感谢我的父亲JosephMaguire是他在年代中期把我引入早期的微机世界:Altair、IMSAI和Sol使我与这一行结缘。我要感谢我于~年在ValparInternational工作时的伙伴EvanRosen他是对我职业生涯最有影响的几个人之一他的知识以及洞察力在本书中都有所体现。还有PaulDavis在过去的年里我与他在全国各地的许多项目中都有过愉快的合作他也无疑的塑造了我的思维方式。最后感谢花时间阅读本书草稿并提供技术反馈意见的所有人。他们是:MarkGerber、MelissaGlerum、ChrisMason、DaveMoore、JohnRaeGrant和AlexTilles。特别感谢EricSchlegel和PaulDavis他们不仅是本书草稿的评审者而且在本书内容的构思上对我很有帮助。命名约定本书采用的命名约定和Microsoft所使用的“匈牙利式”命名约定差不多。该约定是由生于匈牙利布达佩斯的CharlesSimonyi开发的它通过在数据和函数名中加入额外的信息以增进程序员对程序的理解。例如:charch*所有的字符变量均以ch开始*byteb*所有的字节均冠以b*longl*所有的长字均冠以l*对于指向某个数据类型的指针可以先象上面那样建立一个有类型的名字然后给该名字加上前缀字母P:char*pch*指向ch的指针以p开始*byte*pb*同理*long*plvoid*pv*特意显用的空指针*char**ppch*指向字符指针的指针*byte**ppb*指向字节指针的指针*匈牙利式名字通常不那么好念但在代码中读到它们时确实可以从中得到许多的信息。例如当你眼看到某个函数里有一个名为pch的变量时不用查看声明就立即知道它是一个指向字符的指针。为了使匈牙利式名字的描述性更强.或者要区分两个变量名可以在相应类型派生出的基本名字之后加上一个以大写字母开头的“标签”。例如strcpy函数有两个字符指针参数:一个是源指针另一个是目的指针。使用匈牙利式命名约定其相应的原型是:char*strcpy(char*pchTo,char*pchFrom)*原型*在上面的例子中两个字符指针有一个共同的特点──都指向以为结尾的C的字符串。因此在本书中每当用字符指针指向字符串时我们就用一个更有意义的名子str来表示。因此上述strcpy的原型则为:char*strcpy(char*strTo,char*strFrom)*原型*本书用到另一个类型是ANSI标准中的类型sizet。下面给出该类型的一些典型用法:sizetsizeNew,sizeOld*原型*void*malloc(sizetsize)*原型*void*realloc(void*pv,sizetsizeNew)*原型*函数和数组的命名遵循同样的约定名字由相应的返回类型名开始后跟一个描述的标签。例如:ch=chLastKeyPressed*由变量得一字符*ch=chInputBuffer*由数组得一字符*ch=chReadKeyboard*由函数得一字符*如果利用匈牙利式命名方法mall~和reali~可以写成如下形式:void*pvNewBlock(sizetsize)*原型*void*pvResizeBlock(void*pv,sizetsizeNew)*原型*由于匈牙利式命名方法旨在增进程序员对程序的理解所以大多数匈牙利式名字的长度都要超过ANSI严格规定个字母的限制。这就不妙除非所用的系统是几十年前设计的系统否则这个字母的限制只当是历史的遗迹。以上内容基本上没有涉及到匈牙利式命名约定的细节所介绍的都是读者理解本书中所用变量和函数名称意义的必需内容。如果读者对匈牙利式命名约定的详细内容感兴趣可以参考本书末尾参考文献部分列出的Simonyi的博士论文。某些背景本书用到了一些读者可能不太熟悉的软件和硬件系统的名称。下面对其中最常见的几个系统给出简单的描述MacintoshMacintosh是Apple公司的图形窗口计算机公布于年。它是最先支持“所见即所得”拥户界面的流行最广的计算机。WindowsWindows是Microsoft公司的图形窗口操作系统。Microsoft公司年公布了Windows该版本明显好于其早期版本。ExcelExcel是Microsoft公司的图形电子表格软件年首次在Macintosh上公布随后在进行了大量的重写和排错工作后被移植到Windows上。多年来MacintoshExcel和WindowsExcel共用一个名字但程序所用的代码并不相同。在本书中找多次提到曾经当过MacintoshExcel程序员这一经历。但应该说明的是我的大部分工作是将WindowsExcel的代码移到MacintoshExcel上或者是实现与WindowsExcel相似的特征。我与该产品现在的惊人成功并没有特别的关系。我为MacintoshExcel所做的唯一真正重要的贡献是说服Microsft放弃掉MacintoshExcel而直接利用WindowsExcel的源代码构造Macintosh的产品。Macintosh版是第一个基于WindowsExcel的版本它享用了WindowsExcel的源代码。这对MacintoshExcel的用户意义重大因为用了版以后他们会感至MacintoshExcel的功能和质量都有了一个很大的飞跃。WordWord是Microsoft公司的字处理应用软件。实际上Word有三种版本:基于字符并在MS-DOS上运行的DOS版Macintosh版Windows版。到目前为止这三种版本的产品虽然是用不同的源代码做出的但它们非常相象用户改用任何一个都不会有什么困难。由于Excel利用共享代码获得了成功Microsoft已经决定Word的高版本也将采用共享代码进行开发。xx是MSDOS和Windows机器常用的IntelCPU系列。xx是各种Macintosh所用的MotorolaCPU系列。引言几年前在一次偶然翻阅DonaldKnuth所著《TEX:TheProgram》一书时序言中的一段话深深触动了我:我确信TEX的最后一个错误已经在年月日被发现并排除掉了。但是如果出于目前尚不知道的原因TEX仍然潜伏有错误我非常愿意付给第一个发现者$元。(这一金额已是以前的两倍。我打算在本年内再增加一倍。你看我是多么自信!)我对Knuth是否曾经付给某人$甚至$元不感兴趣这并不重要。重要的是他对他的程序所具有的那种自信。那么据你所知究竟有多少程序员会严肃地声称他们的程序完全没有错误?又有多少敢把这一声称印刷在书上并准备为错误的发现者付钱呢?如果程序员确信测试组已经发现了所有的错误那么他也许敢作这种声明。但这本身就是一个问题。每当代码被打包装送给程序经销商之前人们在胸前划着十字带着最好的愿望说:“希望测试已经发现了所有的错误”。这一情景我已见过多次了。由于现代的程序员已经放弃了对代码进行彻底测试的职责他们没法知道代码中是否有错。管理人员也不会公布测试情况只是说:“别操那个心测试人员会为你作好测试的”。更为微妙的是管理人员希望程序员自己进行代码的测试。同时他们希望测试员作得更彻底些因为毕竟这是他们的本职工作。正如你在本书中将会看到的那样编写无错代码的技术或许有几百种程序员能用但测试人员却无法使用因为这些技术和代码的编写直接相关。两个关键的问题本书介绍的所有决窍是当发现错误时不断地就以下两个问题追问自己的结果:怎样才能自动地查出这个错误?怎样才能避免这个错误?第一个问题可能使读者认为本书是有关测试的书其实不是。当编辑程序发现语法错误时它是在做测试吗?不不是。编辑程序只是在自动地检查代码中的错误。语法错误只是程序员可以使用的自动查错方法查出的一种最基本的错误类型。本书将详尽介绍自动向程序员提示错误的方法。编写无错代码的最好方法是把防上错误放在第一位。关于这个问题同样也有许多的技巧。某些技巧与常用的编码惯例有关但它们不是象“每个人都违背原则”或“没有人违背该原则”这样泛泛地考虑问题而是对相应的细节进行详细的讨论。要记住在任何时候跟在大多数人的后面常常是所能选择的最坏一条路。因此在成为别人的追随者之前一定要确定这样做确实有意义而且不要仅仅因为其它的什么人如此自己也如此。本书的最后一章讨论编写无错代码应持正确态度的重要性。如果没有正确的态度发现错误和防止错误就好比在冬季大开着窗户给房间加热虽然也能达到目的但要浪费大量的能量。本书除第章和第章以外都配有练习。但要注意这些练习并不是要测验读者对相应内容的理解。实际上更多的是作者想在该章的正文中阐述却没能放进去的要点。其它的练习为的是让读者思考与该章内容有关的一些问题打开思路琢磨一下以前未曾考虑过的概念。无论哪种情况都是想通过练习再补充一些新的技巧和信息因此值得一读。为使读者了解作者的意图本书在附录C中提供了所有练习的答案。大部分章节还给出了一些课题但这些课题没有答案因为这种课题通常是任务而不是问题。规则或者建议本书的编排类似于BrianKernighan和PJPlauger所写的程序设计经典著作《TheElementsofProgrammingSytle》。该书出于WilliamStrunkJr和EBWhite所写的重要经典著作《TheElementsofStyle》。这两本书采用同样的基本概念表达方法:给出一个例子指出该例子中的某些问题所在用一般的准则改进该例子。确实这是个程式而且是使读者感到舒服的程式因此本书同样采用了这一程式。作者力图使本书读起来是一种享受尽管它有着公式的性质。希望读者会觉得本书很有趣。本书还给出一些似乎不应违背的“一般准则”。我们的第一条准则是:由于准则是用来说明一般情况的所以本书一般并不指明准则的例外情况而把它留给读者。我相信当读者读到某个准则时肯定会怀疑道:“噢当……时不是这样的……”。如果某个人对你说:“不能闯红灯”虽然这是一条准则但你肯定能够举出一种特殊情况在这种情况下闯红灯倒是个正确的行动。这里关键是要记住准则只是在一般情况下才有意义因此只有理由十分充足时才可以违背准则。关于本书代码的说明本书的所有代码都是按ANSIC写的并且通过了MSDOS、MicrosoftWindows和AppleMacintosh上五个流行编译程序的测试:MicrosoftCCMicrosoft公司TurboCCBorland国际公司AztecManx软件系统公司MPWCApple计算机公司THINKCSymantec公司还有一个问题:如果读者想从本书中摘取代码用在自己的程序中那要小心。因为为了说明书中的论点许多例子都有错误。另外书中用到的函数虽然名字和功能都与ANSIC的标准库函数相同但已对相应的界面进行了一些小的修改。例如ANSI版memchr函数的界面是:void*memchr(constvoid*s,intc,sizetn)这里memchr的内部将整数c当作unsignedchar来处理。在本书的许多地方读者都会看到字符类型被显式地声明为unsignedchar而不是int:void*memchr(constvoid*pv,unsignedcharch,sizetsize)ANSI标准将所有的字符变元都声明为int是为了保证其库函数同样可以用于ANSI标准之前编写的非原型程序这时程序使用extern声明函数。由于在本书中只使用ANSIC所以不必考虑这些向下兼容的细节而可以用更加精确的类型声明以改进程序的清晰程度并用原型进行强类型检查(详见第章)。“提到Macintosh了吗?”出于某种原因一本书如果没有提到PDPHoneywell当然还有IBM就不会被认真对待。因此我在这里也提到了它们。仅此而已读者在本书中再也不会见到这些字眼、读者见到最多的是MSDOSMicrosoftWindows特别还有AppleMacintosh因为近年来我一直为这些系统编写代码。但是应该注意本书中的任何代码都不受这些特定的系统约束。它们都是用通用的C编写的应该能够工作于任何的ANSIC编译程序下。因此即使读者使用的系统本书没有提及也不必担心这些操作系统的细节会产生障碍。应该提到的是在大多数的微机系统中用户都可以通过指针进行读写破坏栈框架并在内存甚至是其它应用程序的内存区中留下许多的无用信息而硬件并没有什么反应听任用户为所欲为。之所以提到这一点是因为如果读者习惯于认为通过指针进行写操作会引起硬件故障的话那么可能会对本书中的某些语句感到迷惑不解。遗憾的是目前微机上的保护型操作系统仍不普及破坏内存的隐患必须通过硬件保护(通常它也不能提供充足的保护)之外的方法才能发现。有错误就有错误不必为本书的读者定义什么叫错误相信读者都知道什么是错误。但是错误可以分为两类:一类是开发某一功能时产生的错误另一类是在程序员认为该功能已经开发完成之后仍然遗留在代码中的错误。例如在Microsoft中每个产品都由一些绝不应该含有错误的原版源代码构成。当程序员给产品增加新功能时并不直接改变相应的原版源代码改变的是其副本。只有在这些改变已经完成并且程序员确信相应代码中已经没有错误时才将其合并到原版源代码中。因此从产品质量看在实现指定功能过程中不论产生多少个错误都没有关系只要这些错误在相应代码被并入原版源代码之前被删除掉就行。所有的错误都有害但损害产品最危险的错误是已经进入原版源代码中的错误。因此本书中提到的错误指的就是这些已经进入原版源代码中的错误。作者并不指望程序员在键入计算机之前总是写出没有错误的代码但确信防止错误侵入原版源代码是完全可能的。尤其是程序员在使用了本书提供的秘诀之后更是如此。第章假想的编译程序读者可以考虑一下倘若编译程序能够正确地指出代码中的所有问题那相应程序的错误情况会怎样?这不单指语法错误还包括程序中的任何问题不管它有多么隐蔽。例如假定程序中有“差”错误编译程序可以采用某种方法将其查出并给出如下的错误信息>line:while(i<=j)offbyoneerror:thisshouldbe'<'又如编译程序可以发现算法中有下面的错误:>line:intitoa(inti,char*str)algorithmerror:itoafailswheniis再如当出现了参数传递错误时编译程序可以给出如下的错误信息:>line:strCopy=memcpy(malloc(length),str,length)Invalidargument:memcpyfailswhenmallocreturns好了要求编译程序能够做到这一程度似乎有点过分。但如编译程序真能做到这些可以想象编写无错程序会变得多么容易。那简直是小事一桩和当前程序员的一般作法真没法比。假如在间谍卫星上用摄像机对准某个典型的软件车间.就会看到程序员们正弓着身子趴在键盘上跟踪错误旁边测试者正在对刚作出的内部版本发起攻击轮番轰炸式地输入人量的数据以求找出新的错误。你还会发现测试员正在检查老版本的错误是否溜进了新版本。可以推想这种查错方法比用上面的假想编译程序进行查错要花费大得多的工作量、确实如此而且它还要有点运气。运气?是的运气。测试者之所以能够发现错误不正是因为他注意到了诸如某个数不对、某个功能没按所期望的方式工作或者程序瘫痪这些现象吗?再看看上面的假想编译程序给出的上述错误:程序虽然有了“差”错误但如果它仍能工作那么测试者能看得出来吗?就算看得出来那么另外两个错误呢?这听起来好象很可怕但测试人员就是这样做的大量给程序输入数据希望潜在的错误能够亮相。“噢不!我们测试人员的工作可不这么简单我们还要使用代码覆盖工具、自动的测试集、随机的“猴”程序、抽点打印或其他什么的”。也许是这样但还是让我们来看看这些工具究竟做了些什么吧!覆盖分析工具能够指明程序中哪些部分未被测试到测试人员可以使用这一信息派生出新的测试用例。至于其它的工具无非都是“输入数据、观察结果”这一策略的自动化。请不要产生误解我并不是说测试人员的所作所为都是错误的。我只是说利用黑箱方法所能做的只是往程序里填数据并看它弹出什么。这就好比确定一个人是不是疯子一样。问一些问题得到回答后进行判断。但这样还是不能确定此人是不是疯子。因为我们没法知道其头脑中在想些什么。你总会这样地问自己:“我问的问题够吗?我问的问题对吗……”。因此不要光依赖黑箱测试方法。还应该试着去模仿前面所讲的假想编译程序来排除运气对程序测试的影响自动地抓住错误的每个机会。考虑一下所用的语言你最后一次看推销字处理程序的广告是什么时候?如果那个广告是麦迪逊大街那伙人写的它很可能是这么说:“无论是给孩子们的老师写便条还是为下期的《GreatAmericanNovel》撰稿WordSmasher都能行毫不费劲!WordSmasher配备了令人吃惊的字的拼写字典足足比同类产品多个字。它可以方便地找出样稿中的打字错误。赶快到经销商那里去买一份拷贝。WordSmasher是从圆珠笔问世以来最革命性的书写工具!”。用户经过不断地市场宣传熏陶差不多都相信拼写字典越大越好但事实并非如此。象em、abel和si这些词在任何一本简装字典中都可以查到、但在me、able和is如此常见的情况下您还想让拼写检查程序认为em、abel和si也是拼写正确的词吗?如果是那么当你看到我写的suing时其本意很可能是与之风马牛不相及的using。问题不在于suing是不是一个真正的词而在于它在此处确实是个错误。幸运的是某些质量比较高的拼写检查程序允许用户删去象em这类容易引起麻烦的词。这样一来拼写检查程序就可以把原来合法的单词看成是拼写错误。好的编译程序也应该能够这样───可以把屡次出错的合法的C习惯用法看成程序中的错误。例如这类编译程序能够检查出以下while循环错放了一个分号:*memcpy复制一个不重叠的内存块*void*memcpy(void*pvTo,void*pvFrom,sizetsize){byte*pbTo=(byte*)pvTobyte*pbFrom=(byte*)pvFromwhile(size>)*pbTo=*pbFromreturn(pvTo)}我们从程序的缩进情况就可以知道while表达式后由的分号肯定是个错误但编译程序却认为这是一个完全合法的while语句其循环体为空语句。由于有时需要空语句有时不需要空语句所以为了查出不需要的空语句编译程序常常在遇到空语句时给出一条可选的警告信息自动警告你可能出了上面的错误。当确定需要用空语句时你就用。但最好用使其明显可见。例如:char*strcpy(char*pchTo,char*pchFrom){char*pchStart=pchTowhile(*pchTo=*pchFrom)Return(pchStart)}由于是个合法的C表达式所以这个程序没有间题。使用的更大好处在于编译程序不会为语句生成任何的代码因为只是个常量。这样编译程序接受显式的语句但把隐式空语句自动地当作错误标出。在程序中只允许使用一种形式的空语句如同为了保持文字的一致性文中只想使用zero的一种复数形式zeroes因此要从拼写字典中删除另一种复数形式zeros。另一个常见的问题是无意的赋值。C是一个非常灵活的语言它允许在任何可以使用表达式的地方使用赋值语句。因此如果用户不够谨慎这种多余的灵活性就会使你犯错误。例如以下程序就出现了这种常见的错误:if(ch=‘t’)ExpandTab()虽然很清楚该程序是要将ch与水平制表符作比较但实际上却成了对ch的赋值。对于这种程序编译程序当然不会产生错误因为代码是合法的C。某些编译程序允许用户在和||表达式以及if、for和while构造的控制表达式中禁止使用简单赋值这样就可以帮助用户查出这种错误。这种做法的基本依据是用户极有可能在以上五种情况下将等号==偶然地健入为赋值号=。这种选择项并不妨碍用户作赋值但是为了避免产生警告信息用户必须再拿别的值如零或空字符与赋值结果做显式的比较。因此对于前面的strcpy例子若循环写成:while(*pchTo=*pchFrom)编译程序会产生警告信息一所以要写成while((*pchTo=*pchFrom)!=’’)这样做有两个好处。第一现代的商用级编译程序不会为这种冗余的比较产生额外的代码可以将其优化掉。因此提供这种警告选择项的编译程序是可以信赖的。第二它可以少冒风险尽管两种都合法但这是更安全的用法。另一类错误可以被归入“参数错误”之列。例如多年以前当我正在学C语言时曾经这样调用过fputc:fprintf(stderr,“Unabletoopenfilesn”,filename)……fputc(stderr,‘n’)这一程序看起来好象没有问题但fputc的参数次序错了。不知道为什么我一直认为流指针(stderr)总是这类流函数的第一个参数。事实并非如此所以我常常给这些函数传递过去许多没用的信息。幸好ANSIC提供了函数原型能在编译时自动地查出这些错误。由于ANSIC标准要求每个库函数都必须有原型所以在stdioh头文件中能够找到fputc的原型。fputc的原型是:intfputc(intc,FILE*stream)如果在程序中include了stdioh那么在调用fputc时编译程序会根据其原型对所传递的每个参数进行比较。如果二者类型不同就会产生编译错误。在上面的错误例于中因为在int的位置上传递了FILE*类型的参数所以利用原型可以自动地发现前一个fputc的错误。ANSIC虽然要求标准的库函数必须有原型但并不要求用户编写的函数也必须有原型。严格地说它们可以有原型也可以没有原型。如果用户想要检查出自己程序中的调用错误必须自己建立原型并随时使其与相应的函数保持一致。最近我听到程序员在抱怨他们必须对函数的原型进行维护。尤其是刚从传统C项目转到ANSIC项目时这种抱怨更多。这种抱怨是有一定理由的但如果不用原型就不得不依赖传统的测试方法来查出程序中的调用错误。你可以扪心自问究竟哪个更重要是减少一些维护工作量还是在编译时能够查出错误?如果你还不满意请再考虑一下利用原型可以生成质量更好的代码这一事实。这是因为:ANSIC标准使得编译程序可以根据原型信息进行相应的优化。在传统C中对于不在当前正被编译的文件中的函数编译程序基本上得不到关于它的信息。尽管如此编译程序仍然必须生成对这些函数的调用而且所生成的调用必须奏效。编译程序实现者解决这个问题的办法是使用标准的调用约定。这一方法虽然奏效但常常意味着编译程序必须生成额外的代码以满足调用约定的要求。但如果使用了“要求所有函数都必须有原型”这一编译程序提供的警告选择项由于编译程序了解程序中每个函数的参数情况所以可以为不同的函数选择它认为最有效率的调用约定。空语句、错误的赋值以及原型检查只是许多C编译程序提供的选择项中的一小部分内容实际上常常还有更多的其它选择项。这里的要点是:用户可以选择的编译程序警告设施可以就可能的错误向用户发出警告信息其工作的方式非常类似于拼写检查程序对可能的拼写错误的处理PeterLynch据说是年代最好的合股投资公司管理者他曾经说过:投资者与赌徒之间的区别在于投资者利用每一次机会无论它是多么小去争取利益而赌徒则只靠运气。用户应该将这一概念同样应用于编程活动选择编译程序的所有可选警告设施并把这些措施看成是一种无风险高偿还的程序投资。再不要问:“应该使用这一警告设施吗?而应该问:“为什么不使用这一警告设施呢?”。要把所有的警告开关都打开除非有极好的理由才不这样做。增强原型的能力不幸的是如果函数有两个参数的类型相同那么即使在调用该函数时互换了这两个参数的位置原型也查不出这一调用错误。例如如果函数memchr的原型是:void*memchr(constvoid*pv,intch,intsize)那么在调用该函数时即使互换其字符ch和大小size参数编译程序也不会发出警告信息。但是如果在相应界面和原型中使用了更加精确的类型就可以增强原型提供的错误检查能力。例如如果有了下面的原型:void*memchr(constvoid*pv,unsignedcharch,sizetsize)那么在调用该函数时弄反了其字符ch和大小size参数编译程序就会给出警告错误。在原型中使用更精确类型的缺陷是常常必须进行参数的显式类型转换以消除类型不匹配的错误即使参数的次序正确。lint并不那么差另一种检查错误更详细、更彻底的方法是使用lint这种方法几乎不费什么事。最初lint这个工具用来扫描C源文件并对源程序中不可移植的代码提出警告。但是现在大多数lint实用程序已经变得更加严密它不但可以检查出可移植性问题而且可以检查出那些虽然可移植并且完全合乎语法但却很可能是错误的特性上一节那些可疑的错误就属于这一类。不幸的是许多程序员至今仍然把lint看作是一个可移植性的检查程序认为它只能给出一大堆无关的警告信息。总之lint得到了不值得麻烦的名声。如果你也是这样想的程序员那么你也许应该重新考虑你的见解。想一想究竟是哪一种工具更加接近于前文所述的假想编译程序是你正使用的编译程序还是lint?实际上一旦源程序变成了没有lint错误的形式继续使其保持这种状态是很容易做到的。只要对所改变的部分运行lint没有错误之后再把其并入到原版源代码中即可。利用这种方法并不要进行太多的考虑只要经过一、二周就可以写出没有lint错误的代码。在达到这个程度时就可以得到lint带来的各种好处了。但我做的修改很平常一次在同本书的一个技术评审者共进午餐时他问我本书是否打算包括一节单元测试方面的内容。我回答说:“不”。因为尽管单元测试也与无错代码的编写有关但它实际上属于另一个不同的类别即如何为程序编写测试程序。他说:“不你误解了。我的意思是你是否打算指出在将新做的修改并入原版源代码之前程序员应该实际地进行相应的单元测试。我的小组中的一位程序员就是因为在进行了程序的修改之后没有进行相应的单元测试使一个错误进入到我们的原版源代码中。”这使我感到很惊奇。因为在Microsoft大多数项目负责人都要求程序员在合并修改了的源代码之前要进行相应的单元测试。“你没问他为什么不做单元测试吗?”我问道。我的朋友从餐桌上抬起头来对我说:“他说他并没有编写任何新的代码只是对现有代码进行了某些移动。他说他认为没必要再进行单元测试”。这种事情在我的小组中也曾经发生过。它使我想起曾经有一个程序员在进行了修改之后甚至没有再编译一次就把相应的代码并入了原版源代码中。当然我发现了这一问题因为我在对原版源代码进行编译时产生了错误。当我问这个程序员怎么会漏掉这个编译错误他说:“我做的修改很平常我认为不会出错”但他错了。这些错误本来都应该不会进入原版源代码中因为二者都可以几乎毫不费力地被查出来。为什么程序员会犯这种错误呢?是他们过高地估计了自己编写正确代码的能力。有时似乎可以跳过一些设计用来避免程序出错的步骤但走捷径之时就是麻烦将至之日。我怀疑会有许多的程序员甚至没有对相应的代码进行编译就“完成”了某一功能。我知道这只是偶然情况但绕过单元测试的趋势正在变强尤其是作简单的改动。如果你发现自己正打算绕过某个步骤。而它恰恰可以很容易地用来查错那么一定要阻止自己绕过。相反要利用所能得到的每个工具进行查错。此外单元测试虽然意味着查错但如果你根本就不进行单元测试也是枉然。小结你认识哪个程序员宁愿花费时间去跟踪排错而不是编写新的代码?我肯定有这种程序员但至今我还没有见过一个。对于我认识的程序员如果你答应他们再不用跟踪下一个错误他们会宁愿一辈子放弃享用中国菜。当你写程序时要在心中时刻牢记着假想编译程序这一概念这样就可以毫不费力或者只费很少的力气利用每个机会抓住错误。要考虑编译程序产生的错误、lint产生的错误以及单元测试失败的原因。虽然使用这些工具要涉及到很多的特殊技术但如果不花这么多的功夫那产品中会有多少个错误?如果想要快速容易地发现错误就要利用工具的相应特性对错误进行定位。错误定位得越早就能够越早地投身于更有兴趣的工作。要点:消除程序错误的最好方法是尽可能早、尽可能容易地发现错误要寻求费力最小的自动查错方法。努力减少程序员查错所需的技巧。可以选择的编译程序或lint警告设施并不要求程序员要有什么查错的技巧。在另一个极端高级的编码方法虽然可以查出或减少错误但它们也要求程序员要有较多的技巧因为程序员必须学习这些高级的编码方法。练习:假如使用了禁止在while的条件部分进行赋值的编译程序选择项为什么可以查出下面代码中的运算优先级错误?While(ch=getchar()!=EOF)……看看你怎样使用编译程序查出无意使用的空语句和赋值语句。值得推荐的办法是进行相应的选择使编译程序能够对下列常见问题产生警告信息。怎样才能消除这些警告信息呢?if(flight==)。这里程序员的本意是对号航班进行测试但因为前面多了一个使成了八进制数。结果变成对号航班进行测试。If(pb!=pb!=xff)。这里不小心把键入为结果即使pb等于还会执行*pb!=xff。quot=numer*pdenom。这里无意间多了个*号结果使*被解释为注释的开始。word=bHigh<<bLow。由于出现了运算优先级错误该语句被解释成了:word=bHigh<<(bLow)编译程序怎样才能对“没有与之配对的else”这一错误给出警告?用户怎样消除这一警告?再看一次下面的代码:if(ch==‘t’)ExpandTab()除禁止在if语句中使用简单赋值的方法之外能够查出这个错误的另一种众所周知的方法是把赋值号两边的操作数颠倒过来:if(‘t’==ch)ExpandTab()这样如果应该键入==时健入了=编译程序就会报错因为不允许对常量进行赋值。这个办法彻底吗?为什么它不象编译程序开关自动化程度那么高?为什么新程序员会用赋值号代替等号?C的预处理程序也可能引起某些意想不到的结果。例如宏UINTMAX定义在limith中但假如在程序中忘了include这个头文件下面的伪指令就会无声无息地失败因为预处理程序会把预定义的UINTMAX替换成:……#ifUINTMAX>u……#endif怎样使预处理程序报告出这一错误?课题:为了减轻维护原型的工作量某些编译程序会在编译时自动地为所编译的程序生成原型。如果你用的编译程序没有提供这一选择项自己写一个使用程序来完成这一工作。为什么标准的编码约定可以使这个使用程序的编写变得相对容易?课题:如果你用的编译程序还不支持本章(包括练习)中提及的警告设施那么促进相应的制造商支持这些设施另外要敦促他们除了允许用户设定或者取消对某些类错误的检查之外还要提供有选择地设定或取消一些特定的警告设施为什么要这样做呢?第章自己设计并使用断言利用编译程序自动查错固然好但我敢说只要你观察一下项目中那些比较明显的错误就会发现编译程序所查出的只是其中的一小部分。我还敢说如果排除掉了程序中的所有错误那么在大部分时间内程序都会正确工作。还记得第章中的下面代码吗?strCopy=memcpy(malloc(length),str,length)该语句在多数情况下都会工作得很好除非malloc的调用产生失败。当malloc失败时就会给memcpy返回一个指针。由于memcpy处理不了指针所以出现了错误。如果你很走运在交付之前这个错误导致程序的瘫痪从而暴露出来。但是如果你不走运没有及时地发现这个错误那某位顾客就一定会“走运”了。编译程序查不出这种或其他类似的错误。同样编译程序也查不出算法的错误无法验证程序员所作的假定。或者更一般地编译程序也查不出所传递的参数是否有效。寻找这种错误非常艰苦只有技术非常高的程序员或者测试者才能将它们根除并且不会引起其他的问题。然而假如你知道应该怎样去做的话自动寻找这种错误就变得很容易了。两个版本的故事让我们直接进入memcpy看看怎样才能查出上面的错误。最初的解决办法是使memcpy对指针进行检查如果指针为就给出一条错误信息并中止memcpy的执行。下面是这种解法对应的程序。*memcpy───拷贝不重叠的内存块*voidmemcpy(void*pvTo,void*pvFrom,sizetsize){void*pbTo=(byte*)pvTovoid*pbFrom=(byte*)pvFromif(pvTo==||pvFrom==){fprintf(stderr,“Badargsinmemcpyn”)abort()}while(size>)*pbTo==*pbFromreturn(pvTo)}只要调用时错用了指针这个函数就会查出来。所存在的唯一问题是其中的测试代码使整个函数的大小增加了一倍并且降低了该函数的执行速度。如果说这是“越治病越糟”确实有理因为它一点不实用。要解决这个问题需要利用C的预处理程序。如果保存两个版本怎么样?一个整洁快速用于程序的交付另一个臃肿缓慢件(因为包括了额外的检查)用于调试。这样就得同时维护同一程序的两个版本并利用C的预处理程序有条件地包含或不包含相应的检查部分。voidmemcpy(void*pvTo,void*pvFrom,sizetsize){void*pbTo=(byte*)pvTovoid*pbFrom=(byte*)pvFrom#ifdefDEBUGif(pvTo==||pvFrom==){fprintf(stderr,“Badargsinmemcpyn”)abort()}#endifwhile(size>)*pbTo==*pbFromreturn(pvTo)}这种想法是同时维护调试和非调试(即交付)两个版本。在程序的编写过程中编译其调试版本利用它提供的测试部分在增加程序功能时自动地查错。在程序编完之后编译其交付版本封装之后交给经销商。当然你不会傻到直到交付的最后一刻才想到要运行打算交付的程序但在整个的开发工程中都应该使用程序的调试版本。正如在这一章和下一章所建这样要求的主要原因是它可以显著地减少程序的开发时间。读者可以设想一下:如果程序中的每个函数都进行一些最低限度的错误检查并对一些绝不应该出现的条件进行测试的活相应的应用程序会有多么健壮。这种方法的关键是要保证调试代码不在最终产品中出现。利用断言进行补救说老实话memcpy中的调试码编得非常蹩脚且颇有点喧宾夺主的意味。因此尽管它能产生很好的结果多数程序员也不会容忍它的存在这就是聪明的程序员决定将所有的调试代码隐藏在断言assert中的缘故。assert是个宏它定义在头文件asserth中。assert虽然不过是对前面所见#ifdef部分代码的替换但利用这个宏原来的代码从行变成了行。voidmemcpy(void*pvTo,void*pvFrom,sizetsize){void*pbTo=(byte*)pvTovoid*pbFrom=(byte*)pvFromassert(pvTo!=pvFrom!=)while(size>)*pbTo==*pbFromreturn(pvTo)}aasert是个只有定义了DEBUG才起作用的宏如果其参数的计算结果为假就中止调用程序的执行。因此在上面的程序中任何一个指针为都会引发assert。assert并不是一个仓促拼凑起来的宏为了不在程序的交付版本和调试版本之间引起重要的差别需要对其进行仔细的定义。宏assert不应该弄乱内存不应该对未初始化的数据进行初始化即它不应该产主其他的副作用。正是因为要求程序的调试版本和交付版本行为完全相同所以才不把assert作为函数而把它作为宏。如果把assert作为函数的话其调用就会引起不期望的内存或代码的兑换。要记住使用assert的程序员是把它看成一个在任何系统状态下都可以安全使用的无害检测手段。读者还要意识到

用户评价(0)

关闭

新课改视野下建构高中语文教学实验成果报告(32KB)

抱歉,积分不足下载失败,请稍后再试!

提示

试读已结束,如需要继续阅读或者下载,敬请购买!

文档小程序码

使用微信“扫一扫”扫码寻找文档

1

打开微信

2

扫描小程序码

3

发布寻找信息

4

等待寻找结果

我知道了
评分:

/182

编程精粹Microsoft编写优质无错C程序秘诀

VIP

在线
客服

免费
邮箱

爱问共享资料服务号

扫描关注领取更多福利