微信搜索:编程笔记本
微信搜索:编程笔记本
微信搜索:编程笔记本
点击上方蓝字关注我,我们一起学编程
欢迎小伙伴们分享、转载、私信、赞赏
今天来研究一个看似简单、实则不那么简单的问题:**定义一个求两数较小值的宏。**下面我将以百分制的方式为各种写法打分。
一、【青铜选手】得分:1分
#define MIN(x, y) x < y ? x : y
为什么给得这么低呢?我们来看看下面的例子:
#include <stdio.h>
#define MIN(x, y) x < y ? x : y
int main()
{
int sum = 2 + MIN(3, 4);
printf("sum is %d.\n", sum);
return 0;
}
理论上,sum
的值应该为 5
才对。但是上面这种写法得到的结果是什么呢?我们先运行一下,看看结果:
➜ $ gcc test.c -o test
➜ $ ./test
sum is 4.
是不是很奇怪?怎么变出个 4
出来了?对于宏的问题,一种很好的方式是检查预处理后的代码,查看宏到底展开成了什么样子。下面的代码是使用 gcc -E test.c -o test.i
得到的预处理文件:
// 与本文无关的代码已略去
int main()
{
int sum = 2 + 3 < 4 ? 3 : 4;
printf("sum is %d.\n", sum);
return 0;
}
可以看到,此处的宏是直接展开的。这就导致了一个问题:展开后的宏会与上下文发生直接联系,在遇到运算符优先级等问题时,会产生各种错误。
本例中,2 + MIN(3, 4)
被展开成了 2 + 3 < 4 ? 3 : 4
。由于 +
运算符的优先级大于 <
运算符的优先级,所以这一句又被解释为 (2 + 3) < 4 ? 3 : 4
。所以,最终的运算结果为 4
。
二、【白银选手】得分:60 分
#define MIN(x, y) (x < y ? x : y)
上面这种写法,利用一对括号将整个宏与上下文的联系独立开来。但仍然有很大的缺点,且看下面的例子:
#include <stdio.h>
#define MIN(x, y) (x < y ? x : y)
int main()
{
int min = MIN(2 < 3 ? 6 : 5, 4);
printf("min is %d.\n", min);
return 0;
}
理论上,sum
的值应该为 4
才对。我们再来看一下运行结果:
➜ $ gcc test.c -o test
➜ $ ./test
sum is 6.
又翻车了!老样子,我们再来看一下预处理后的文件吧:
// 与本文无关的代码已略去
int main()
{
int min = (2 < 3 ? 6 : 5 < 4 ? 2 < 3 ? 6 : 5 : 4);
printf("min is %d.\n", min);
return 0;
}
原来,作为第一个参数的 2 < 3 ? 6 : 5
没有做到与第二个参数之间的隔离,导致了运算的粘连。本例中,MIN(2 < 3 ? 6 : 5, 4)
被展开成了 (2 < 3 ? 6 : 5 < 4 ? 2 < 3 ? 6 : 5 : 4)
,由于条件运算符具有右结合性,所以这一句又被解释为 (2 < 3 ? 6 : (5 < 4 ? (2 < 3 ? 6 : 5) : 4))
。所以,最终的结果是 6
。
三、【黄金选手】得分:90 分
#define MIN(x, y) ((x) < (y) ? (x) : (y))
这种写法使用括号将两个参数独立出来,避免第二种写法存在的问题。看样子,内部、外部均已隔离干净,应该没啥问题了?!但大家可能注意到我只给了 90 分,肯定还有美中不足之处。且看下面例子:
#include <stdio.h>
#define MIN(x, y) ((x) < (y) ? (x) : (y))
int main()
{
double x = 1.0;
int min = MIN(x++, 1.5);
printf("min is %lf.\n", min);
return 0;
}
理论上,min
的值应该为 1.0
才对。我们先来看看运行结果:
➜ $ gcc test.c -o test
➜ $ ./test
min is 2.000000.
又是一个出乎预料的结果。老规矩,还是看看预处理文件吧:
// 与本文无关的代码已略去
int main()
{
double x = 1.0;
double min = ((x++) < (1.5) ? (x++) : (1.5));
printf("min is %lf.\n", min);
return 0;
}
原来如此,x++
进行了两次运算,并且没有保留自加操作前的值。所以,在这种情况下,即使 x++
只进行一次运算,结果也是不正确的。
微信搜索:编程笔记本
微信搜索:编程笔记本
微信搜索:编程笔记本
四、【王者选手】得分:100 分
#define MIN(X, Y) ( \
{ \
__typeof__(X) __x = (X); \
__typeof__(Y) __y = (Y); \
__x < __y ? __x : __y; \
} \
)
使用 __typeof__
获取参数类型,并基于参数值定义一个新的变量,使用自定义变量的值进行大小判断。这种写法正确的原因是,不会受参数表达式的影响,且参数纸之间不会粘连。测试一下上述三种情况:
#include <stdio.h>
#define MIN(X, Y) ( \
{ \
__typeof__(X) __x = (X); \
__typeof__(Y) __y = (Y); \
__x < __y ? __x : __y; \
} \
)
int main()
{
int sum = 2 + MIN(3, 4);
printf("sum is %d.\n", sum);
int min1 = MIN(2 < 3 ? 6 : 5, 4);
printf("min1 is %d.\n", min1);
double x = 1.0;
double min2 = MIN(x++, 1.5);
printf("min2 is %lf.\n", min2);
return 0;
}
运行结果:
➜ $ gcc test.c -o test
➜ $ ./test
sum is 5.
min1 is 4.
min2 is 1.000000.
这就是大名鼎鼎的 GNU 中的写法。
其实,我们是局限在宏定义的范围内来实现取小函数。之所以会出现各种各样的问题,是由于宏的本质是文本的直接替换,这会引入表达式之间、参数之间的粘连,且表达式的值与运算次数都无法保证正确。
其实,如果使用函数实现,所有的问题都会迎刃而解,因为函数会提供天然的隔离环境与求值操作:
int min(int x, int y)
{
return x < y ? x : y;
}
但是使用函数实现有一个不足之处,函数只能针对特定类型的数据进行取小操作,这也是宏定义的方式存在的原因。
微信搜索:编程笔记本
微信搜索:编程笔记本
微信搜索:编程笔记本