提示:本文是一篇个人读书笔记,并不是对原书内容的系统总结整理,除了两处标红部分,可能对作者以外的读者没有太大的参考意义。
每条准则都有例外
第1章 假想的编译程序
使用编译程序所有可选警告设施
在使用nginx时深切地体会到了这一点的好处:有一行语句在编码时漏掉了,由于这个语句是唯一使用某个变量的地方,而且默认开启了-Werror=unused-value,才发现了这个错误。
使用函数原型来检查参数(现在不使用也可以);
必要的空语句用NULL代替,以防while()后多打的分号造成的bug
相等判断反着写以防写成赋值:'t' == char
使用lint来查出编译程序漏掉的错误
如果有单元测试,进行单元测试
第2章 自己设计并使用语言
既要维护程序的交付版本,又要维护程序的开发版本
从使用#ifdef DEBUG ... #endif到断言assert()的转变,后者也只在定义了DEBUG时有效。
assert()宏的实现版本之一,其中__LINE__是源文件中的行数,并不包括包含了头文件后的改变量。
为什么要用宏定义+函数实现,并将宏中的__LINE__传递给后面实现的函数?因为__LINE__用其所在的行号替换内容,使用函数则只会变成该函数内部的行号,而使用宏则只是把__LINE__放到对应需要检查的位置。更具体的说明可以参考以下链接:
http://stackoverflow.com/questions/11214260/behavior-of-line-in-inline-functions
http://stackoverflow.com/questions/7929291/get-code-line-with-line
要使用断言对函数参数进行确认
要从程序中删去无定义的特性,或者在程序中使用断言来检查出无定义特性的非法使用
不要浪费别人的时间——详细说明不清楚的断言
消除所做的隐式假定,或者利用断言检查其正确性
利用断言检查不可能发生的情况
在进行防错性程序设计时,不要隐瞒错误
要利用不同的算法对程序的结果进行确认
不要等待错误发生,要使用初始检查程序
第3章 为子系统设防
外壳函数(cover function)和UNP上的包裹函数(wrapper function)很类似。
指针作为参数传值的时候可以修改指向地址的内容,但不能修改其指向的地址本身。如何修改?传递**p即可。
int fNewMemory(void **pv,size_t size) { void** ppb = pv; *ppb = (void *)malloc(size); return (*ppb!=NULL); }
用重复而有特征(取决于机器)的垃圾字符填充分配而未初始化的内存空间有助于错误再现,这也是下两条准则的来源。
要消除随机特性——使错误可再现
冲掉无用的信息,以免被错误地使用
如果某件事甚少发生的话,设法使其经常发生
关于日志:
保存调试信息,以便进行更强的错误检查
结合日志、内存块分配管理,可以进行一致性检查。
建立详尽的子系统检查并且经常地进行这些检查
仔细设计程序的测试代码,任何选择都应该经过考虑
努力做到透明的一致性检查
调试版本应该尽量考虑如何方便地找出错误,而不是一味地追求速度和考虑约束,后者是交付版本应该完成的,而同时交付版本不用考虑前者。
不要把对交付版本的约束应用到相应的调试版本上,要用大小和速度来换取错误检查能力
第4章 对程序进行逐条跟踪
不要等到出了错误再对程序进行逐条的跟踪
对每一条代码路径进行逐条的跟踪
当对代码进行逐条跟踪时,要密切注意数据流
即使是单步运行也很难对三目运算?:这样的语句进行内部细节的查看,而生成的汇编语句则可以办到。但这里也带来了一个疑问:如果以帮助程序调试为目的,汇编应该掌握到何种地步?
源级调试程序可能会隐瞒执行的细节,对关键部分的代码要进行汇编指令级的逐条跟踪
第5章 糖果机界面
比如getchar(),参数是int,返回值在EOF时为-1,作者认为这就像一个容易误导人的糖果机。
要是用户不容易忽视错误情况,不要在正常地返回值中银隐藏错误代码
要不遗余力第寻找并消除函数界面中的缺陷
作者认为realloc这样的函数集多功能为一身,并不是很好的函数。
不要编写多种功能集于一身的函数;为了对参数进行更强的确认,要编写功能单一的函数
不要模棱两可,要明确地定义函数的参数
原书作者提到,他为微软的面试者提出一个问题:编写正确的tolower()。很多人都不能给出正确答案,问题在于假定参数是大写字母以及如果不是大写字母就提示出错并使用返回值,后者把错误的结果与正确的混在一起了。作者建议使用断言来避免不是大写字母的情况。
编写函数使其在给定有效的输入情况下不会失败
函数参数尽量避免用一个布尔值来控制其具体实现的功能,可以使用更为具体的值,并且用断言来检查不合法的输入。
使程序在调用点明了易懂;要避免布尔参数
编写注释突出可能的异常情况
第6章 风险事业
使用有严格定义的数据类型
针对在循环和不等式判断任何整数都有可能的上溢和无符号整数的下溢(具体例子参考原书),
经常反问:“这个变量表达式会上溢或者下溢吗?”
尽可能精确地实现设计,近似地实现设计就可能出错
一个“任务”应一次完成
避免无关紧要的if语句
避免使用嵌套的?:语句
每种特殊情况只处理一次
原书中提到了C语言中可能导致错误的惯用语:
地址比较pch<pchEnd;pch++;可能在循环中,pch指向内存末尾;
while(--size=>0)与while(size-->0)相比,尽管看上去是一样的,其实后者无论size是否有符号都能正常工作;
代码中,无符号运算时用位移代替除2的幂似乎可以加快速度,但其实很多机器会自动地把后者优化为前者;有符号数是可以这样做的,如果操作数总为正,更好的方式是把它转成无符号数,然后进行除法,是否进行优化工作留给编译器完成。
避免使用有风险的语言惯用语
不能毫无必要地将不用类型地操作符混合使用,如果必须将不同类型地操作符混合使用,就用括号把它们隔离开来
避免调用返回错误的函数
第7章 编码中的假象
如果是对一个不完全由调用函数所掌握的空间进行修改,可能导致错误。具体的例子是在一段地址内寻找字符时,为了防止要处理找不到的情况,将最后一个设为目标。这在查找算法中是可能的。准则中“引用”指不仅读而且写。
只引用属于你自己的存储空间
只有系统才能拥有空闲的存储区,程序员不能拥有
指向输出的指针不是指向工作空间缓冲区的指针
不要利用静态(或全局)量存储区传递数据
寄生函数是依赖别的函数的具体实现的函数。
不要写寄生函数
不要滥用程序设计语言
紧凑的C代码并不能保证得到高效的机器代码
为一般水平的程序员编写代码
第8章 剩下的就是态度问题
错误几乎不会“消失”
马上修改错误,不要推迟到最后
修改错误要治本,不要治表
除非关系产品的成败,否则不要整理代码
不要实现没有战略意义的特征
不设自由特征
不允许没有必要的灵活性
在找到正确的解法之前,不要一味地 “ 试 ” ,要花时间寻求正确的解
尽量编写和测试小块代码。即使测试代码会影响进度,也要坚持测试代码
测试代码的责任不在测试员身上,而是程序员自己的责任
不要责怪测试员发现了你的错误
建立自己优先级列表并坚持之
走向何方
决不允许同样错误出现两次