关于使用宏定义实现比较两个数取最小值的一些思考
前言
之前的文章有总结工作中应用的一些宏定义的问题,今天来思考一个比较简单的宏定义的标准面试题的思考。题目如下:实现一个标准的宏定义,这个宏输入两个参数并且返回其中较小的一个参数。
MIN的实现
理所当然,作为一个使用过C语言的编程者很理所当然的想到以下的代码实现。
#define MIN(x, y) ((x) < (y) ? (x) : (y))
宏定义在代码编译期进行替换展开,所以使用宏定义的时候最好使用括号,在有比较时候使用do { 表达 } while(0)
方式括起来,这样可以避免当以个表达式中含有宏并且有其他的高优先级的运算符而破坏了宏的实现顺序。
上面的答案是可以取出两数之中的最小值,但是在使用这个宏的时候是否存在一些风险了,例如我们使用这个宏来实现以下的代码
least = MIN(i++, j++);
将宏展开后:
least = ((i++) < (j++) ? (i++) : (j++));
这样无论那个数更小,在比较后都做两次的自增运算而导致i和j的比较后出现错误。那么要如何消除这种比较带来问题了,那么我们可以使用语句表达式来定义这个宏。
Note:语句表达式是GNU对C语言标准的扩展,它允许在一个表达式中执行多个语句,并返回最后一个语句的值。语法如下:
({ statement1; statement2; ... statementN; expression; })
其中,statement1到statementN是一系列语句,expression是最后一个语句的表达式。整个语句表达式的值就是expression的值。
所以使用语句表达式可以对以上的MIN的宏进行改造。
#define MIN(x, y) ({\
int _x = x; \
int _y = y; \
_x < _y ? _x:_y; \
})
在语句表示中使用两个局部变量来存储宏的参数x和y的值,然后使用局部变量来进行大小的比较,可以避免两次自增导致问题,但是使用上面的宏也存在一个不足点,就是我们使用的是临时变量的数据类型是int型,也就是说这个MIN宏只能比较int类型的数据。这样使用起来还必须清楚比较的类型,这样明显不是很方便。
那如何完善这个宏,使得这个宏可以的支持任何的数据类型。当然一种比较直接的方法如下所示:
#define MIN(type, x, y) ({\
type _x = x; \
type _y = y; \
_x < _y ? _x:_y; \
})
在使用宏的时候,也将数据的类型传进去,这样这个宏就存在三个参数,数据的类型和两个要比较的数据。尽管可以实现MIN的功能,但是是否有更优雅的方法了?
对于GNU C而言, 这是有办法解决的,因为他扩展了一个关键字typeof
,可以或者数据类型,而对于其他的C语言版本需要对手册进行查询。通过使用typeof关键词,最后的得到宏定义写法。
#define MIN(x, y) ({\
typeof(x) _x = x; \
typeof(y) _y = y; \
(void)(&_x == &_y); \
_x < _y ? _x:_y; \
})
那么这个宏定义相比较原始的宏就安全很多了。
其中(void)(&_x == &_y);
是用于检查x和y的类型是否一致,他有两个主要的作用:
- 用来给编译者一个安全的检测,如果不同类型的指针比较,编译器会发出一个警告,提示两种数据的类型不同。
- 两个数据进行比较但是运算的结果没有用到,在一些编译器中会给出一个警告,但是这个警告是可以忽视的,所以使用(void)就可以消除这个警告。
最后一点吹毛求疵的优化,这个优化是基于typeof
的关键词进行优化,而typeof
关键词是基于GNU编译器,如果当前的编译器不支持这个关键词则宏定义会出现问题。
所以为了提高兼容性可以使用__GNUC__
宏来判断当前是否是GNU编译器。
__GNUC__
是GNU C编译器定义的宏,它的值为编译器的版本号。如果当前编译器是GNU C编译器,__GNUC__
宏的值将大于0。因此,可以使用以下代码来判断当前是否是GNU编译器:可以使用__GNUC__
宏来判断当前是否是GNU编译器。
__GNUC__
是GNU C编译器定义的宏,它的值为编译器的版本号。如果当前编译器是GNU C编译器,__GNUC__
宏的值将大于0。因此,可以使用以下代码来判断当前是否是GNU编译器:
#ifdef __GNUC__
#define MIN(x, y) ({\
typeof(x) _x = x; \
typeof(y) _y = y; \
(void)(&_x == &_y); \
_x < _y ? _x:_y; \
})
#else
MIN的其他实现方式
#endif