目录

微软C编程

每条准则都有例外

  • 准则是用来说明一般情况的,当理由十分充足时,才可以违背准则

假想编译程序

-> line 23: while (i<=j)
off by one error: this should be '<'

-> line 42: int itoa(int i, char* str)
algorithm error: itoa fails when i is -32768

-> line 318: strCopy = memcpy(malloc(length), str, length);
Invalid argument: memcpy fails when malloc returns NULL
  • 把自己当作一个假设可以发现任何错误的编译程序,去运行程序,从而发现潜伏的错误

使用编译程序所有的可选警告设施

使用静态分析程序

Lint (在linux上是Splint)

进行单元测试

  • 哪怕只是移动下代码的位置,也需要运行下单元测试

  • 消除程序错误的最好方法是尽可能早、尽可能容易地发现错误,要寻求费力最小的 自动查错方法。

// 条件里进行赋值
while(ch = getchar() != EOF)

// 八进制表示法 使 063 实际含义是 51
if(flight == 063)

// 将 && 写成 &
if(pb != NULL & pb != 0xff)

// / 和 * 之间没有空格 变成注释的开始
quot = numer/*pdenom;
*/

// 运算符优先级错误 , 实际执行的是,bHigh << (8+bLow)
word = bHigh << 8 + bLow;

// 预处理器带来的意想不到的错误

为了防止传参错误

void memcpy(void* pvTo , void* pvFrom , size_t size){
    if(pvTo == NULL | | pvFrom == NULL)
    {
        fprintf(stderr, “Bad args in memcpy\n”);
        abort();
    }
}

既要维护程序的交付版本,又要维护程序的调试版本

要同时维护交付和调试两个版本。封装交付的版本,应尽可能地使用调试版本进行 自动查错。

void memcpy(void* pvTo , void* pvFrom , size_t size){
    #ifdef DEBUG
    if(pvTo == NULL | | pvFrom == NULL)
    {
        fprintf(stderr, “Bad args in memcpy\n”);
        abort();
    }
    #endif
}

要使用断言对函数参数进行确认

void memcpy(void* pvTo , void* pvFrom , size_t size){
    assert(pvTo != NULL && pvFrom != NULL);
}
  • aasert 是个只有定义了 DEBUG 才起作用的宏,如果其参数的计算结果为假,就中止调 用程序的执行。因此在上面的程序中任何一个指针为 NULL 都会引发 assert

  • 宏 assert 不应该弄乱内存,不应该对未初始化的 数据进行初始化,即它不应该产主其他的副作用。正是因为要求程序的调试版本和交付版本 行为完全相同,所以才不把 assert 作为函数,而把它作为宏。如果把 assert 作为函数的话, 其调用就会引起不期望的内存或代码的兑换。

  • 错误分两种,一种是程序正常工作时绝对不应该发生的,另一种是在某些外界条件下会发生的,对于前一种,我们使用断言,对于后一种,我们要自己捕获这个错误,并且处理它,比如在打开文件不存在的情况,我们要提示用户文件不存在

  • 断言是进行调试检查的简单方法。要使用断言捕捉不应该发生的非法情况。不要混 淆非法情况与错误情况之间的区别,后者是在最终产品中必须处理的。

不要对程序作隐式假定,而是利用断言检查其正确性

  • 使用断言对函数的参数进行确认,并且在程序员使用了无定义的特性时向程序员报警。函数定义得越严格,确认其参数就越容易。

  • 在编写函数时,要进行反复的考查,并且自问:“我打算做哪些假定?”一旦确定 了相应的假定,就要使用断言对所做的假定进行检验,或者重新编写代码去掉相应 的假定。另外,还要问:“这个程序中最可能出错的是什么,怎样才能自动地查出 相应的错误?”努力编写出能够尽早查出错误的测试程序。

要从程序中删去无定义的特性 或者在程序中使用断言来检查出无定义特性的非法使用

  • 同样的代码,在不同的编译器下,产生的效果不一样,这种代码是要极力避免的

不要浪费别人的时间 ─── 详细说明不清楚的断言

  • 就是使用注释说明代码做什么用的意思

在进行防错性程序设计时,不要隐瞒错误

  • 防错性程序设计会隐瞒错误

  • 一个程序所能导致 的最坏结果是执行崩溃,并使用户可能花几个消失建立的数据全部丢掉。在非理想的世界中, 程序确实会瘫痪,因此为了防止用户数据丢失而参去的任何措施都是值得的。防错性程序设计要实现的就是这个目标。

  • 我们还希望在进行防错性程序设计时, 错误不要被隐瞒。

  • 一般教科书都鼓励程序员进行防错性程序设计,但要记住这种编码风格会隐瞒错 误。当进行防错性编码时如果“不可能发生”的情况确实发生了,要使用断言进行报警。

要利用不同的算法对程序的结果进行确认

不要等待错误发生,要使用初始检查程序

  • 出现了断言失败是件好事,也许这个程序员就不会那么惊慌 了。然而,对错误感到惊慌的并不止程序员

在子程序调用处添加调试点

  • 子系统都要对其实现细节进行隐藏,所隐藏的实现细节可能相当复杂。在进行实 现细节隐藏的同时,子系统为用户提供了一些关键的入口点。程序员通过调用这些关键的入 口点来实现同子系统的通讯。因此如果在程序中使用这样的子系统并且在其调用点加上了调试检查,那么不用花很大力气就可以进行许多的错误检查。

内存管理程序思考

  • 分配一个内存块并使用其中未经初始化的内容;

  • 释放一个内存块但继续引用其中的内容;

  • 调用realloc对一个内存块进行扩展,因此原来的内容发生了存储位置的变化,但程序引用的仍是原来存储位置的内容;

  • 分配一个内存块后即“失去”了它,因为没有保存指向所分配内存块的指针;

  • 读写操作越过了所分配内存块的边界;

  • 没有对错误情况进行检查。

要消除随机特性 ─── 使错误可再现

  • 暴露错误的关键是消除错误发生的随 机性。

外壳函数:

void FreeMemory(void* pv){
    ASSERT(pv != NULL);
    #ifdef DEBUG
    {
        memset(pv, bGarbage, sizeofBlock(pv) );
    }
    #endif
    free(pv);
}
  • 该函数中的调试代码不仅对所释放内存块的内容进行了废料填充,而且在调用sizeofBlock 时,还顺便对 pv 进行了确认。如果该指针不合法,就会被 sizeofBlock 查出

  • 利用外壳函数,对一些重要的调用进行封装,从而记录它们的调用返回,或者处理参数

如果某件事甚少发生的话,设法使其经常发生

  • 对于可能会产生特殊情况的代码,应该在调试代码中,让这种特殊情况强制出现,然后去测试