c语言编译时检查逻辑错误吗,C语言陷阱与技巧20节,自定义“编译时”assert方法,在代码编译阶段检查“逻辑”错误...

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

4c30524dc1fa4e78f655fd37415127b4.png

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

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

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

使用 assert

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

#include void assert(scalar expression);

27325f2342d46cc67c3f336634c9b44d.png

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

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

int fd = open("/dev/sth", O_RDONLY);assert(fd > 0);printf("fd = %d\n", fd);

7399146120038643cab8729921b08dfd.png

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

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

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

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

dbfc82ce56f543c6fdc2979a1898b318.png

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

#include #include #include #include #define NDEBUG#include int main(){int fd = open("/dev/sth", O_RDONLY); assert(fd > 0);printf("fd = %d\n", fd);return0; }

2b7821d00ecf943e8229b064ce91dd62.png

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

# gcc t.c# ./a.out fd = -1编译时 assert

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

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

5811242cb6b729f84457e2d327542b3d.png

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

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

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

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

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

int main() { static_assert(2>1);printf("assert 2>1\n");static_assert(2<1);printf("assert 2<1\n");return0; }

ef079cbcf565efea27520162e1780ce4.png

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

093896b7cbd6316fb0fe59b01892297b.png

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

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

小结

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值