《Writingcleancode》读书笔记hopesophite2006-8-1写在前面的话:这两天看了《WritingCleanCode》,很受启发,感觉值得再读,于是整理了一点笔记,作为checklist,以备速查。原书共8章,每章都举一些例子,指出不足,再用通用的规则改写,每章结束时会总结一下要点,其中覆盖了比较重要的规则。附录A是作者整理的编码检查表。本笔记前8章和原书前8章对应,列出了所有的规则,对比较特别或者比较难理解的规则还附上了书中的例子,偶尔加一两句个人的想法。第9章是原书各章末尾要点的汇总。第10章是原书的编码检查表。本笔记只作为原书的一个速查手册,详细的内容请看原书。中译本:《编程精粹───Microsoft编写优质无错C程序秘诀》SteveMaguire著,姜静波佟金荣译,麦中凡校,电子工业出版社英文版:《WritingCleanCode──MicrosoftTechniquesforDevelopingBug-freeCPrograms》Stevemaguire,MicrosoftPress英文版原名:《WritingSolidCode──MicrosoftTechniquesforDevelopingBug-freeCPrograms》Stevemaguire,MicrosoftPress1假想的编译程序1.1使用编译程序所有的可选警告设施1.2使用lint来查出编译程序漏掉的错误1.3如果有单元测试,就进行单元测试1.4TipsC的预处理程序也可能引起某些意想不到的结果。例如,宏UINT_MAX定义在limit.h中,但假如在程序中忘了include这个头文件,下面的伪指令就会无声无息地失败,因为预处理程序会把预定义的UINT_MAX替换成0:#ifUINT_MAX65535u…#endif怎样使预处理程序报告出这一错误?2构造自己的断言2.1既要维护程序的交付版本,又要维护程序的调试版本少用预处理程序,那样会喧宾夺主,尝试用断言2.2断言是进行调试检查的简单方法。要使用断言捕捉不应该发生的非法情况。不要混淆非法情况与错误情况之间的区别,后者是在最终产品中必须处理的。这是断言和错误处理的区别2.3要使用断言对函数参数进行确认2.4要从程序中删去无定义的特性或者在程序中使用断言来检查出无定义特性的非法使用这个对C/C++很适用2.5不要浪费别人的时间───详细说明不清楚的断言森林中只标有“危险”,而没指出具体是什么危险的指示牌将会被忽略。2.6断言不是用来检查错误的当程序员刚开始使用断言时,有时会错误地利用断言去检查真正地错误,而不去检查非法的况。看看在下面的函数strdup中的两个断言:char*strdup(char*str){char*strNew;ASSERT(str!=NULL);strNew=(char*)malloc(strlen(str)+1);ASSERT(strNew!=NULL);strcpy(strNew,str);return(strNew);}第一个断言的用法是正确的,因为它被用来检查在该程序正常工作时绝不应该发生的非法情况。第二个断言的用法相当不同,它所测试的是错误情况,是在其最终产品中肯定会出现并且必须对其进行处理的错误情况。2.7用断言消除所做的隐式假定,或者利用断言检查其正确性Eg.对于和机器相关的内存填充程序,不必也无法将其写成可移植的。可以用条件编译。但其中应该对某种机器的隐含假设做检查。2.8利用断言来检查不可能发生的情况压缩程序的例子:正常情况和特殊情况,重复次数=4或者就等于12.9在进行防错性程序设计时,不要隐瞒错误2.10要利用不同的算法对程序的结果进行确认2.11不要等待错误发生,要使用初始检查程序2.12Tips不要把真正需要执行的语句放在断言里3为子系统设防3.1要消除随机特性───使错误可再现3.2冲掉无用的信息,以免被错误地使用分配内存时填充上非法值:eg.68000用0xA3,IntelX86系列用0xCC释放内存时立刻填上非法值引申:这个和《代码大全》中讲的进攻式编程观点类似3.3如果某件事甚少发生的话,设法使其经常发生eg.让realloc函数中移动内存块这种比较少发生的事情经常发生--自己包装一个relloc。3.4保存调试信息到日志,以便进行更强的错误检查这里的日志信息相当于一个簿记功能的信息,写到内存链表中。p168代码有错:if(pbiPrev==NULL)pbiHead=pbi-pbiHead;3.5建立详尽的子系统检查并且经常地进行这些检查--调试检查eg。利用簿记和‘是否被引用’的标志,检查是否有内存泄漏和悬挂指针3.6仔细设计程序的测试代码,任何选择都应该经过考虑eg.先后顺序是有讲究的:先看500元的套装,再看80元的毛衣3.7努力做到透明的一致性检查不要影响代码的使用者的使用方式3.8不要把对交付版本的约束应用到相应的调试版本上要用大小和速度来换取错误检查能力3.9每一个设计都要考虑如何确认正确性如果可能的话,把测试代码放到所编写的子系统中,而不要把它放到所编写子系统的外层。不要等到进行了系统编码时,才考虑其确认方法。在子系统设计的每一步,都要考虑“如何对这一实现进行详尽的确认”这一问题。引申:回忆高中时检查结果:如果是解方程,则代入数值验算就可;如果是计算题,换一个方法再算一遍。总之,要有方法确认其正确性。3.10“调试代码时附加了额外信息的代码,而不是不同的代码”加调试代码时要保证产品代码一定也要运行,这样才能测试到真正的产品代码。3.11在自己包装的内存函数中加上允许注入错误的机制。eg.定义一个failure结构,在NewMemory中测试这个结构,如果为真,则返回false,表示内存分配失败。这样,开发者和测试者都能利用这个机制,人为的注入错误。4对程序进行逐条跟踪4.1代码中不会自己生出错误来,错误是程序员编写新代码或者修改现有代码的产物。如果你想发现代码中的错误,没有哪个办法比在对代码进行编译时对其进行逐条跟踪更好。这个如果用个“完美的”编译器就更好。4.2不要等到出了错误再对程序进行逐条的跟踪而是把对程序逐条跟踪看成是一个必要的过程。这可以保证程序按你预想的方式工作。引申:可以和代码走查结合在一起。或者先进行代码走查,再逐条跟踪,共两遍检查代码。4.3对每一条代码路径进行逐条的跟踪注意覆盖率问题:语句覆盖or分支覆盖4.4当对代码进行逐条跟踪时,要密切注视数据流这样有助于发现以下错误:上溢和下溢错误;数据转换错误;差1错误;NULL指针错误;使用废料内存单元错误(0xA3类错误);用=代替==的赋值错误;运算优先级错误;逻辑错误。4.5源级调试程序可能会隐瞒执行的细节,对关键部分的代码要进行汇编指令级的逐条跟综对条件语句的各个子条件,不要一次越过,而要看每个子条件的值。5糖果机界面作者以糖果机的糟糕的界面设计导致人犯错讲起,阐述界面设计应该指导程序员少犯错误。5.1要使用户不容易忽视错误情况,不要在正常地返回值中隐藏错误代码作者以getchar函数为例:这个函数返回一个char或者是-1,由此要求使用getchar的程序员必须用int来接收getchar的返回值,但肯定会有很多程序员忘记这一点,由此可能会引发难以捕捉的错误。作者设计了另一个函数界面来处理这种情况:intfGetChar(char*),返回值存入char*所指位置,而int返回flag,为true表示正确。这样,由于划分了正常的返回值和错误代码,避免了getchar的返回值要用int接收的问题。5.2要不遗余力地寻找并消除函数界面中的缺陷Eg.下述代码隐含着一个错误pbBuf=(byte*)realloc(pbBuf,sizeNew);if(pbBuf!=NULL)使用初始化这个更大的缓冲区如果realloc分配内存时失败,返回NULL,则pbBuf为NULL,它原来指向的内存将会丢失。如果界面是flagfResizeMemory(void**ppv,size_tsizeNew)则好得多5.3不要编写多种功能集于一身的函数,为了对参数进行更强的确认,要编写功能单一的函数以realloc为例,它接受的指针为NULL但size大于0时相当于malloc,指针不为NULL但size为0时相当于free。这样realloc就混杂了malloc和free的功能,极其容易出错。5.4不要模棱两可,要明确地定义函数的参数像realloc那样灵活的参数不一定很好,要考虑程序员给出这样的输入参数可能是出于什么原因,如果没有充分的理由,用断言来禁止太灵活的输入能减少错误。5.5返回值与错误处理:编写函数使其在给定有效的输入情况下不会失败返回错误码不是唯一的处理错误的方式。Eg.Tolower函数在遇到输入是小写字母时,应该怎么办?如果返回-1,那么将遇到和getchar相同的问题:程序员要用int来存储tolower的返回值。此时,tolower返回原字符也许是一个更好的方式。5.6使程序在调用点明了易懂:要避免布尔参数通过检查调用代码,检验界面设计的合理性。Eg.以下两个函数声明会导致调用方式的不同:voidUnsignedToStr(unsignedu,char*strResult,flagfDecimal);voidUnsignedToStr(unsignedu,char*str,unsignedbase);前者的调用方式是:UnsignedToStr(u,str,TRUE);UnsignedToStr(u,str,FALSE);这显然不好。而后者是UnsignedToStr(u,str,BASE10)则好的多。5.7编写注解突出可能的异常情况用注释写出常见的错误用法和正确用法的例子。5.8小结本章先给出一个界面不好的例子,再给出一般原则:要不遗余力的检查界面的合理性。然后讲功能要单一,输入要有限制,输出的正常返回值要与错误码分开,用调用方式检查界面,用注释来指出异常情况。6风险事业6.1使用有严格定义的数据类型可移植类型最值得注意之处是:它们只考虑了三种最通用的数制:壹的补码、贰的补码和有符号的数值。Char只有0~127吗是可移植的Unsignedchar是0~255,但signedchar是-127~127(没有-128吗)是可移植的6.2经常反问:“这个变量表达式会上溢或下溢吗?”Eg.以下代码会导致无穷循环,因为ch会上溢为0,导致不可能大于UCHAR_MAX。unsignedcharch;/*首先将每个字符置为它自己*/for(ch=0;ch=UCHAR_MAX;ch++)chToLower[ch]=ch;eg.以下代码会下溢,导致无穷循环,因为size_t是无符号型,不可能小于0size_tsize=100;while(--size=0)NULL;6.3尽可能精确地实现设计,近似地实现设计就可能出错6.4一个“任务”应只实现一次(Implementthetaskjustonce).一个原则:Strivetomakeeveryfunctionperformitstaskexactlyonetimestaticwindow*pwndRootChildren=NULL;voidAddChild(window*pwndParent,window*pwndNewBorn){/*新窗口可能只有子窗口⋯*/ASSERT(pwndNewBorn-pwndSibling==NULL);if(pwndParent==NULL){/*将窗口加入到顶层根列表*/pwndNewBorn-pwndSibling=pwndRootChildren;pwndRootChildren=pwndNewBorn;}else{/*如果是父母的第一个孩子,那么开始一个链,*否则加到现存兄弟链的末尾处*/if(pwndParent-pwndChild==NULL)pwndParent-pwndChild=pwndNewBorn;else{w