编程精粹--编写高质量的C语言代码(2):自己设计并使用断言(一)

上一篇文章<<编程精粹--编写高质量C语言代码(1):假想编译程序>>中讲述了如何利用编译程序的所有警告设施以及lint程序等来更加容易地自动发现程序中的错误。但是即使使用编译程序提供的所有警告设施,编译程序所发现的错误,也只是程序错误中的一小部分。例如以下一行代码:

  strCopy=memecpy(malloc(length),str,length));

当malloc 调用失败时,返回一个空指针,而memcpy如果没有处理空指针,程序就会出现错误。编译程序是无法查出这种或其他类似的错误。同样编译程序也无法查出算法的错误,无法验证程序员的假设。编译程序也查不出传递的参数是否有效。那如何自动寻找出这些错误呢?接着上面那个例子,最初的解决办法就是在memcpy对NULL指针进行检查。

/* memcpy:拷贝不重叠的内存块 */
void memcpy(void *pvTo, void *pvFrom, size_t size)
{
   byte* pbTo=(byte*)pvTo;
   byte* pbFrom=(byte*)pvFrom;
   if(NULL==pbTo||NULL==pbFrom)
   {
   	  fprintf(stderr,"bad args in memcpy\n");
   	  abort();
   }
   while(size-->0)
     *pbTo++=*pbFrom++;
}

当空指针调用memcpy时,函数就会检查出这个错误,并打印出错误信息。可是这样编写代码带来两个问题:1,测试空指针的代码增加了函数的代码量;2,降低了函数的执行速度。

我们应当知道,当程序正常运行时,是不应该出现把空指针传给memcpy函数的。但是当程序还处于调试过程中,这种情况是会出现的。所以应该保存两个版本,一个整洁快速用于程序的交付,另一个臃肿缓慢(因外需要包过额外的检查),用于调试。所以应当维护程序的两个版本,并利用C语言的条件编译,有条件的包含或不包含相应的检查部分。

/* memcpy:拷贝不重叠的内存块 */
void memcpy(void *pvTo, void *pvFrom, size_t size)
{
   byte* pbTo=(byte*)pvTo;
   byte* pbFrom=(byte*)pvFrom;
   #ifdef DEBUG
   if(NULL==pbTo||NULL==pbFrom)
   {
   	  fprintf(stderr,"bad args in memcpy\n");
   	  abort();
   }
   #endif
   while(size-->0)
     *pbTo++=*pbFrom++;
}

这样就同时维护了两个版本,在调试时,编译其调试版本,这样就会包含空指针的测试代码,便于自动差错,而在程序编写完成后,编译其交付版本,保证最终产品中不会包含调试代码,保证了程序的整洁快速。

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

上面的memcpy的调试代码略显复杂,聪明的程序员会把所有的调试代码隐藏在断言assert中。assert是个宏,定义在assert.h中。虽然assert 只不过是#ifdef 部分代码的替换,但是使用assert宏使代码更简洁。

/* memcpy:拷贝不重叠的内存块 */
void memcpy(void *pvTo, void *pvFrom, size_t size)
{
   byte* pbTo=(byte*)pvTo;
   byte* pbFrom=(byte*)pvFrom;
   assert(pbTo!=NULL&&pbFrom!=NULL);
   while(size-->0)
     *pbTo++=*pbFrom++;
}

assert是个只有定义了DEBUG才会起作用的宏,所以是用于程序的调试,而程序的最终产品是不会包含assert的代码。正是因为assert只会在程序的调试版本中起作用,所以为了避免程序的交付版本和调试版本之间引起重要的差别,需要对assert宏进行仔细定义。assert宏不应该弄乱内存,不应该对未初始化的数据进行初始化。也就是说assert宏不应该有其它副作用。如果assert的参数计算结果为假,就会终止程序的执行。而且还要意识到,一旦程序员学会了断言,就常常会对宏assert进行重定义。这样就可以自定义assert宏的行为。

不管断言宏最终以什么样的方法定义,都要使用它来对传递的函数参数进行确认。断言宏的最好作用是使用户在发生错误时,就可以自动地把它们检查出来。

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

在ANSI C 中,如果是对两个存储空间相互重叠的对象间进行拷贝,memcpy的结果是未定义的。结果未定义就意味着不同的编译程序,其结果也可能不同。因此对于无定义的特性,我们可以通过断言来对其进行确认。

