c#发展

首页 » 常识 » 问答 » C语言陷阱与技巧20节,自定义编译时
TUhjnbcbe - 2024/6/24 16:34:00

在C语言程序开发中,程序员写代码时应该考虑的“面面俱到”,这样才能写出功能稳定的程序。例如,在实现open()函数时,先完成它的功能固然是重要的,但是程序员还需要考虑各种“意外”,比如下面这种情况。

假设不存在/dev/sth这个文件,仍然调用open()函数打开它:

intfd=open(/dev/sth,O_RDONLY);此时open()函数不应该感到迷惑,而是具备处理这种“意外”的能力。标准库的open()函数在遇到这种情况时,会返回一个错误码,对应着“文件不存在”的错误信息。

所以我们在开发C语言程序的过程中,写出的代码也应具备这种处理“意外”的能力。处理“意外”最常用的方式之一就是返回一个错误码,输出一段错误提示信息,这一点其实之前的文章讨论过。

使用assert

在C语言程序开发阶段,为了方便,我们可以在可能出现不预期的“意外”处使用assert()。assert()的C语言原型如下:

#includeassert.hvoidassert(scalarexpression);

使用它需要包含assert.h,assert()接收一个参数expression,可以是一个表达式,如果expression为真,则什么都不会发生。如果expression为假,则assert()会终止C语言程序,并且输出assert失败的代码位置。

例如下面这段C语言代码:

intfd=open(/dev/sth,O_RDONLY);assert(fd0);printf(fd=%d\n,fd);

编译并执行,得到如下结果:

#gcct.c#./a.outa.out:t.c:11:main:Assertion`fd0failed.Aborted可以看出,第12行的printf()函数并没有被执行。这是因为程序运行环境里并没有“/dev/sth”这个文件,所以open()函数执行失败,传递给assert()的参数为假,C语言程序被终止,并且输出t.c源文件第11行代码assert失败。

assert()可以输出出错的代码位置,这个特性在较为大型的C语言程序开发中是非常好用的,因为无需程序员再去手工调试代码,排查出错代码的位置了。

不过,assert()在遇到假参数时,直接将C语言程序终止太过于死板。比如某个C语言程序有两套逻辑,第一套逻辑在open()函数成功打开文件时运行,第二套逻辑则在open()函数打开文件失败时运行。要是使用assert()判断open()函数是否成功打开文件,则第二套逻辑永远没有机会运行。

所以,assert()一般仅用于开发阶段帮助程序员定位错误,不能依赖assert()处理“意外”。事实上,为了便于使用,在定义了NDEBUG宏之后,assert()就不再生成代码了,此时assert()相当于一个空格。请看下面这段C语言代码:

#includestdio.h#includesys/types.h#includesys/stat.h#includefcntl.h#defineNDEBUG#includeassert.hintmain(){intfd=open(/dev/sth,O_RDONLY);assert(fd0);printf(fd=%d\n,fd);return0;}

编译上述C语言代码并执行,得到如下输出:

#gcct.c#./a.outfd=-1编译时assert

可以看出,assert()用于处理C语言程序可能出现诸多预期之外的“意外”时很有用,它能够自己输出究竟哪一个“意外”发生。但是assert()也是死板的,它在遇到假条件时直接把程序终止,剩余的代码逻辑不再有机会执行。

另外还有一点要说明,assert()本身也会影响C语言程序的运行效率,这也是它常常只被使用在开发阶段的另一个原因。

其实仔细想想,使用assert()的目的其实只是希望它能够在C语言程序遇到不预期的“意外”时提醒程序员,我们并不关心assert()是否参与程序运行。如果使用assert()判断的是常量表达式,那我们可以自己定义一个static_assert()宏,并且让它在编译时就判断条件表达式是否成立,这样的宏可能在某些场合更加好用。

那该如何实现编译时assert这个功能呢?

其实很简单,首先应该明白数组的长度不可能是负数,基于这一点,static_assert()宏就容易实现了,请看下面的C语言代码:

#definestatic_assert(expr)\do{chartmp[(expr)?1:-1];}while(0)如果条件表达式为真,则static_assert()宏会定义一个长度为1的数组,否则就会尝试定一个长度为-1的数组,此时必定无法编译通过。这里值得一提的一个小技巧是使用{}符号将定义的tmp数组的作用域限定在本次调用的static_assert宏里,避免多次调用static_assert时出现重复定义。

写出如下C语言代码测试之:

intmain(){static_assert(21);printf(assert21\n);static_assert(21);printf(assert21\n);return0;}

编译这段C语言代码,得到如下输出:

显然,static_assert()宏在编译阶段就将假条件表达式找出来了。可能有些读者会觉得如果assert成功,就会定义一个tmp数组,虽然它的长度很短,但是仍然浪费了栈空间。其实这里可以把长度为零的数组,即:

#definestatic_assert(expr)\do{chartmp[(expr)?0:-1];}while(0)在assert成功时会执行chartmp[0];,它的长度为0,感兴趣的读者可以使用sizeof()测试一下。到这里,我们就较为粗略的定义好了static_assert宏,它在编译阶段就能发现假条件。

小结

本节主要介绍了assert()的使用,应该能够发现,在开发阶段,它能够帮助程序员快速的定位“意外”,也讨论了assert()的不足之处,并在此基础上自己定义了“编译时”的static_assert宏。按照这样的思路,其实还有很多定义static_assert()宏的其他方法,具体哪些方法留给读者自己思考了。

1
查看完整版本: C语言陷阱与技巧20节,自定义编译时