[珠玑之椟]浅谈代码正确性:循环不变式、断言、debug

简介:

这个主题和代码的实际写作有关,而且内容和用法相互交织,以下只是对于其内容的一个划分。《编程珠玑》上只用了两个章节20页左右的篇幅介绍,如果希望能获得更多的实例和技巧,我比较推崇《程序设计实践》 (Practise of Programming)、《编程精粹:编写高质量C语言代码》(Writing Solid Code)这两本书,只要有一般的C语言基础就能读懂,而且读起来比较快,读完后能提高不少coding的实践水平。

    目录


 循环不变式(invariant)

  循环不变式主要用来帮助理解算法的正确性,具体来看,比较针对于循环迭代。形式上很类似与数学归纳法,它是一个需要保证正确断言。对于循环不变式,必须证明它的三个性质:

初始化:它在循环的第一轮迭代开始之前,应该是正确的。

保持:如果在循环的某一次迭代开始之前它是正确的,那么,在下一次迭代开始之前,它也应该保持正确。

终止:循环能够终止,并且可以得到期望的结果。

具体的使用实例可以参考我的旧作一篇:如何写出正确的二分查找?——利用循环不变式理解二分查找及其变体的正确性以及构造方式

 


debug之脚手架

  听起来挺玄乎,其实所谓的脚手架,就是在debug版本里加入的为了验证程序是否正确的额外代码,比如一条为了确定循环中临时变量是否按期望变化的printf语句,这比复杂的调试器更快。我相信很多人在写代码时都这样做过,看到这里,我们并不需要为自己过去“简陋”的调试方式而不安,而是继续合理地使用它。

  脚手架能完成更多的工作,不仅限于变量追踪。利用断言,可以建立脚手架进行程序的自动测试而不是人为的追踪,也可以利用clock()建立脚手架,把待测的代码放在两个clock()中间测试运行时间(相比之下,我更喜欢用gettimeofday())。以上三种脚手架的用法是《编程珠玑》提到的例子,关于这种代码可以做更多的引申,以下内容结合了Practise of Programming和Writing Solid Code的相关内容。

1.#ifdef DEBUG

  很多人都使用过下面这样的代码:

#ifdef DEBUG
    ...
#endif

  这种想法是同时维护调试和非调试(即交付)两个版本,在调试版本中自动地查错;最后提交交付版本。当然,这种方法的关键是保证调试代码不在最终产品中出现。只是这种代码在原来的代码里看起来十分突兀(排版不好看,而且可能看上去喧宾夺主),使用断言assert()是一个代替方案之一,它只在定义了DEBUG时才有效,在我的Ubuntu的assert.h里是如果定义了NDEBUG就无效。关于断言会在后面详写,这里点到为止。

2.外壳函数/包裹函数

  做法是把待测试或者可能发生错误而需要检测的函数用一段代码加一个外壳,或者说是包裹起来,在其中增加出错检查和处理代码,并提供合理的返回值。这两种名称前者出自《程序设计实践》,后者见于UNP,举一个UNP上的简单的例子:

复制代码
int Socket(int family, int type, int protocol)
{
    int n;
    if( (n = socket(family,type,protocol)) < 0)
        err_sys("socket error");
    return n;
}
复制代码

3.用不同的算法验证正确性

   如果编写了一个较快的算法又担心其不正确,想要检查其正确性怎么办?在脚手架中构造一个包括能提供同样功能并且正确但速度较慢的算法(往往是旧版本中所留下的),比较两者的执行结果。

4.提高代码覆盖度

  在if判断中,总有一部分代码未必执行。如果想要测试不常执行的代码的正确性,可以用脚手架强制执行这部分代码。

5.消除随机性,使错误重现

  利用脚手架对分配的内存块用garbage值0xA3而不是0填充,这样当发现指针指向0xA3A3或内容是连续的0xA3A3时,显然是个未定义的值,需要排错。具体的取值和系统有关,0xA3是早期macintosh的建议用的garbage值。

6.不要担心脚手架带来的性能损失

  正如Writing Solid Code所言,不要把对交付版本的约束应用到相应的调试版本上,要用大小和速度来换取错误检查能力。脚手架只是为了查错,在交付版本中它们是不存在的。

 


 

断言(assert)

   正如在脚手架中提到的,断言可以对程序正确性的测试。除此以外,在一段小型的代码demo中,编写断言远比精心编制一套完整的出错处理机制或者繁复的#ifdef DEBUG要简单的多。

  为了帮助理解这个宏,Writing Solid Code探讨了assert宏的一种实现机制:

复制代码
#ifdef DEBUG
    void _Assert(char*, unsigend);
#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();
}
复制代码

  为什么要用宏定义+函数实现,并将宏中的__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.5的完善解答。

 

 

“珠玑之椟”系列简介与索引

往期回顾:

    位向量/位图的定义和应用

    估算的应用与Little定律

    随机数函数取样与概率






本文转自五岳博客园博客,原文链接:http://www.cnblogs.com/wuyuegb2312/p/3133758.html,如需转载请自行联系原作者

目录
相关文章
|
3月前
在代码优化过程中,常见的错误和bug包括以下几点
在代码优化过程中,常见的错误和bug包括以下几点
|
21天前
|
程序员 编译器 C++
【实用编程技巧】不想改bug?初学者必须学会使用的报错函数assert!(断言函数详解)
【实用编程技巧】不想改bug?初学者必须学会使用的报错函数assert!(断言函数详解)
29 2
|
9月前
|
测试技术
04-测试用例设计方法-判定表法
04-测试用例设计方法-判定表法
feof用法重点详解(易被误用判断文件结束!!!)
feof用法重点详解(易被误用判断文件结束!!!)
|
测试技术
测试中进行断言是什么意思?底层原理是什么?
测试中进行断言是什么意思?底层原理是什么?
1010 0
|
XML 前端开发 安全
【全网最全】JSR303参数校验与全局异常处理(从理论到实践别用if判断参数了)
【全网最全】JSR303参数校验与全局异常处理(从理论到实践别用if判断参数了)
123 0
【全网最全】JSR303参数校验与全局异常处理(从理论到实践别用if判断参数了)
|
测试技术
软件测试面试题:SoftAssert的特点,并对软断言进行举例说明?
软件测试面试题:SoftAssert的特点,并对软断言进行举例说明?
81 0
|
XML 存储 Java
Java Review(三十三、异常处理----补充:断言、日志、调试)
Java Review(三十三、异常处理----补充:断言、日志、调试)
149 0
|
存储 监控
关于bug分析与异常处理的一些思考
我将bug依据复现的难易程度分为:必现的bug,比较容易复现的bug,很难复现的bug。 对于必现的bug,我通常淡定地称为其不是bug,因为,通过不断地复现,不断地调试,这些bug通常都能被解决,被解决了,还是bug么? 对于比较容易复现的bug,所谓比较容易复现,就是通过不太复杂操作,尝试几次、十几次,现象就可出现的bug,因为复现操作变得复杂,所以,为了每次复现能够获得更多的信息,尽量多地增加调试信息,以期望问题复现后,极大地缩小问题原因的范围。
1105 0
|
Java 测试技术 程序员
第七章 异常, 断言和日志
处理错误 出现错误时程序应该(1)返回安全状态并允许用户执行一些其他命令; (2)或者允许用户保存所有操作的结果,并以妥善方式退出程序; 错误产生的原因:(1)用户输入错误;(2)设备错误;(3)物理限制;(4)代码错误: 有些情况下代码的调用方并不能根据程序的错误返回码进行处理, 这时就需要抛出...
1007 0