这是本系列的最后一篇,介绍一些宏在日常使用中常见的一些坑。 由于宏本质上是对程序的文本进行操作,且宏和 C 语言本身完全是两套系统,所以很多时候总会引入一些奇怪到的错误,这一节我们就来介绍一些常见的需要注意的地方。
不知为什么,似乎在宏的教程中实现最小值宏是某种传统,所以这回我们也以实现一个标准的求最小值的宏为例,进行讲解。
操作符优先级
假设我们定义一个 MIN
宏如下:
|
|
该宏正常运行。但是如果我们调用的时候稍微复杂一点:
|
|
看出问题所在了么?由于算法优先级的问题,所以此处 12 * 2
被先行计算了。导致了错误的结果。解决办法也很简单,就是加括号。
|
但是这样就没有问题了么?并不是,只要遇到相同优先级的运算符还是会出问题,比如我想要比较三个数:
|
|
还是会存在问题。所以比较好的实践是给每一个参数和整体都加上括号。于是我们的 MIN
宏变成了:
|
拷贝副作用
宏的本质是替换。这就引入一个问题,每一个参数都会在字面上被拷贝一份,并替换到相应的位置。这会导致有副作用的函数被调用多次,例如, 我有一个 count
函数,记录自己被调用的次数,并将次数返回:
|
|
那么不同的写法会导致不同的结果,例如:
|
|
|
|
两次的结果居然不一样。这是为什么呢?因为第二次的时候宏展开会变成这个样子:
|
|
count
在这里被调用了 2 次。处理方法就是我们在这里加入一个临时变量:
|
|
其中({})
是一个 GNU 的扩展,它能够使其中的最后一个语句的值作为整个式子的返回值。如果这个扩展不被支持的话……我目前也没什么好办法了。
不过使用临时变量的话就有一个问题,有可能会出现命名冲突的情况。比如在 Clang 中的 MIN 是这么实现的:
|
|
第一个宏 __NSX_PASTE__
我们很熟悉了,这就是我们前几节提到的 PRIMITIVE_CAT
,之所以分开写是因为保证参数完全展开。第二个宏里边有个 __COUNTER__
这个宏的意思是是个计数器,每次调用的时候 +1。这样的话,这个宏就很清楚了。其实就是在我们上一个版本的基础上,对每一个变量后边都添加一个数字,比如a12321这种,这样的话就能最大限度的防止明明冲突。正常人也不会用a1234 之类的当做变量名吧。至此这个 MIN
宏就能在大多数情况下正常使用了。那么问题来了,这是 MIN
的最佳实践么?当然不是,真正的最佳实践是这样的:
|
|
吞分号
一般来说我们在调用宏的时候都会在最后加一个分号,就是这个分号会引入新的问题。例如我定义了一个宏用来跳过无用的内存空间:
|
|
为了防止命名冲突,在这里为语句加上了{}。那么如果有人这么写:
|
|
那么展开后就变成了:
|
|
注意最后的那个小分号。这使得后边的 else 空置了。(其实如果不加{}也会有类似的问题,读者可以想想看。)为了解决这个额外的分号的问题,我们使用一种惯用法do...while(0)
:
|
|
注意此处的 while(0) 后边是没有分号的(某大厂高 t 员工就写错了 (๑•̀ㅂ•́)و✧ )编译器一般会把这个多余的一次循环优化掉,所以性能也不会有什么损失。
到这里整个系列就完结了,感谢大家能够看到这里,作为答谢,送大家一个奇技淫巧,如何判断两个指针变量是否同类型:
|
如果不是同类型这个三元操作符会在编译期间报错,你说这些人是怎么想到的呢 _(:з」∠)_