void* memcpy(void *pvTo,void *pvFrom,size_t size)
{
	byte *pbTo=(byte*)pvTo;
	byte *pbFrom=(byte*)pvFrom;
	ASSERT(pbTo!=NULL&&pbFrom!=NULL);
	ASSERT(pbTo>=pvFrom+size||pbFrom>=pbTo+size);
	while(size--)
	   *pbTo++=*pvFrom++;
    return pvTo;
} 

代码中只利用了ASSERT(pbTo>=pvFrom+size||pbFrom>=pbTo+size),就可以完成两个内存块是否重叠。所以如果程序中使用了无定义的特性就要把它从相应的设计里去掉,或者在程序中包含相应的断言,以便在使用了无定义的特性,能够向程序员通报。

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

还有一点需要注意,当我们千辛万苦地跟踪到一个断言时,却有可能不知道该断言的作用。所以为了使程序员理解断言的意图,要给不够清楚的断言加上注解。

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

当程序员刚开始使用断言时,有时会错误地利用断言去检查真正地错误,而不是去检查非法的状况。

char* strdup(char *str)
{
	char *strNew;
	ASSERT(str!=NULL);
	strNew!=malloc(strlen(str)+1);
	ASSERT(strNew!=NULL);
	strcpy(strNew,str);
	return strNew;
}

第一个断言是正确的,因为它用来检查程序正常运行时绝对不应该发生的非法状况,而第二个断言的用法是不当的,它所测试的是错误状况,程序运行时的确可能会出现内存分配失败,我们应该在最终产品中对这种错误进行处理而不是使用断言。

对于程序中使用的假定,要使用断言和条件编译进行相应的验证,例如:有时候我们会不自觉的认为一个字节占8位,或者说一个long型占据4个字节,这些都是对编译程序或操作系统做的一些假定。这使得我们需要在程序中使用断言例如ASSERT(sizeof(long)==4&&CHAR_BIT==8)。

消除所做的隐式假定,或者利用断言检查其正确性。

总结:

1,同时维护程序的调试版本和交付版本,封装交付版本,应尽可能地利用调试版本自动查错。

2,断言是进行调试检查的简单方法。要使用断言来检查绝对不应该发生的非法情况,不要混淆非法情况和错误情况,错误情况是需要在最终产品中处理的。

3,利用断言对函数的参数进行确认,并且当程序员使用了无定义特性时向程序员报警。

4,当编写函数时,应反复问自己做了哪些假定,一旦确定了相应的假定,就要使用断言对所做的假定进行检验,或者重新编写代码去除假定。

最后用作者在这章中的一句话结束这篇文章:

如果我告诉大家出现了断言失败是件好事,也许这个程序员就不会这么惊慌了。

编程精粹--编写高质量的C语言代码(2):自己设计并使用断言(一)

时间: 2024-12-27 03:52:33

编程精粹--编写高质量的C语言代码(2):自己设计并使用断言(一)的相关文章

编程精粹--编写高质量C语言代码(4):为子系统设防(一)

通常,子系统都要对其实现细节进行隐藏,在进行细节隐藏的同时,子系统为用户提供了一些关键入口点.程序员通过调用这些关键的入口点来实现与子系统的通信.因此如果在程序中使用这样的子系统并且在其调用点加上了调试检查,那么不需要花费多少力气就可以进行许多错误检查. 当子系统编写完成后,要问自己:"程序员什么情况下会错误地使用这个子系统,在这个子系统中怎样才能自动检查出这些问题?"在这篇文章中,将讲述一些用来肃清子系统中错误的技术.使用这些技术,可以免除许多麻烦.本章将以C的内存管理程序为例,但所

编程精粹--编写高质量C语言代码(6):对程序进行逐条跟踪

发现程序错误最好的方法就是执行程序.在程序执行过程中,我们利用我们的眼睛,或者通过我们编写的断言和子系统一致性检查等自动测试的工具来发现错误.虽然断言和子系统检查都很有用,但是如果程序员事先没有想到应该对某些问题进行检查,那么也就无法保证程序没有问题. 程序员可以在代码中设置断点,一步步跟踪代码的运行,观察输入变为输出的过程.程序员测试其程序最好的方法就是对程序进行逐条跟踪,对中间的结果进行认真的查看.对代码进行逐条跟踪是需要时间的,但它同编码比,只是一小部分.一旦逐条地跟踪代码成为习惯后,我们

编程精粹--编写高质量C语言代码(3):自己设计并使用断言(二)

