9.2 如何设计一种灵活的断言

在实际应用时,可能需要根据具体情况灵活地设计断言,所以我们对断言的认识就不能仅停留在使用上面,应该学会设计自己的断言代码,以便在代码中灵活地使用断言。接下来就介绍如何自己在代码中编写断言,先来看下面断言的实现代码。


include<stdio.h>

include<stdlib.h>

//#define NDEBUG

ifndef NDEBUG

define assert(p)if(!(p))\

{\

fprintf(stderr,"Assertion failed:%s,file%s,

line%d\n",#p,FILELINE);\

abort();\

}

else

define assert(p)(void)(0)

endif

int sum(int a[],int n)

{

assert(n>0);

int i;

int sum=0;

for(i=0;i<n;i++)

{

sum+=a[i];

}

return sum;

}

int main()

{

int i=0;

int total;

int arr[]={2,3,4,7};

total=sum(arr,4);

printf("数组中的元素之和为:%d\n",total);

total=sum(arr,0);

return 0;

}


运行结果:


数组中的元素之和为:16

Assertion failed:n>0,file E:\fdsa\fdsag.cpp,line 16


以上代码中断言的实现方法是采用条件编译指令,这是为了在程序中可以开启或者关闭断言。下面来看开启断言的实现方法。


define assert(p)if(!(p))\

{\

fprintf(stderr,"Assertion failed:%s,file%s,line%d\n",#p,__

FILELINE__);\

abort();\

}


以上代码采用fprintf函数实现打印输出,其中,输出设备是stderr,同时在参数中使用了宏FILELINE,用于输出当前出错的文件名及行号。参数中还有一个“#p”,关于形参的使用在第2章“预处理”中已经进行了相应的讲解,读者可以查阅相关内容。再来看关闭断言的实现部分,这里使用的方法之前也介绍过,如果删掉代码中的“||”,关闭断言,那么宏assert(p)可用(void)(0)来替代,这时宏在代码中就像被清除了一样,不会有任何的作用。再来看以上代码中对宏的使用,通过宏来检测函数参数传递是否满足要求。

看上面采用VC++6.0编译实现断言的方法,输出信息中只包含文件名和行号,如果在Linux环境下采用gcc编译运行,那么可以得到更加具体的信息。例如,可以通过宏func获取出错位置的函数名,实现方法如下:


include<stdio.h>

include<stdlib.h>

//#define NDEBUG//禁用

ifndef NDEBUG//启用断言测试

void assert_report(const charfile_name,const charfunction_name,unsigned int line_no)

{

printf("file_name:%s,function_name:%s,line%u\n",

file_name,function_name,line_no);

abort();

}

define assert(condition)\

do{\

if(condition)\

NULL;\

else\

{\

printf("Error Report:%s",#condition);\

assertreport(FILEfunc_LINE);\

}\

}while(0)

else

define assert(condition)NULL

endif

int main(void)

{

int i;

i=0;

assert(i++);

printf("%d\n",i);

return 0;

}


运行结果:


root@ubuntu:/home#gcc assert.c-o assert

root@ubuntu:/home#./assert

Error Report:i++file_name:assert.c,function_name:main,line 33

Aborted


分析上面的运行结果,得到的出错信息包括文件名、函数名、行号,有了这些信息可以对出错位置进行快速定位。在断言的实现中使用了abort()函数,如果条件为假,那么就打印出错信息,同时运行abort函数终止程序的运行。

仔细阅读代码,会发现在宏定义中使用了一个do{}while(0)。使用它有什么好处呢?或许从以上代码中看不出来,那么看下面的代码就知道了。


include<stdio.h>

void print_1(void)

{

printf("成功地调用print_1函数\n");

}

void print_2(void)

{

printf("成功地调用print_2函数\n");

}

define printf_value()\

print_1();\

print_2();

int main(void)

{

int i=0;

if(i==1)

printf_value();

return 0;

}


运行结果:


成功地调用print_2函数


看了上面的运行结果,可能有的读者会疑惑为什么会出现这样的错误。如果if语句的条件不满足,那么print_value()函数应该不会被调用,怎么会出现打印结果呢?如果把上面的“printf_value()”替换为“print_1();print_2();”,就会很清楚地发现if语句在此处的作用仅仅是限制不调用“print_1();”,而“print_2();”在控制之外,所以出现了上面的结果。有的读者可能会马上想到加上括号{}就好了,这里加上括号{}的确就可以了,因为这里是一种特殊情况,即没有else语句。如果在宏定义中使用{}并加入else语句,结果会如何呢?看下面一段代码。


include<stdio.h>

void print_1(void)

{

printf("成功地调用print_1函数\n");

return;

}

void print_2(void)

{

printf("成功地调用print_2函数\n");

return;

}

define printf_value()\

{\

print_1();\

print_2();}

int main(void)

{

int i=0;

if(i==1)

printf_value();

else

printf("add else word!");

return 0;

}


运行结果:


error C2181:illegal else without matching if


看似正确的代码,编译的时候就会出现错误。为什么会出现这样的错误呢?因为在编写C语言代码时,在每个语句后面加分号是一种约定俗成的习惯,以上代码中的printf_value()语句后面有一个分号,这个分号的作用使else没有了与之相对应的if,所以编译时会出错。如果使用do while(0)就不会出现这种问题,因此在编写代码时应该学会在宏定义中使用do{}while(0)。