转载自:http://blog.csdn.net/hopesophite/archive/2006/08/02/1010643.aspx
写在前面的话:
这两天看了《Writing Clean Code》,很受启发,感觉值得再读,于是整理了一点笔记,作为checklist,以备速查。
原书共8章,每章都举一些例子,指出不足,再用通用的规则改写,每章结束时会总结一下要点,其中覆盖了比较重要的规则。附录A是作者整理的编码检查表。本笔记前8章和原书前8章对应,列出了所有的规则,对比较特别或者比较难理解的规则还附上了书中的例子,偶尔加一两句个人的想法。第9章是原书各章末尾要点的汇总。第10章是原书的编码检查表。
本笔记只作为原书的一个速查手册,详细的内容请看原书。
中译本:
《编程精粹 ─── Microsoft 编写优质无错C 程序秘诀》Steve Maguire 著,姜静波 佟金荣 译,麦中凡 校,电子工业出版社
英文版:
《Writing Clean Code ── Microsoft Techniques for Developing Bug-free C Programs》Steve maguire, Microsoft Press
英文版原名:
《Writing Solid Code ── Microsoft Techniques for Developing Bug-free C Programs》Steve maguire, Microsoft Press
1 假想的编译程序
1.1 使用编译程序所有的可选警告设施
1.2 使用lint 来查出编译程序漏掉的错误
1.3 如果有单元测试,就进行单元测试
1.4 Tips
C 的预处理程序也可能引起某些意想不到的结果。例如,宏UINT_MAX 定义在limit.h
中,但假如在程序中忘了include 这个头文件,下面的伪指令就会无声无息地失败,
因为预处理程序会把预定义的UINT_MAX 替换成0:
#if UINT_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.12 Tips
不要把真正需要执行的语句放在断言里
3 为子系统设防
3.1 要消除随机特性 ─── 使错误可再现
3.2 冲掉无用的信息,以免被错误地使用
分配内存时填充上非法值:eg. 68000 用0xA3,Intel X86系列用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的返回值,但肯定会有很多程序员忘记这一点,由此可能会引发难以捕捉的错误。
作者设计了另一个函数界面来处理这种情况:int fGetChar(char*),返回值存入char*所指位置,而int返回flag,为true表示正确。这样,由于划分了正常的返回值和错误代码,避免了getchar的返回值要用int接收的问题。
5.2 要不遗余力地寻找并消除函数界面中的缺陷
Eg. 下述代码隐含着一个错误
pbBuf = (byte*)realloc( pbBuf, sizeNew );
if( pbBuf != NULL )
使用初始化这个更大的缓冲区
如果realloc分配内存时失败,返回NULL,则pbBuf为NULL,它原来指向的内存将会丢失。
如果界面是flag fResizeMemory( void** ppv, size _t sizeNew )则好得多
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. 以下两个函数声明会导致调用方式的不同:
void UnsignedToStr(unsigned u, char *strResult, flag fDecimal);
void UnsignedToStr(unsigned u, char* str, unsigned base);
前者的调用方式是:
UnsignedToStr(u, str, TRUE);
UnsignedToStr(u, str, FALSE);
这显然不好。而后者是UnsignedToStr(u, str, BASE10)则好的多。
5.7 编写注解突出可能的异常情况
用注释写出常见的错误用法和正确用法的例子。
5.8 小结
本章先给出一个界面不好的例子,再给出一般原则:要不遗余力的检查界面的合理性。然后讲功能要单一,输入要有限制,输出的正常返回值要与错误码分开,用调用方式检查界面,用注释来指出异常情况。
6 风险事业
6.1 使用有严格定义的数据类型
可移植类型最值得注意之处是:它们只考虑了三种最通用的数制:壹的补码、贰的补码
和有符号的数值。
Char只有0~127吗是可移植的
Unsigned char 是0~255,但signed char是-127~127 (没有-128吗)是可移植的
6.2 经常反问:“这个变量表达式会上溢或下溢吗?”
Eg. 以下代码会导致无穷循环,因为ch会上溢为0,导致不可能大于UCHAR_MAX。
unsigned char ch;
/* 首先将每个字符置为它自己 */
for (ch=0; ch <= UCHAR_MAX;ch++)
chToLower[ch] = ch;
eg. 以下代码会下溢,导致无穷循环,因为size_t是无符号型,不可能小于0
size_t size = 100;
while (--size >= 0)
NULL;
6.3 尽可能精确地实现设计,近似地实现设计就可能出错
6.4 一个“任务”应只实现一次(Implement "the task" just once).
一个原则:Strive to make everyfunction perform its task exactly
one time
static window * pwndRootChildren = NULL;
void AddChild( 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 语句
以指针为中心的树的构建,可以不必为特殊情况编写代码:
void AddChild(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( pch < pchEnd )
NULL;
如果pchEnd恰好查找到存储器的结尾处,那么所指的位置就不存在了
Eg. 除以2和移位:移位有风险
Eg. while (--size >= 0) 和while(size-- > 0),前者有风险,后者却没有。
6.9 不能毫无必要地将不同类型地操作符混合使用,如果必须将不同类型地操作符混合使用,就用括号把它们隔离开来
6.10 避免调用返回错误的函数(Avoid calling functions that return errors)
这样,就不会错误地处理或漏掉由其它人设计的函数所返回的错误条件。
如果自始至终程序反复处理同样的错误条件,就将错误处理部分独立出来。Eg. 单独的错误处理子程序。
有时更好的方法是使错误根本不会发生。Eg. 窗口的rename函数可能要realloc,从而导致失败,但通过分配超额的内存空间(都取名字长度的最大值),则这个使错误不会出现,从而避免了错误处理的代码。
7 编码中的假象
7.1 只引用属于你自己的存储空间
7.2 不能引用已释放的存储区
7.3 只有系统才能拥有空闲的存储区,程序员不能拥有
决不要使用free以后的内存
7.4 不要把输出内存用作工作区缓存
Don't use output memory as workspace buffers.
7.5 不要利用静态(或全局)量存储区传递数据
7.6 不要写寄生函数
依赖于别的函数内部处理的函数叫寄生函数,被依赖的叫宿主函数。
宿主函数的实现一旦改变,寄生函数就不能正常工作。
Eg. ,FIG(FORTH Interest Group)公布的FORTH-77中有CMOVE, FILL等函数。如果用CMOVE实现FILL,则FILL就是寄生函数。如果CMOVE实现为一次拷贝4个字节,则FILL就失败。
/* CMOVE ─── 用头到头的移动来转移存储 */
void CMOVE (byte *pbFrom,byte *pbTo,size_t size)
{
while(size-- > 0 )
*pbTo++ = *pbFrom++;
}
/* FILL 填充某一存储域 */
void FILL (byte *pb,size_t size,byte b)
{
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 马上修改错误,不要推迟到最后
l 不要通过把改正错误移置产品开发周期的最后阶段来节省时间。修改一年前写的代
码比修改几天前写的代码更难,实际上这是浪费时间。
l “一次性”地修改错误会带来许多问题:早期发现的错误难以重现。
l 错误是一种负反馈,程序开发倒是快了,却使程序员疏于检查。如果规定只有把错误全部改正之后才能增加新特征的话,那么在整个产品开发期间都可以避免程序员的疏漏,他们将忙于修改错误。反之,如果允许程序员略过错误,那就使管理失控。
l 若把错误数保持在近乎于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 你必须养成经常询问怎样编写代码的习惯。
本书就是长期坚持询问一些简单问题所得的结果。
l 我怎样才能自动检测出错误?
l 我怎样才能防止错误?
l 这种想法和习惯是帮助我编写无错代码呢还是妨碍了我编写无错代码?
9 本书各章要点汇总
书中每章结束时都小结了本章要点,这里汇总如下:
9.1 假想的编译程序
l 消除程序错误的最好方法是尽可能早、尽可能容易地发现错误,要寻求费力最小的自动查错方法。
l 努力减少程序员查错所需的技巧。可以选择的编译程序或lint 警告设施并不要求程序员要有什么查错的技巧。在另一个极端,高级的编码方法虽然可以查出或减少错误,但它们也要求程序员要有较多的技巧,因为程序员必须学习这些高级的编码方法。
9.2 自己设计并使用断言
l 要同时维护交付和调试两个版本。封装交付的版本,应尽可能地使用调试版本进行自动查错。
l 断言是进行调试检查的简单方法。要使用断言捕捉不应该发生的非法情况。不要混淆非法情况与错误情况之间的区别,后者是在最终产品中必须处理的。
l 使用断言对函数的参数进行确认,并且在程序员使用了无定义的特性时向程序员报警。函数定义得越严格,确认其参数就越容易。
l 在编写函数时,要进行反复的考查,并且自问:“我打算做哪些假定?”一旦确定了相应的假定,就要使用断言对所做的假定进行检验,或者重新编写代码去掉相应的假定。另外,还要问:“这个程序中最可能出错的是什么,怎样才能自动地查出相应的错误?”努力编写出能够尽早查出错误的测试程序。
l 一般教科书都鼓励程序员进行防错性程序设计,但要记住这种编码风格会隐瞒错误。当进行防错性编码时如果“不可能发生”的情况确实发生了,要使用断言进行报警。
9.3 为子系统设防
l 考查所编写的子系统,问自己:“在什么样的情况下,程序员在使用这些子系统时会犯错误。”在子系统中加上相应的断言和确认检查代码,以捕捉难于发现的错误和常见的错误。
l 如果不能使错误不断重现,就无法排除它们。找出程序中可能引起随机行为的因素,并将它们从程序的调试版本中清除。把目前尚“无定义”的内存单元置成了某个常量值,就可能产生这种错误。在这种情况下,如果程序在该单元被正确地定义为某个值之前引用了它的内容,那么每次执行这部分错误的代码,都会得到同样的错误结果。
l 如果所编写的子系统释放内存(或者其它的资源),并因此产生了“ 无用信息”,那么要把它搅乱,使它真的像无用信息。否则,这些被释放了的数据就有可能仍被使用,而又不会被注意到。
l 类似地,如果在所编写的子系统中某些事情可能发生,那么要为该子系统加上相应的调试代码,使这些事情一定发生。这样可以增大查出通常得不到执行的代码中的错误的可能性。
l 尽力使所编写的测试代码甚至在程序员对其没有感觉的情况下亦能起作用。最好的测试代码是不用知道其存在也能起作用的测试代码。
l 如果可能的话,把测试代码放到所编写的子系统中,而不要把它放到所编写子系统的外层。不要等到进行了系统编码时,才考虑其确认方法。在子系统设计的每一步,都要考虑“如何对这一实现进行详尽的确认”这一问题。如果发现这一设计难于测试或者不可能对其进行测试,那么要认真地考虑另一种不同的设计,即使这意味着用大小或速度作代价去换取该系统的测试能力也要这么做。
l 在由于速度太慢或者占用的内存太多而抛弃一个确认测试程序之前,要三思而后行。切记,这些代码并不是存在于程序的交付版本中。如果发现自己正在想:“这个测试程序太慢、太大了”,那么要马上停下来问自己:“怎样才能保留这个测试程序,并使它既快又小?”
9.4 对程序进行逐条跟踪
l 代码中不会自己生出错误来,错误是程序员编写新代码或者修改现有代码的产物。如果你想发现代码中的错误,没有哪个办法比在对代码进行编译时对其进行逐条跟踪更好。
l 虽然直观上你可能认为对代码进行走查会花费大量的时间,但这是不对的。刚开始进行代码的走查确实要多花一点时间,但当这一切习惯成自然之后并不会多花多少时间,你可以很快地走查一遍。
l 一定要对每一条代码路径进行逐条的跟踪,至少要跟踪一遍,尤其是对代码中的错误处理部分。不要忘记 &&、|| 和?:这些运算符,它们每个都有两条代码路径需要进行测试。
l 在某些情况下也许需要在汇编语言级对代码进行逐条的跟踪。尽管不必经常这样做,但在必要的时候不要回避这种做法。
9.5 糖果机界面
l 最容易使用和理解的函数界面,是其中每个输入和输出参数都只代表一种类型数据的界面。把错误值和其它的专用值混在函数的输入和输出参数中,只会搞乱函数的界面。
l 设计函数的界面迫使程序员考虑所有重要细节(如错误情况的处理),不要使程序员能够很容易地忽视或者忘记有关的细节。
l 老要想到程序员调用所编函数的方式,找出可能使程序员无意间引入错误的界面缺陷。尤其重要的是要争取编出永远成功的函数,使调用者不必进行相应的错误处理。
l 为了增加程序的可理解性从而减少错误,要保证所编函数的调用能够被必须阅读这些调用的程序员所理解。莫明其妙的数字和布尔参数都与这一目标背道而驰,因此应该予以消除。
l 分解多功能的函数。取更专门的函数名(如ShrinkMemory 而不是 realloc)不仅可以增进人们对程序的理解,而且使我们可以采用更加严格的断言自动地检查出调用错误。
l 为了向程序员展示出所编函数的适当调用方法,要在函数的界面中通过注解的方式详细说明。要强调危险的方面。
9.6 风险事业
l 在选择数据类型的时候要谨慎。虽然ANSI 标准要求所有的执行程序都要支持char, int,long 等类型,但是它并没有具体定义这些类型。为了避免程序出错,应该只按照ANSI 的标准选择数据类型。
l 由于代码可能会在不理想的硬件上运行,因此很可能算法是正确的而执行起来却有错。所以要经常详细检查计算结果和测试结果的数据类型范围是否上溢或下溢。
l 在实现某个设计的时候,一定要严格按照设计去实现。如果在编写代码时只是近似地实现所提出的要求,那就很容易出错。
l 每个函数应该只有一个严格定义的任务,不仅如此,完成每个任务也应只有一种途径。假如不管输入什么都能执行同样的代码,那就会大大降低那些不易被发现的错误所存在的概率。
l if 语句是个警告信号,说明代码所做的工作可能比所需要的要多。努力消除代码中每一个不必要的if 语句,经常反问自己:“怎样改变设计从而删掉这个特殊情况?”有时可能要改变数据结构,有时又要改变一下考察问题的方式,就象透镜是凸的还是凹的问题一样。
l 有时if 语句隐藏在while 和for 循环的控制表达式中。“?:”操作符是if 语句的另外一种形式。
l 曾惕有风险的语言惯用语,注意那些相近但更安全的惯用语。特别要警惕那些看上去象是好编码的惯用语,因为这样的实现对总体效率很少有显著的影响,但却增加了额外的风险性。
l 在写表达式时,尽量不要把不同类型的操作符混合起来,如果必须混合使用,用括号把它们分隔开来。
l 特殊情况中的特殊情况是错误处理。如果有可能,应该尽量避免调用可能失败的函数,假如必须调用返回错误的函数,将错误处理局部化以便所有的错误都汇集到一点,这将增加在错误处理代码中发现错误的机会。
l 在某些情况下,取消一般的错误处理代码是有可能的,但要保证所做的事情不会失败。这就意味着在初始化时要对错误进行一次性处理或是从根本上改变设计。
9.7 编码中的假象
l 如果你要用到的数据不是你自己所有的,那怕是临时的,也不要对其执行写操作。尽管你可能认为读数据总是安全的,但是要记住,从映射到I/O 的存储区读数据,可能会对硬件造成危害。
l 每当释放了存储区人们还想引用它,但是要克制自己这么做。引用自由存储区极易引起错误。
l 为了提高效率,向全局缓冲区或静态缓冲传递数据也是很吸引人的,但是这是一条充满风险的捷径。假若你写了一个函数,用来创建只给调用函数使用的数据,
那么就将数据返回给调用函数,或保证不意外地更改这个数据。
l 不要编写依赖支持函数的某个特殊实现的函数。我们已经看到,FILL 例程不该象给出的那样调用CMOVE,这种写法只能作为坏程序设计的例子。
l 在进行程序设计的时候,要按照程序设计语言原来的本意清楚、准确地编写代码。避免使用有疑问的程序设计惯用语,即使语言标准恰好能保证它工作,也不要使用。请记住,标准也在改变。
l 如果能用C 语言有效地表示某个概念,那么类似地,相应的机器代码也应该是有效的。逻辑上讲似乎应该是这样,可是事实上并非如此。因此在你将多行C代码压缩为一行代码之前,一定要弄清楚经过这样的更改以后,能否保证得到更好的机器代码。
l 最后,不要象律师写合同那样来编写代码。如果一般水平的程序员不能阅读和理解你的代码,那就说明你的代码太复杂了,使用简单一点的语言。
9.8 剩下来的就是态度问题
l 错误既不会自己产生,也不会自己改正。如果你得到了一个错误报告,但这个错误不再出现了。不要假设测试员发生了幻觉,而要努力查找错误,甚至要恢复程序的老版本。
l 不能“以后”再修改错误。这是许多产品被取消的共同教训。如果在你发现错误的时候就及时地更正了错误,那你的项目就不会遭受毁灭性的命运。当你的项目总是保持近似于0 个错误时,怎么可能会有一系列的错误呢?
l 当你跟踪查到一个错误时,总要问一下自己,这个错误是否会是一个大错误的症状。当然,修改一个刚刚追踪到的症状很容易,但是要努力找到真正的起因。
l 不要编写没有必要的代码。让你的竞争者去清理代码,去实现“冷门”但无价值的特征,去实现自由特征。让他们花大量的时间去修改由于这些无用代码所引起的所有没有必要的错误。
l 记住灵活与容易使用并不是一回事。在你设计函数和特征时,重点是使之容易使用;如果它们仅仅是灵活的,象realloc 函数和Excel 中的彩色格式特征那样,那么就没法使得代码更加有用;相反地,使得发现错误变得更困难了。
l 不要受“试一试”某个方案以达到预期结果的影响。相反,应把花在尝试方案上的时间用来寻找正确的解决方法。如果必要,与负责你操作系统的公司联系,这比提出一个在将来可能会出问题的古怪实现要好。
l 代码写得尽量小以便于全面测试。在测试中不要马虎。记住,如果你不测试你的代码,就没有人会测试你的代码了。无论怎样,你也不要期望测试组为你测试代码。
l 最后,确定你们小组的优先级顺序,并且遵循这个顺序。如果你是约克,而项目需要吉尔,那么至少在工作方面你必须改变习惯。
10 本书附录A 编码检查表
本附录给出的问题列表,总结了本书的所有观点。使用本表的最好办法是花两周时间评
审一下你的设计和编码实现。先花几分钟时间看一看列表,一旦熟悉了这些问题,就可以灵
活自如地按它写代码了。此时,就可以把表放在一边了。
一般问题
── 你是否为程序建立了DEBUG 版本?
── 你是否将发现的错误及时改正了?
─一 你是否坚持彻底测试代码.即使耽误了进度也在所不惜?
── 你是否依靠测试组为你测试代码?
─一 你是否知道编码的优先顺序?
─一 你的编译程序是否有可选的各种警告?
关于将更改归并到主程序
─一 你是否将编译程序的警告(包括可选的)都处理了?
── 你的代码是否未用Lint
─一 你的代码进行了单元测试吗?
─一 你是否逐步通过了每一条编码路径以观察数据流?
─一 你是否逐步通过了汇编语言层次上的所有关键代码?
── 是否清理过了任何代码?如果是,修改处经过彻底测试了吗?
─一 文档是否指出了使用你的代码有危险之处?
── 程序维护人员是否能够理解你的代码?
每当实现了一个函数或子系统之时
─一 是否用断言证实了函数参数的有效性?
─一 代码中是否有未定义的或者无意义的代码?
─一 代码能否创建未定义的数据?
─一 有没有难以理解的断言?对它们作解释了没有?
─一 你在代码中是否作过任何假设?
─一 是否使用断言警告可能出现的非常情况?
─一 是否作过防御性程序设计?代码是否隐藏了错误?
─一 是否用第二个算法来验证第一个算法?
─一 是否有可用于确认代码或数据的启动(startup)检查?
─一 代码是否包含了随机行为?能消除这些行为吗?
── 你的代码若产生了无用信息,你是否在DEBUG 代码中也把它们置为无用信息?
── 代码中是否有稀奇古怪的行为?
── 若代码是子系统的一部分,那么你是否建立了一个子系统测试?
── 在你的设计和代码中是否有任意情况?
── 即使程序员不感到需要,你也作完整性检查吗?
── 你是否因为排错程序太大或太慢,而将有价值的DEBUG 测试抛置一边?
── 是否使用了不可移植的数据类型?
─一 代码中是否有变量或表达式产生上溢或下溢?
── 是否准确地实现了你的设计?还是非常近似地实现了你的设计?
── 代码是否不止一次地解同一个问题?
── 是否企图消除代码中的每一个if 语句?
── 是否用过嵌套?:运算符?
── 是否已将专用代码孤立出来?
── 是否用到了有风险的语言惯用语?
─一 是否不必要地将不同类型的运算符混用?
── 是否调用了返回错误的函数?你能消除这种调用吗?
─一 是否引用了尚未分配的存储空间?
─一 是否引用已经释放了的存储空间?
── 是否不必要地多用了输出缓冲存储?
── 是否向静态或全局缓冲区传送了数据?
── 你的函数是否依赖于另一个函数的内部细节?
── 是否使用了怪异的或有疑问的C 惯用语?
── 在代码中是否有挤在一行的毛病?
── 代码有不必要的灵活性吗?你能消除它们吗?
─一 你的代码是经过多次“试着”求解的结果吗?
─一 函数是否小并容易测试?
每当设计了一个函数或子系统后
─一 此特征是否符合产品的市场策略?
─一 错误代码是否作为正常返回值的特殊情况而隐藏起来?
─一 是否评审了你的界面,它能保证难于出现误操作吗?
─一 是否具有多用途且面面俱到的函数?
─一 你是否有太灵活的(空空洞洞的)函数参数?
─一 当你的函数不再需要时,它是否返回一个错误条件?
─一 在调用点你的函数是出易读?
─一 你的函数是否有布尔量输入?
修改错误之时
── 错误无法消失,是否能找到错误的根源?
─一 是修改了错误的真正根源,还是仅仅修改了错误的症状?