接着上一遍文章<<编程精粹--编写高质量C语言代码(2):自己设计并使用断言(一)>>,继续学习如何自己设计并使用断言,来更加容易,更加不费力地自动寻找出程序中的错误. 首先看一个简单的压缩还原程序: byte* pbExpand(byte *pbFrom,byte *pbTo,size_t sizeFrom) { byte b, *bpEnd; size_t size; pbEnd=pbFrom+sizeFrom; while(pbFrom<pbEnd) { b=*pbFr

编程精粹--编写高质量C语言代码(1):假想编译程序

编译程序仅仅能查找出程序的语法错误,而对于"数组越界访问","对空指针解引用"等错误,编译程序是束手无策的.同时我们知道测试人员所使用的黑箱测试方法所能做的只是往程序里填数据,并看它弹出什么.这就决定了对程序错误的检测可能需要点运气. 假如编译程序能够检测出"数组越界访问","差一错误","空指针"等等错误,那么编写无错代码其实就要简答多了. 所以我们需要一个思维转变: 不要光依赖黑箱测试方法,还应该试着去

借助 SublimeLinter 编写高质量的 JavaScript &amp; CSS 代码

SublimeLinter 是前端编码利器——Sublime Text 的一款插件,用于高亮提示用户编写的代码中存在的不规范和错误的写法,支持 JavaScript.CSS.HTML.Java.PHP.Python.Ruby 等十多种开发语言.这篇文章介绍如何在 Windows 中配置 SublimeLinter 进行 JS & CSS 校验. 准备工作 安装 Sublime Text 包管理工具:http://wbond.net/sublime_packages/package_control

如何编写高质量的 JS 函数(4) --函数式编程[实战篇]

本文首发于 vivo互联网技术 微信公众号? 链接:https://mp.weixin.qq.com/s/ZoXYbjuezOWgNyJKmSQmTw 作者:杨昆 ?[编写高质量函数系列],往期精彩内容: <如何编写高质量的 JS 函数(1) -- 敲山震虎篇>介绍了函数的执行机制,此篇将会从函数的命名.注释和鲁棒性方面,阐述如何通过 JavaScript 编写高质量的函数. ?<如何编写高质量的 JS 函数(2)-- 命名/注释/鲁棒篇>从函数的命名.注释和鲁棒性方面,阐述如何通

如何编写高质量的 JS 函数(3) --函数式编程[理论篇]

本文首发于 vivo互联网技术 微信公众号 链接:https://mp.weixin.qq.com/s/EWSqZuujHIRyx8Eb2SSidQ作者:杨昆 [编写高质量函数系列]中, <如何编写高质量的 JS 函数(1) -- 敲山震虎篇>介绍了函数的执行机制,此篇将会从函数的命名.注释和鲁棒性方面,阐述如何通过 JavaScript 编写高质量的函数. <如何编写高质量的 JS 函数(2)-- 命名/注释/鲁棒篇>从函数的命名.注释和鲁棒性方面,阐述如何通过 JavaScri

每周一书-编写高质量代码:改善C程序代码的125个建议

首先说明,本周活动有效时间为2016年8月28日到2016年9月4日.本周为大家送出的书是由机械工业出版社出版,马伟编著的<编写高质量代码:改善C程序代码的125个建议>. 编辑推荐 10余年开发经验的资深C语言专家全面从C语法和C11标准两大方面深入探讨编写高质量C代码的技巧.禁忌和实践 C语言因为既具有高级语言特性,又具有汇编语言特性,所以它是近二十几年来使用较为广泛.生命力较强的编程语言.无论是操作系统.嵌入式系统.普通应用软件,还是移动智能设备开发,它都能够很好地胜任,是公认的强大的语

[ 转 ]编写高质量代码:改善Java程序的151个建议

记得3年前刚到公司,同桌同事见我无事可做就借我看<编写高质量代码:改善Java程序的151个建议>这本书,当时看了几页没上心就没研究了.到上个月在公司偶然看到,于是乎又找来看看,我的天,真是非常多的干货,对于我这种静不下心的人真是帮助莫大呀. 看完整本书,也记了不少笔记,我就分享一部分个人觉得有意义的内容,也为了方便以后自己温习. --警惕自增陷阱 i++表示先赋值后自增,而++i表示先自增后赋值.下面的代码返回结果为0,因为lastAdd++有返回值,而返回值是自增前的值(在自增前变量的原始