3. 条件预处理指示
我们在第 2.2 节 “头文件”中见过 Header Guard 的用法:
#ifndef HEADER_FILENAME
#define HEADER_FILENAME
/* body of header */
#endif
条件预处理指示也常用于源代码的配置管理,例如:
#if MACHINE == 68000
int x;
#elif MACHINE == 8086
long x;
#else /* all others */
#error UNKNOWN TARGET MACHINE
#endif
假设这段程序是为多种平台编写的,在 68000 平台上需要定义 x 为 int 型,在 8086 平台上需要定义 x 为 long 型,对其它平台暂不提供支持,就可以用条件预处理指示来写。如果在预处理这段代码之前, MACHINE 被定义为 68000,则包含 intx; 这段代码;否则如果 MACHINE 被定义为 8086,则包含 long x; 这段代码;否则( MACHINE 没有定义,或者定义为其它值),包含 #error UNKNOWN TARGET MACHINE 这段代码,编译器遇到这个预处理指示就报错退出,错误信息就是 UNKNOWN TARGET MACHINE 。
如果要为 8086 平台编译这段代码,有几种可选的办法:
1、手动编辑代码,在前面添一行 #define MACHINE 8086 。这样做的缺点是难以管理,如果这个项目中有很多源文件都需要定义 MACHINE ,每次要为 8086 平台编译就得把这些定义全部改成 8086,每次要为 68000 平台编译就得把这些定义全部改成 68000。
2、在所有需要配置的源文件开头包含一个头文件,在头文件中定义 #define MACHINE 8086 ,这样只需要改一个头文件就可以影响所有包含它的源文件。通常这个头文件由配置工具生成,比如在 Linux 内核源代码的目录下运行 make menuconfig 命令可以出来一个配置菜单,在其中配置的选项会自动转换成头文件 include/linux/autoconf.h 中的宏定义。
举一个具体的例子,在内核配置菜单中用回车键和方向键进入 Device Drivers ---> Network device support ,然后用空格键选中 Network device support (菜单项左边的 [ ] 括号内会出现一个 * 号),然后保存退出,会生成一个名为 .config 的隐藏文件,其内容类似于:
...
#
# Network device support
#
CONFIG_NETDEVICES=y
# CONFIG_DUMMY is not set
# CONFIG_BONDING is not set
# CONFIG_EQUALIZER is not set
# CONFIG_TUN is not set
...
然后运行 make 命令编译内核,这时根据 .config 文件生成头文件 include/linux/autoconf.h ,其内容类似于:
...
/*
* Network device support
*/
#define CONFIG_NETDEVICES 1
#undef CONFIG_DUMMY
#undef CONFIG_BONDING
#undef CONFIG_EQUALIZER
#undef CONFIG_TUN
...
上面的代码用 #undef 确保取消一些宏的定义,如果先前没有定义过 CONFIG_DUMMY ,用 #undef CONFIG_DUMMY 取消它的定义没有任何作用,也不算错。
include/linux/autoconf.h 被另一个头文件include/linux/config.h 所包含,通常内核代码包含后一个头文件,例如net/core/sock.c :
...
#include <linux/config.h>
...
int sock_setsockopt(struct socket *sock, int level, int optname,
char __user *optval, int optlen)
{
...
#ifdef CONFIG_NETDEVICES
case SO_BINDTODEVICE:
{
...
}
#endif
...
再比如 drivers/isdn/i4l/isdn_common.c :
...
#include <linux/config.h>
...
static int
isdn_ioctl(struct inode *inode, struct file *file, uint cmd, ulong arg)
{
...
#ifdef CONFIG_NETDEVICES
case IIOCNETGPN:
/* Get peer phone number of a connected
* isdn network interface */
if (arg) {
if (copy_from_user(&phone, argp, sizeof(phone)))
return -EFAULT;
return isdn_net_getpeer(&phone, argp);
} else
return -EINVAL;
#endif
...
#ifdef CONFIG_NETDEVICES
case IIOCNETAIF:
...
#endif /* CONFIG_NETDEVICES */
...
这样,在配置菜单中所做的配置通过条件预处理最终决定了哪些代码被编译到内核中。 #ifdef 或 #if 可以嵌套使用,但预处理指示通常都顶头写不缩进,为了区分嵌套的层次,可以像上面的代码中最后一行那样,在 #endif 处用注释写清楚它结束的是哪个 #if 或 #ifdef 。
3、要定义一个宏不一定非得在代码中用 #define 定义,早在第 6 节 “折半查找”我们就见过用 gcc 的 -D 选项定义一个宏 NDEBUG 。对于上面的例子,我们需要给 MACHINE 定义一个值,可以写成类似这样的命令: gcc -c -DMACHINE=8086 main.c 。这种办法需要给每个编译命令都加上适当的选项,和第 2 种方法相比似乎也很麻烦,第 2 种方法在头文件中只写一次宏定义就可以在很多源文件中生效,第 3 种方法能不能做到“只写一次到处生效”呢?等以后学习了 Makefile 就有办法了。
最后通过下面的例子说一下 #if 后面的表达式:
#define VERSION 2
#if defined x || y || VERSION < 3
-
首先处理
defined运算符,defined运算符一般用作表达式中的一部分,如果单独使用,#if defined x相当于#ifdef x,而#if !defined x相当于#ifndef x。在这个例子中,如果x这个宏有定义,则把defined x替换为 1,否则替换为 0,因此变成#if 0 || y || VERSION < 3。 -
然后把有定义的宏展开,变成
#if 0 || y || 2 < 3。 -
把没有定义的宏替换成 0,变成
#if 0 || 0 || 2 < 3,注意,即使前面定义了一个变量名是y,在这一步也还是替换成 0,因为#if的表达式必须在编译时求值,其中包含的名字只能是宏定义。 -
把得到的表达式
0 || 0 || 2 < 3像 C 表达式一样求值,求值的结果是#if 1,因此条件成立。