Writing Clean Code读书笔记.docx
《Writing Clean Code读书笔记.docx》由会员分享,可在线阅读,更多相关《Writing Clean Code读书笔记.docx(24页珍藏版)》请在冰豆网上搜索。
WritingCleanCode读书笔记
【转载】《Writingcleancode》读书笔记
转载自:
写在前面的话:
这两天看了《WritingCleanCode》,很受启发,感觉值得再读,于是整理了一点笔记,作为checklist,以备速查。
原书共8章,每章都举一些例子,指出不足,再用通用的规则改写,每章结束时会总结一下要点,其中覆盖了比较重要的规则。
附录A是作者整理的编码检查表。
本笔记前8章和原书前8章对应,列出了所有的规则,对比较特别或者比较难理解的规则还附上了书中的例子,偶尔加一两句个人的想法。
第9章是原书各章末尾要点的汇总。
第10章是原书的编码检查表。
本笔记只作为原书的一个速查手册,详细的内容请看原书。
中译本:
《编程精粹───Microsoft编写优质无错C程序秘诀》SteveMaguire著,姜静波佟金荣译,麦中凡校,电子工业出版社
英文版:
《WritingCleanCode──MicrosoftTechniquesforDevelopingBug-freeCPrograms》Stevemaguire,MicrosoftPress
英文版原名:
《WritingSolidCode──MicrosoftTechniquesforDevelopingBug-freeCPrograms》Stevemaguire,MicrosoftPress
1假想的编译程序
1.1使用编译程序所有的可选警告设施
1.2使用lint来查出编译程序漏掉的错误
1.3如果有单元测试,就进行单元测试
1.4Tips
C的预处理程序也可能引起某些意想不到的结果。
例如,宏UINT_MAX定义在limit.h
中,但假如在程序中忘了include这个头文件,下面的伪指令就会无声无息地失败,
因为预处理程序会把预定义的UINT_MAX替换成0:
#ifUINT_MAX>65535u
…
#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或者就等于1
2.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是无符号型,不可能小于0
size_tsize=100;
while(--size>=0)
NULL;
6.3尽可能精确地实现设计,近似地实现设计就可能出错
6.4一个“任务”应只实现一次(Implement"thetask"justonce).
一个原则:
Strivetomakeeveryfunctionperformitstaskexactly
onetime
staticwindow*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
{
window*pwnd=pwndParent->pwndChild;
while(pwnd->pwndSibling!
=NULL)
pwnd=pwnd->pwndSibling;
pwnd->pwndSibling=pwndNewBorn;
}
}
}
.
假如AddChild是一个任务,要在现有窗口中增加子窗口,而上面的代码具有三个单独的插入过程。
常识告诉我们如果有三段代码而不是一段代码来完成一个任务,则很可能有错。
这往往意味着这个实现中有例外情况。
其最终的改进见下一节。
6.5避免无关紧要地if语句
以指针为中心的树的构建,可以不必为特殊情况编写代码:
voidAddChild(window*pwndParent,window*pwndNewBorn)
{
window**ppwindNext;
/*新窗口可能没有兄弟窗口?
*/
ASSERT(pwndNewBorn->pwndSibling==NULL);
/*使用以指针为中心的算法
*设置ppwndNext指向pwndParent->pwndChild
*因为pwndParent->pwndChild是链中第一个“下一个兄弟指针”
一个“任务”应只实现一次
*/
ppwndNext=&pwndParent->pwndChild;
while(*ppwndNext!
=NULL)
ppwndNext=&(*ppwndNext)->pwndSibling;
*ppwndNext=pwndNewBorn;
}
由于没有无关的if语句,使所有的程序都会经过同样的路径,因此这段代码就会被测试的很充分。
6.6避免使用嵌套的“?
:
“运算符
重新整理思路,甚至用查表法,都能简化过程。
6.7每种特殊情况只能处理一次
不要让处理同一个特殊情况的代码散布在多个地方
6.8避免使用有风险的语言惯用语
这里举了好几个例子。
Eg.pchEnd=pch+size;
while(pchNULL;
如果pchEnd恰好查找到存储器的结尾处,那么所指的位置就不存在了
Eg.除以2和移位:
移位有风险
Eg.while(--size>=0)和while(size-->0),前者有风险,后者却没有。
6.9不能毫无必要地将不同类型地操作符混合使用,如果必须将不同类型地操作符混合使用,就用括号把它们隔离开来
6.10避免调用返回错误的函数(Avoidcallingfunctionsthatreturnerrors)
这样,就不会错误地处理或漏掉由其它人设计的函数所返回的错误条件。
如果自始至终程序反复处理同样的错误条件,就将错误处理部分独立出来。
Eg.单独的错误处理子程序。
有时更好的方法是使错误根本不会发生。
Eg.窗口的rename函数可能要realloc,从而导致失败,但通过分配超额的内存空间(都取名字长度的最大值),则这个使错误不会出现,从而避免了错误处理的代码。
7编码中的假象
7.1只引用属于你自己的存储空间
7.2不能引用已释放的存储区
7.3只有系统才能拥有空闲的存储区,程序员不能拥有
决不要使用free以后的内存
7.4不要把输出内存用作工作区缓存
Don'tuseoutputmemoryasworkspacebuffers.
7.5不要利用静态(或全局)量存储区传递数据
7.6不要写寄生函数
依赖于别的函数内部处理的函数叫寄生函数,被依赖的叫宿主函数。
宿主函数的实现一旦改变,寄生函数就不能正常工作。
Eg.,FIG(FORTHInterestGroup)公布的FORTH-77中有CMOVE,FILL等函数。
如果用CMOVE实现FILL,则FILL就是寄生函数。
如果CMOVE实现为一次拷贝4个字节,则FILL就失败。
/*CMOVE───用头到头的移动来转移存储*/
voidCMOVE(byte*pbFrom,byte*pbTo,size_tsize)
{
while(size-->0)
*pbTo++=*pbFrom++;
}
/*FILL填充某一存储域*/
voidFILL(byte*pb,size_tsize,byteb)
{
if(size>0)
{
*pb=b;
CMOVE(pb,pb+1,size-1);
}
}
7.7不要滥用程序设计语言
用一把螺丝刀来播开油漆罐的盖子,然后又用这把螺丝刀来搅拌油漆――这并不是正确的做法,之所以这样做是因为当时这样很方便,而且能够解决问题。
程序设计语言也是如此。
Eg.不要将比较的结果作为计算表达式的一部分
另外标准也会变。
Eg.Forth-77和Forth-83中的布尔值定义
7.8紧凑的C代码并不能保证得到高效的机器代码
我的观点是:
如果你总是使用稀奇古怪的表达式,以便把C代码尽量写在源代码的一行上,从而达到最好的瑜伽状态的话,你很可能患有可怕的“一行清”(one-line-itis)疾病(也称为程序设计语言综合症)
7.9为一般水平的程序员编写代码
8剩下来的就是态度问题
8.1错误几乎不会“消失”
错误消失有三个原因:
一是错误报告不对;二是错误已被别的程序员改正了;三是这个错误依然存在但没有表现出来。
8.2马上修改错误,不要推迟到最后
●不要通过把改正错误移置产品开发周期的最后阶段来节省时间。
修改一年前写的代
码比修改几天前写的代码更难,实际上这是浪费时间。
●“一次性”地修改错误会带来许多问题:
早期发现的错误难以重现。
●错误是一种负反馈,程序开发倒是快了,却使程序员疏于检查。
如果规定只有把错误全部改正之后才能增加新特征的话,那么在整个产品开发期间都可以避免程序员的疏漏,他们将忙于修改错误。
反之,如果允许程序员略过错误,那就使管理失控。
●若把错误数保持在近乎于0的数量上,就可以很容易地预言产品的完成时间。
只需要估算一下完成32个特征所需的时间,而不需要估算完成32个特征加上改正1742个错误所需的时间。
更好的是,你总能处于可随时交出已开发特征的有利地位。
8.3修改错误要治本,不要治标
8.4除非关系产品的成败,否则不要整理代码
整理代码的问题在于程序员总不把改进的代码作为新代码处理,导致测试不够
8.5不要实现没有战略意义的特征
8.6不设自由特征
对于程序员来说,增加自由特征可能不费事,但是对于特征来讲,它不仅仅增多了代码,还必须有人为该特征写又档,还必须有人来测试它。
不要忘记还必须有人来修改该特征可能出现的错误。
8.7不允许没有必要的灵活性
Eg.realloc的参数
8.8在找到正确的解法之前,不要一味地“试”,要花时间寻求正确的解
8.9尽量编写和测试小块代码。
即使测试代码会影响进度,也要坚持测试代码
8.10测试代码的责任不在测试员身上,而是程序员自己的责任
开发人员和测试人员分别从内外开始测试,所以不是重复劳动。
8.11不要责怪测试员发现了你的错误
8.12建立自己优先级列表并坚持之
约克的优先级列表
吉尔的优先级列表
正确性
正确性
全局效率
可测试性
大小
全局效率
局部效率
可维护性/明晰性
个人方便性
一致性
可维护性/明晰性
大小
个人表达方式
局部效率
可测试性
个人表达方式
一致性
个人方便性
8.13你必须养成经常询问怎样编写代码的习惯。
本书就是长期坚持询问一些简单问题所得的结果。
●我怎样才能自动检测出错误?
●我怎样才能防止错误?
●这种想法和习惯是帮助我编写无错代码呢还是妨碍了我编写无错代码?
9本书各章要点汇总
书中每章结束时都小结了本章要点,这里汇总如下:
9.1假想的编译程序
●消除程序错误的最好方法是尽可能早、尽可能容易地发现错误,要寻求费力最小的自动查错方法。
●努力减少程序员查错所需的技巧。
可以选择的编译程序或lint警告设施并不要求程序员要有什么查错的技巧。
在另一个极端,高级的编码方法虽然可以查出或减少错误,但它们也要求程序员要有较多的技巧,因为程序员必须学习这些高级的编码方法。
9.2自己设计并使用断言
●要同时维护交付和调试两个版本。
封装交付的版本,应尽可能地使用调试版本进行自动查错。
●断言是进行调试检查的简单方法。
要使用断言捕捉不应该发生的非法情况。
不要混淆非法情况与错误情况之间的区别,后者是在最终产品中必须处理的。
●使用断言对函数的参数进行确认,并且在程序员使用了无定义的特性时向程序员报警。
函数定义得越严格,确认其参数就越容易。
●在编写函数时,要进行反复的考查,并且自问:
“我打算做哪些假定?
”一旦确定了相应的假定,就要使用断言对所做的假定进行检验,或者重新编写代码去掉相应的假定。
另外,还要问:
“这个程序中最可能出错的是什么,怎样才能自动地查出相应的错误?
”努力编写出能够尽早查出错误的测试程序。
●一般教科书都鼓励程序员进行防错性程序设计,但要记住这种编码风格会隐瞒错误。
当进行防错性编码时如果“不可能发生”的情况确实发生了,要使用断言进行报警。
9.3为子系统设防
●考查所编写的子系统,问自己:
“在什么样的情况下,程序员在使用这些子系统时会犯错误。
”在子系统中加上相应的断言和确认检查代码,以捕捉难于发现的错误和常见的错误。
●如果不能使错误不断重现,就无法排除它们。
找出程序中可能引起随机行为的因素,并将它们从程序的调试版本中清除。
把目前尚“无定义”的内存单元置成了某个常量值,就可能产生这种错误。
在这种情况下,如果程序在该单元被正确地定义为某个值之前引用了它的内容,那么每次执行这部分错误的代码,都会得到同样的错误结果。
●如果所编写的子系统释放内存(或者其它的资源),并因此产生了“无用信息”,那么要把它搅乱,使它真的像无用信息。
否则,这些被释放了的数据就有