请稍侯

最小值宏定义的解析

15 September 2014

   如果让你写一个求最小值的宏定义时,大部分人可能觉得很简单,心想只要注意加上括号不久可以么,信手拈来,刷刷写下:

#define min(x, y) ((x) < (y) ? (x) : (y))

   确实在大部分情况下,这个宏定义已经足够使用,我们考虑这么一种情况:

  int a = 3;
    int b = 4;
    printf("%d\n", min(a++,b++));
    printf("a = %d,b = %d", a, b);

   代码中将将a++和b++作为参数传入,本是先比较a和b,然后将a和b加1,运行这段代码会发现首先输出最小值4,然后a等于5,b等于5。问题就出在++运算符上。min(x++,y++)被替换为((a++) < (b++) ? (a++) : (b++)),首先执行(a++) < (b++)语句,比较a和b,比较完成之后两者都加1,然后输出时因为输出的是a,此时返回的值是a++,于是先返回4之后,a又加1。
   那我们如何避免这种情况呢?我们可以用两个值来保存两个参数,然后使用两个参数进行计算,那这两个参数的类型我们如何得知呢?可以通过typeof关键字,这个关键字是GNU 扩展的C语言关键字,VC不支持。我们看下代码:

#define min(x, y) ({                            \
    typeof(x) _min1 = (x);                      \
    typeof(y) _min2 = (y);                      \
    _min1 < _min2 ? _min1 : _min2; })    

   注意在这些复合语句外面套了两层括号:{}和():
   使用{}将宏里面多个语句包裹起来,也就是所谓的局部程序块,可以让这个局部程序全部执行完,假如没有了这个{},在一些语句中就会出现歧义,比如if(a) min(x,y),这样就会出问题,条件为真时,只会执行typeof(x) _min1 = (x);这一条语句,后面的语句就不会执行了。
   使用()是因为()里面加上复合语句是GCC扩展中的一个用法,在GCC扩展中允许把一个()内的复合语句看成是一个表达式,称为语句表达式,它可以出现在任何语句表达式可以出现的地方。比如当我们写return min(x,y);时,会将这些复合语句看成一个表达式,不然的话编译会报错。而且当你的宏定义需要返回值时,使用小括号()把复合语句括起来使得该复合语句最后一条语句的运算结果作为该宏的返回值。
   使用了两个变量来保存传入的参数,这样似乎达到了我们的目的。可是如果我们传入的参数类型不一样呢?宏定义的一个缺点就是不会进行类型检查,我们如何能够实现参数类型的检验呢?有同学可能会说直接比较typeof(a)和typeof(b)是否相等好了,可是事与愿违,使用typeof()判断类型相等时会报错。我们可以使用&_min1 == &_min2来判断,判断&_min1和&_min2是否相等,就是判断x和y的指针是否相等。在判断两个变量是否相等的时候,编译器会先判断两个变量的类型是否相等。如果x和y的类型不等,编译的时候会报错。
   &_min1 == &_min2比较的结果我们并没有使用,如果编译选项中有-Wunused-value选项(GCC警告选项,用来警告一个显式计算表达式的结果未被使用)时,编译会产生警告,我们可以通过在&_min1 == &_min2前面加上void来将两个地址比较后的比较结果强行扔掉,此时编译选项中有-Wunused-value选项就不会有警告了,此时代码变为:

#define min(x, y) ({                            \
    typeof(x) _min1 = (x);                      \
    typeof(y) _min2 = (y);                      \
    (void) (&_min1 == &_min2);                  \
    _min1 < _min2 ? _min1 : _min2; })

   此时,我们的代码应该就没什么问题了,其实上面这段代码是Linux内核源码中求最小值的宏定义,短短几行代码却暗藏许多机巧,也显示了牛人们扎实的基本功,还是继续敲代码好好学习吧!