1.1 基本概念
断言是一种让错误在运行时候自我暴露的简单有效实用的技术。它们帮助你较早较轻易地发现错误,使得整个调试过程效率更高。
断言是布尔调试语句,用来检测在程序正常运行的时候某一个条件的值是否总为真,它能让错误在运行时刻暴露在程序员面前。使用断言的最大好处在于,能在更解决错误的发源地的地方发现错误。断言具有以下特征:
n 断言是用来发现运行时刻错误的,发现的错误是关于程序实现方面的。
n 断言中的布尔表达式显示的是某个对象或者状态的有效性而不是正确性。
n 断言在条件编译后只存在于调试版本中,而不是发布版本里。
n 断言不能包含程序代码。
n 断言是为了给程序员而不是用户提供信息。
使用断言最根本的好处是自动发现许多运行时产生的错误,但断言不能发现所有错误。断言检查的是程序的有效性而不是正确性,可通过断言把错误限制在一个有限的范围内。当断言为假,激活调试器显示出错代码时,通常能检查出导致断言失败的原因。_ASSERTE宏(属于C运行时间库)还能在断言失败时显示出失效断言。
1.2 C中的断言(assert)
assert宏在C语言程序的调试中发挥着重要的作用,它用于检测不会发生的情况,表明一旦发生了这样的情况,程序就实际上执行错误了,例如strcpy函数:
char *strcpy(char *strDest, const char *strSrc)
{
char *address = strDest;
assert((strDest != NULL) && (strSrc != NULL));
while ((*strDest++ = *strSrc++) != ’/0’)
;
return address;
}
其中包含断言assert( (strDest != NULL) && (strSrc != NULL) ),它的意思是源和目的字符串的地址都不能为空,一旦为空,程序实际上就执行错误了,会引发一个abort。
assert宏的定义为:
#ifdef NDEBUG
#define assert(exp) ((void)0)
#else
#ifdef __cplusplus
extern "C"
{
#endif
_CRTIMP void __cdecl _assert(void *, void *, unsigned);
#ifdef __cplusplus
}
#endif
#define assert(exp) (void)( (exp) || (_assert(#exp, __FILE__, __LINE__), 0) )
#endif /* NDEBUG */
如果程序不在debug模式下,assert宏实际上什么都不做;而在debug模式下,实际上是对_assert()函数的调用,此函数将输出发生错误的文件名、代码行、条件表达式。
一定要记住的是assert本质上是一个宏,而不是一个函数,因而不能把带有副作用的表达式放入assert的"参数"中。
1.3 C中的异常终止(exit)
标准C库提供了abort()和exit()两个函数,它们可以强行终止程序的运行,其声明处于<stdlib.h>头文件中。这两个函数本身不能检测异常,但在C程序发生异常后经常使用这两个函数进行程序终止。
对于exit函数,可以利用atexit函数为exit事件"挂接"另外的函数,这种"挂接"有点类似Windows编程中的"钩子"(Hook)。程序输出"atexit挂接的函数"后即终止,即便是我们不调用exit函数,当程序本身退出时,atexit挂接的函数仍然会被执行。
atexit可以被多次执行,并挂接多个函数,这些函数的执行顺序为后挂接的先执行。
在Visual C++中,如果以abort函数(此函数不带参数,原型为void abort(void))终止程序,则会在debug模式运行时弹出错误提示的对话框。
1.4 MFC中的断言
1.4.1 ASSERT(布尔表达式)
在VC中检查变量合法性一般利用ASSERT(x)宏,ASSERT的作用在于检查表达式是否为假或为NULL,如果为假则会引发异常。在MFC中ASSERT宏被大量使用,它的优点是即使出现了WM_QUIT消息也能显示断言失效消息框。
当ASSERT失败并引发异常时会有对话框弹出并报告发生该ASSERT失败位置。并允许选择继续运行(Ignore)或是终止(Abort)程序。(当然选择继续运行是很危险的)选择Retry将会启动调试软件对程序进行调试。
此外还有一点,ASSERT宏只在调试版本中才会有作用,在调试版本中ASSERT(f)宏被展开为
do
{
if (!(f) && AfxAssertFailedLine(THIS_FILE, __LINE__))
AfxDebugBreak();
} while (0)
而在发行版本中会被展开为:
((void)0)
所以对程序内部状态改变的代码不能够放置在ASSERT宏中否则在发行版中会出现不正常的现象。
1.4.2 VERIFY(布尔表达式)
如果希望合法检查在发行版本中同样起作用则可以利用VERIFY宏,VERIFY宏简化了对函数返回值的检查,一般用来检查Windows API的返回值。VERIFY宏与ASSERT宏的不同在与VERIFY在发行版本中同样会起作用,但是使用VERIFY会导致非常不友好的用户界面,因此最好尽量不要使用这个宏以实现程序代码和调试代码的完全分离。
1.4.3 ASSERT_VALID(指向CObject派生类对象的指针)
对象的合法性检查需要根据对象自身的状态和一些对象自己的逻辑来作出判断,因此在对象外部就无法正确判断,一个省时有效的办法是在对象内部进行检查,由对象自己负责合法性检查,
MFC利用成员函数 void CObject::AssertValid() const来实现对象的合法性检查,所以新的类必须是CObject的派生类,(在MFC中几乎所有的类都由CObject派生)由于C++的多态性派生类的AssertValid函数会被正确的调用。函数定义中的const表示该函数体中不能改变成员变量的值。 我们所需要做的就是重载AssertValid,并实现对象状态合法性的检查。在AssertValid我们不但可以检查数据的正确性,也可以对数据的逻辑性进行检查。
此外MFC中定义了ASSERT_VALID宏来执行安全的对象检查,ASSERT_VALID宏会展开AfxAssertValidObject,并先检查指针的合法性。这样避免了下面的错误:
CView *pV=NULL;
pV->AssertValid();
//安全的方法是利用
ASSERT_VALID(pView);
ASSERT_VALID宏通过调用重载的AssertValid函数来确定指向CObject派生类对象的指针是否有效。无论你什么时候从CObject派生类中得到一个对象,在对这个对象做任何操作之前都应该调用ASSERT_VALID宏。
与ASSERT宏一样,ASSERT_VALID宏只在调试版本中起作用。
1.4.4 其它
ASSERT_KINDOF(类名, 指向CObject派生类对象的指针):这个宏用来验证指向CObject派生类对象的指针是否从某个特殊类中派生,在调用它之前先调用ASSERT_VALID宏。只有在很特殊的场合下才用得到,如检测编译器可能错过的对象类型问题。
MFC还有两个没有正式文件的ASSERT宏的变种:ASSERT_POINTER(指针,指针类型),ASSERT_NULL_OR_POINTER(指针,指针类型)。
1.5 什么时候使用断言
把断言看作一种简单的制造栅栏的方法,这种栅栏能使错误在穿过自己时暴露。
u 检查函数的输入
u 检查函数的输出
u 检查对象的当前状态
u 坚持逻辑变量的合理性和一致性
u 检查类中的不变量
公有成员函数比私有和保护的成员函数需要更全面的断言。
不正确地使用断言会导致错误。断言应该检测那些在程序正常运行的时候永远都不可能出现的状态。断言是用来揭示错误的,而不是用来纠正运行时刻错误的。
1.6 断言与防御性编程(Defensive Programming)
断言在调试的时候向程序员揭示运行时刻错误(调试版本里),而防御性编程使用户在运行程序(发布版本里)时,当出现意外情况时程序仍能继续工作。实际上,防御性的编程要求程序在检测到意外时返回一个“安全”的值(比如布尔函数返回false,指针和句柄返回空值),一个错误代码或者抛出一个异常来解决问题。特定的防御性编程技术包括:处理无效函数参数和数据、出现问题的时候程序失败、检查临界函数返回的错误代码以及处理异常。需要防御性编程的标准问题包括:错误的输入数据、内存或者硬盘空间不够、不能打开一个文件、外部设备不能访问、网络连接不上或者甚至在程序中还有错误,目的是保持程序的运行状态。
如果你的程序是防御性的,别忘了使用断言。如果你使用断言,也别忘了防御性编程。这两种技术最好结合在一起使用。