------------------------------------------------------------------------------------------------------------
1、
为了使匈牙利式名字的描述性更强.或者要区分两个变量名,可以在相应类型派生出的基本名字之后加上一个以大写字母开头的“标签”。
2、当确定需要用空语句时,你就用。但最好用NULL使其明显可见。
3、如果函数有两个参数的类型相同,那么即使在调用该函数时互换了这两个参数的位置,原型也查不出这一调用错误。在原型中使用更精确类型的缺陷是常常必须进行参数的显式类型转换,以消除类型不匹配的错误,即使参数的次序正确。
--使用编译程序所有的可选警告设施
4、另一种检查错误更详细、更彻底的方法是使用lint,这种方法几乎不费什么事。最初,lint这个工具用来扫描C源文件并对源程序中不可移植的代码提出警告。但是现在大多数lint实用程序已经变得更加严密,它不但可以检查出可移植性问题,而且可以检查出那些虽然可移植并且完全合乎语法但却很可能是错误的特性--使用lint来查出编译程序漏掉的错误
5、C的预处理程序也可能引起某些意想不到的结果。例如,宏UINT_MAX定义在limit.h中,但假如在程序中忘了include这个头文件,下面的伪指令就会无声无息地失败,因为预处理程序会把预定义的UINT_MAX替换成 0:
……
#if UINT_MAX > 65535u
……
#endif
怎样使预处理程序报告出这一错误?
------------------------------------------------------------------------------------------------------------
6、自己设计并使用断言。要使用断言对函数参数进行确认
#ifdef DEBUG
void _Assert(char* , unsigned); /* 原型 */
#define
ASSERT(f) \
if(f) \
NULL; \
else \
_Assert(__FILE__ , __LINE__)
#else
#define ASSERT(f) NULL
#endif
void _Assert(char* strFile, unsigned uLine)
{
fflush(stdout);
fprintf(stderr, “\nAssertion failed: %s, line %u\n”,strFile, uLine);
fflush(stderr);
abort();
}
assert与ASSERT之间的另一点不同。标准宏assert除了给出以上信息之外,还显示出已经失败了的测试条件。
在错误消息中包括测试表达式的唯一麻烦是每当使用assert时,它都必须为_Assert产生一条与该条件对应的正文形式打印消息。但问题是,编译程序要在哪儿存储这个字符串呢?Macintosh、DOS 和
Windows 上的编译程序通常在全局数据区存储字符串。但内存有限啊!!!
7、“无定义”意味着“要避开”。对于程序员来说,无定义的特性就相当于非法的特征,因此要利用断言对其进行检查。
要经常停下来看看程序中有没有使用无定义的特性。如果程序中使用了无定义的特性就要把它从相应的设计中去掉,或者在程序中包括相应的断言,以便在使用了无定义的特性时,能够向程序员发出通报。还要注意给不够清晰的断言加上注解。
如果读者停下来读读ANSI C中 memcpy函数的定义,就会看到其最后一行说:“如果在存储空间相互重叠的对象之间进行了拷贝,其结果无定义”。例如对memcpy函数的加强:
void
memcpy(void* pvTo, void* pvFrom, size_t size)
{
void* pbTo =
(byte*)pvTo;
void* pbFrom = (byte*)pvFrom;
ASSERT(pvTo !=
NULL && pvFrom != NULL);
ASSERT(pbTo>=pbFrom+size ||
pbFrom>=pbTo+size);
while(size-->0)
*pbTo++ ==
*pbFrom++;
return(pvTo);
}
8、当程序员刚开始使用断言时,有时会错误地利用断言去检查真正地错误,而不去检查非法的况。看看在下面的函数strdup中的两个断言:
char* strdup(char* str)
{
char* strNew;
ASSERT(str != NULL);
strNew =
(char*)malloc(strlen(str)+1);
ASSERT(strNew != NULL);
strcpy(strNew,
str);
return(strNew);
}
第一个断言的用法是正确的,因为它被用来检查在该程序正常工作时绝不应该发生的非法情况。第二个断言的用法相当不同,它所测试的是错误情况,是在其最终产品中肯定会出现并且必须对其进行处理的错误情况。
9、消除所做的隐式假定,或者利用断言检查其正确性。
10、利用断言来检查不可能发生的情况。
11、在进行防错性程序设计时,不要隐瞒错误。
12、要利用不同的算法对程序的结果进行确认
13、不要等待错误发生,要使用初始检查程序
------------------------------------------------------------------------------------------------------------
14、瓶颈子程序是加入断言的绝佳之处。
15、通常,子系统都要对其实现细节进行隐藏,所隐藏的实现细节可能相当复杂。在进行实现细节隐藏的同时,子系统为用户提供了一些关键的入口点。程序员通
过调用这些关键的入口点来实现同子系统的通讯。因此如果在程序中使用这样的子系统并且在其调用点加上了调试检查,那么不用花很大力气就可以进行许多的错误检查。
当子系统编写完成之后,要问自己:“程序员什么情况下会错误地使用这个子系统,在这个子系统中怎样才能自动地检查出这些问题?”在正常情况下,
当开始编码排除设计中的危险因素时就应该问过了这个问题。例如:
对于内存管理程序。程序员可能犯的错误是:
1) 分配一个内存块并使用其中未经初始化的内容;
2) 释放一个内存块但继续引用其中的内容;
3) 调用 realloc 对一个内存块进行扩展,因此原来的内容发生了存储位置的变化,但程序引用的仍是原来存储位置的内容;
4) 分配一个内存块后即“失去”了它,因为没有保存指向所分配内存块的指针;
5) 读写操作越过了所分配内存块的边界;
6) 没有对错误情况进行检查。
16、由于用户可能得不到子系统的源代码,或者即使能够得到,这些源代码的实现也未必都相同,所以本书不
是直接在子程序的源代码中加上相应的测试代码,而是利用所谓的“外壳”函数把内存管理程序包装起来,并在这层包装的内部加上相应的测试代码。这就是在得不
到子系统源代码的情况下所能采用的方法。
下面我们先讨论malloc的外壳函数。它的形式如下:
/* fNewMemory ───
分配一个内存块 */
flag fNewMemory(void** pv, size_t size)
{
byte** ppb = (byte**)ppv;
*ppb = (byte*)malloc(size);
return(*ppb
!= NULL); /* 成功 */
}
在fNewMemory中加上内存块大小的检查和内存块的填充代码之后,其形式如下:
#define bGarbage 0xA3
flag
fNewMemory(void** ppv, size_t size)
{
byte** ppb =
(byte**)ppv;
ASSERT(ppv!=NULL && size!=0);
*ppb =
(byte*)malloc(size);
#ifdef DEBUG
{
if( *ppb != NULL )
memset(*ppb, bGarbage, size);
}
#endif
return(*ppb != NULL);
}
同样,free, relloc, realloc函数也应该写一个外壳函数。
void
FreeMemory(void* pv)
{
ASSERT(pv != NULL);
#ifdef DEBUG
{
memset(pv, bGarbage, sizeofBlock(pv) );
}
#endif
free(pv);
}
flag fResizeMemory(void** ppv, size_t size)
{
byte** ppb = (byte**)ppv;
byte* pbResize;
pbResize =
(byte*)realloc(*ppb, sizeNew);
if( *pbResize != NULL )
*ppb = pbResize;
return(*pbResize != NULL);
}
flag
fResizeMemory(void** ppv, size_t sizeNew)
{
byte** ppb =
(byte**)ppv;
byte* pbResize;
#ifdef DEBUG /* 在此引进调试局部变量
*/
size_t sizeOld;
#endif
ASSERT(ppb!=NULL &&
sizeNew!=0);
#ifdef DEBUG
{
sizeOld =
sizeofBlock(*ppb);
/* 如果缩小,冲掉块尾释放的内容 */
if(sizeNew memset((*ppb)+sizeNew, bGarbage,
sizeOld-sizeNew);
}
#endif
pbResize =
(byte*)realloc(*ppb, sizeNew);
if(pbResize != NULL)
{
#ifdef DEBUG
{
/* 如果扩大,对尾部增加的内容进行初始化 */
if(sizeNew
> sizeOld)
memset(pbResize+sizeOld, bGarbage,
sizeNew-sizeOld);
}
#endif
*ppb = pbResize;
}
return( pbResize != NULL );
}
17、要消除随机特性───使错误可再现
18、冲掉无用的信息,以免被错误地使用,例如上面代码中的 #define bGarbage 0xA3
19、
如果某件事甚少发生的话,设法使其经常发生
产生移动和震荡的程序 。realloc扩大内存块时使原有存储位置发生移动这种现象很罕见
尽管如此,但我们却能够模拟realloc的所作所为。如果程序员调用fResizeMemory扩大了某个内存块,那么可以通过先建一个新的内存块,然后再把原有内存块的内容拷贝到这个新块中,最后释放掉原有内存块的方法,准确地模拟出realloc的全部动作。
Flag fResizeMemory(void** ppv, size_t sizeNew)
{
byte** ppb = (byte**)ppv;
byte* pbResize;
#ifdef
DEBUG
size_t sizeOld;
#endif
ASSERT(ppb!=NULL
&& sizeNew!=0);
#ifdef DEBUG
{
sizeOld =
sizeofBlock(*ppb);
/* 如果缩小,先把将被释放的内存空间填写上废料
* 如果扩大,通过模拟 realloc
的操作来迫使新的内存块产生移动
*(不让它在原有的位置扩展)如果新块和老块的长度相同,不
* 做任何事情
*/
if(sizeNew
< sizeOld)
memset((*ppb)+sizeNew, bGarbage, sizeOld-sizeNew);
else
if(sizeNew > sizeOld)
{
byte* pbNew;
if(
fNewMemory(&pbNew, sizeNew) )
{
memcpy(pbNew, *ppb,
sizeOld);
FreeMemory(*ppb);
*ppb = pbNew;
}
}
}
#endif
pbResize = (byte*)realloc(*ppb, sizeNew);
……
}
20、保存调试信息,以便进行更强的错误检查
从调试的端点看,内存管理程序的问题是当第一次创建内存块时知道其大小,但随后几乎马上就会失去这一信息,除非在某个地方保存了一个有关的记录。我们已经看到函数sizeofBlock的价值很大,但如果能够知道已分配内存块的数目及其在内存中的具体存储位置,用处会更大。假如能够知道这些信息,那么不管指针的值是什么,我们都能够确定它是否有效。如果能这样,该有多大的用处,尤其是对于函数参数的确认。
显然,我们可以把这三个动作封装在三个不同的调试界面中:
/*
为新分配的内存块建立一个内存记录 */
flag fCreateBlockInfo(byte* pbNew, size_t
sizeNew);
/* 释放一个内存块对应的日志信息 */
void FreeBlockInfo(byte* pb);
/*
修改现有内存块对应的日志信息 */
void UpdateBlockInfo(byte* pbOld, byte* pbNew,
size_t sizeNew);
21、建立详尽的子系统检查并且经常地进行这些检查
22、非确定性原理
23、仔细设计程序的测试代码,任何选择都应该经过考虑
24、努力做到透明的一致性检查
25、不要把对交付版本的约束应用到
相应的调试版本上,要用大小和速度来换取错误检查能力
------------------------------------------------------------------------------------------------------------
26、不要等到出了错误再对程序进行逐条的跟踪
27、对每一条代码路径进行逐条的跟踪
28、当对代码进行逐条跟踪时,要密切注
视数据流。
对代码进行逐条跟踪的真正作用是它可以使我们观察到数据在函数中的流动。如果在对代码进行逐条跟踪时密切地注视数据流,就会帮助你查出
下面这么多的错误:
1) 上溢和下溢错误;
2) 数据转换错误;
3) 差 1 错误;
4) NULL
指针错误;
5) 使用废料内存单元错误(0xA3 类错误);
6) 用 = 代替 == 的赋值错误;
7)
运算优先级错误;
8) 逻辑错误。
29、源级调试程序可能会隐瞒执行的细节,对关键部分的代码要进行汇编指令级的逐条跟踪
30、要使用户不容易忽视错误情况,不要在正常地返回值中隐藏错误代码
31、要不遗余力地寻找并消除函数界面中的缺陷
32、不要编写多种功能集于一身的函数,为了对参数进行更强的确认,要编写功能单一的函数
void* realloc( void* pv,
size_t size );
realloc改变先前已分配的内存块的大小,该内存块的原有内容从该块的开始位置到新块和老块长度的最小长度之间得到保留。
1) 如果该内存块的新长度小于老长度,realloc释放该块尾部不再想要的内存空间,返回的pv不变。
2) 如果该内存块的新长度大于老长度,扩大后的内存块有可能被分配到新的地址
处,该块的原有内容被拷贝到新的位置。返回的指针
指向扩大后的内存块,并且
该块扩大部分的内容未经初始化。
3) 如果满足不了扩大内存块的请求,realloc返回NULL,当缩小内存块时,realloc总会成功。
4) 如果pv为NULL,那么realloc的作用相当于调用malloc(size),并返回指向新分配内存块的指针,或者在该请求无法满足时返回 NULL。
5) 如果pv不是NULL,但新的块长为零,那么realloc的作用相当于调用 free(pv)并且总是返回NULL。
6) 如果pv为NULL且当前的内存块长为零,结果无定义
33、不要模棱两可,要明确地定义函数的参数
34、编写函数使其在给定有效的输入情况下不会失败
35、使程序在调用点明了易懂;要避免布尔参数
36、编写注解突出可能的异常情况
----------------------------------------------------------------------------------------------
37、使用有严格定义的数据类型。为了避免将来的重复劳动,最好写可移植代码。尽量用可移植的数据类型
可移植类型最值得注意之处是:它们只考虑了三种最通用的数制:壹的补码、贰的补码和有符号的数值。
38、经常反问:“这个变量表达式会上溢或下溢吗?”
39、尽可能精确地实现设计,近似地实现设计就可能出错
40、一个“任务”应一次完成
41、避免无关紧要地 if语句
42、避免使用嵌套的“?:“运算符
43、每种特殊情况只能处理一次
44、避免使用有风险的语言惯用语
45、不能毫无必要地将不用类型地操作符混合使用,如果必须将不同类型地操作符混合使用,就用括号把它们隔离开来。
程序员可能清楚知道各类操作符的优先级,但是在他们混合使用各类操作符时,很容易出现问题。因此,第一条原则是:如果有可能,就不要把不同类型的操作符混合使用。第二条原则是:如果必须将不同类型操作符混合使用,就用括号把它们隔离开来。
46、避免调用返回错误的函数
--------------------------------------------------------------------------------------------------------------
47、只引用属于你自己的存储空间
48、只有系统才能拥有空闲的存储区,程序员不能拥有
49、指向输出的指针不是指向工作空间缓冲区的指针
50、不要利用静态(或全局)量存储区传递数据
51、不要写寄生函数
52、不要滥用程序设计语言
53、紧凑的C代码并不能保证得到高效的机器代码
54、为一般水平的程序员编写代码
------------------------------------------------------------------------------
55、错误几乎不会“消失”
56、马上修改错误,不要推迟到最后
57、修改错误要治本,不要治表
58、除非关系产品的成败,否则不要整理代码
59、不要实现没有战略意义的特征
60、不设自由特征
61、不允许没有必要的灵活性
62、在找到正确的解法之前,不要一味地“试”,要花时间寻求正确的
63、尽量编写和测试小块代码。即使测试代码会影响进度,也要坚持测试代码
64、测试代码的责任不在测试员身上,而是程序员自己的责任
65、不要责怪测试员发现了你的错误
66、建立自己优先级列表并坚持之,比如:
约克的优先级列表 吉尔的优先级列表
正
确性 正确性
全局效率 可测试性
大
小 全局效率
局部效率 可维护性 / 明晰性
个人方便性 一致性
可维护性 / 明晰性 大小
个人表达方
式 局部效率
可测试性 个人表达方式
一致性
个人方便性
1) 我怎样才能自动检测出错误?
2) 我怎样才能防止错误?
3)
这种想法和习惯是帮助我编写无错代码呢还是妨碍了我编写无错代码?
----------------------------------------------------------------------------------
67、决不允许同样错误出现两次
最重要的是,自己要坚持建立一个查错表列出你查出的错误,以避免重犯以前犯过的错误。这个表中的某些项也许会使你大吃
一惊。
----------------------------------------------------------------------------------
一
般问题
── 你是否为程序建立了DEBUG版本?
── 你是否将发现的错误及时改正了?
─一
你是否坚持彻底测试代码.即使耽误了进度也在所不惜?
── 你是否依靠测试组为你测试代码?
─一 你是否知道编码的优先顺序?
─
一 你的编译程序是否有可选的各种警告?
关于将更改归并到主程序
─一 你是否将编译程序的警告(包括可选的)都处理了?
──
你的代码是否未用Lint
─一 你的代码进行了单元测试吗?
─一 你是否逐步通过了每一条编码路径以观察数据流?
─一
你是否逐步通过了汇编语言层次上的所有关键代码?
── 是否清理过了任何代码?如果是,修改处经过彻底测试了吗?
─一
文档是否指出了使用你的代码有危险之处?
── 程序维护人员是否能够理解你的代码?
每当实现了一个函数或子系统之时
─
一 是否用断言证实了函数参数的有效性?
─一 代码中是否有未定义的或者无意义的代码?
─一 代码能否创建未定义的数据?
─
一 有没有难以理解的断言?对它们作解释了没有?
─一 你在代码中是否作过任何假设?
─一 是否使用断言警告可能出现的非常情况?
─
一 是否作过防御性程序设计?代码是否隐藏了错误?
─一 是否用第二个算法来验证第一个算法?
─一
是否有可用于确认代码或数据的启动(startup)检查?
─一 代码是否包含了随机行为?能消除这些行为吗?
──
你的代码若产生了无用信息,你是否在 DEBUG 代码中也把它们置为无用信息?
── 代码中是否有稀奇古怪的行为?
──
若代码是子系统的一部分,那么你是否建立了一个子系统测试?
── 在你的设计和代码中是否有任意情况?
──
即使程序员不感到需要,你也作完整性检查吗?
── 你是否因为排错程序太大或太慢,而将有价值的 DEBUG 测试抛置一边?
──
是否使用了不可移植的数据类型?
─一 代码中是否有变量或表达式产生上溢或下溢?
──
是否准确地实现了你的设计?还是非常近似地实现了你的设计?
── 代码是否不止一次地解同一个问题?
──
是否企图消除代码中的每一个 if 语句?
── 是否用过嵌套?:运算符?
── 是否已将专用代码孤立出来?
──
是否用到了有风险的语言惯用语?
─一 是否不必要地将不同类型的运算符混用?
── 是否调用了返回错误的函数?你能消除这种调用吗?
─一 是否引用了尚未分配的存储空间?
─一 是否引用已经释放了的存储空间?
── 是否不必要地多用了输出缓冲存储?
──
是否向静态或全局缓冲区传送了数据?
── 你的函数是否依赖于另一个函数的内部细节?
── 是否使用了怪异的或有疑问的 C
惯用语?
── 在代码中是否有挤在一行的毛病?
── 代码有不必要的灵活性吗?你能消除它们吗?
─一
你的代码是经过多次“试着”求解的结果吗?
─一 函数是否小并容易测试?
每当设计了一个函数或子系统后
─一
此特征是否符合产品的市场策略?
─一 错误代码是否作为正常返回值的特殊情况而隐藏起来?
─一
是否评审了你的界面,它能保证难于出现误操作吗?
─一 是否具有多用途且面面俱到的函数?
─一
你是否有太灵活的(空空洞洞的)函数参数?
─一 当你的函数不再需要时,它是否返回一个错误条件?
─一 在调用点你的函数是出易读?
─一 你的函数是否有布尔量输入?
修改错误之时
── 错误无法消失,是否能找到错误的根源?
─一
是修改了错误的真正根源,还是仅仅修改了错误的症状?
阅读(690) | 评论(0) | 转发(0) |