再来个更简单的宏BUG。
#define SQUARE(a, b) (a * b)
printf("%d\n", SQUARE(1+1, 1+1));
猜猜结果是什么。
再来个更简单的宏BUG。
#define SQUARE(a, b) (a * b)
printf("%d\n", SQUARE(1+1, 1+1));
猜猜结果是什么。
The result is 3
这算是个 common 的 gotcha 了吧,我记得大一 C 语言考试就有这种题型,C 的宏只是文本替换,不是函数调用,理解这点就好了。
稍微写过工程级代码的都知道要加括号。。。。。。。
一般程序员用成熟的库就好了,不需要自己写吧。C 的优势在于简洁,相比之下 C++ feature 就十分庞大,不同人可以写出来不同的风格,从维护性考虑,不见得比 C 好多少吧。
什么叫成熟的库?你觉得你上面列举的 tezc/sc 算成熟的库吗?你可以轻松玩坏它:
sc_array_add(&arr, 0);
sc_array_add(&arr, 1);
sc_array_add(&arr, 2);
/*到这里数组是0,1,2*/
/*现在你以为你删除下标1*/
int i = 0;
sc_array_del_unordered(&arr, ++i);
/*结果它删除了下标2,剩余元素是0,1*/
如果你平时写程序的时候需要不停地思考宏展开会不会搞坏你的逻辑,那简直是没法好好干什么有用的活的。更不用说光看调用方的代码你根本看不出来这是个函数还是个宏,或者这个宏里面是怎么写的,你怎么知道需要提防什么?
对于这个,我觉得在参数里面写带 side effect 的表达式不是一个很好的实践
++i只是举个简单的例子。参数里写个函数调用很普通吧,难道你能保证你的C函数全都是Pure Function没有副作用的吗?
真的有人觉得这种步步都是坑的代码很简单很容易维护吗?
我在这写几个例子大家一看就明白问题在哪。在真正的工程项目里,几十万行,上百万行代码里,没有人能把问题指给你,反正程序崩了,或者结果不对,运维给你发了一个紧急ticket,不修复不准下班。
我想了想,似乎我还真不怎么写这种。因为会导致函数调用太长… 可能个人习惯吧。
另外宏展开的这个问题也是可以在写宏的时候避免,但是需要 gcc 的 Statement Expression 拓展。 比如这个例子,其中的 int 也可以使用 typeof 来替换。
#define maxint(a,b) \
({int _a = (a), _b = (b); _a > _b ? _a : _b; })
这是你写 preprocessor macro 不行,不是 C 本身的问题。
tezc 确实不成熟,API 稳定都不保证。你能把 Xlib 或 sqlite 玩坏吗?
要是我写macro的水平不行,那我看99%的程序员都不行。这样公司还能招到足够的水平能行的人吗?这是不是说明语言的局限性呢?
99% 的 C 程序员都懂得写 macro 时要用括号保护参数。
sqlite基本上没有这种大块macro当函数的用法。但是sqlite的C API易用性不行,大家不愿意直接用。(顺便一提,Snapchat的sqlite C++封装库是我写的)
你连这里的简单例子都没看明白吧?
sc_array_add(&arr, 0);
sc_array_add(&arr, 1);
sc_array_add(&arr, 2);
/*到这里数组是0,1,2*/
/*现在你以为你删除下标1*/
int i = 0;
/* -> 这里加了括号就不坏了?*/
sc_array_del_unordered(&arr, (++i));
/*结果它删除了下标2,剩余元素是0,1*/
看明白了,sc_array_del_unordered 是 macro, ++i 会执行两次。
C preprocessor 不能不否认容易被滥用,但是这不能表明 C 语言本身有问题,也不能否定 C preprocessor 的实用性。像 sc_array_del_unordered 的 macro 应该写成 header 文件中的 inline 函数,不仅会避免多次 evaluation 带来的 bug,还更方便 debug。
那也不能不承认C preprocessor 的局限性吧!
C preprocessor 的局限性不影响 C 语言本身,毕竟是 preprocessor。