C语言的本质(20)——预处理之二:条件预处理和包含头文件

 

我们可以通过定义不同的宏来决定编译程序对哪些代码进行处理。条件编译指令将决定那些代码被编译,而哪些是不被编译的。可以根据表达式的值或者某个特定的宏是否被定义来确定编译条件。

条件编译可分为三种情况,按照不同的条件去编译不同的程序部分,因而产生不同的目标文件,这对于程序的移植和调试都非常有用。

 

1、

 

#ifdef  标识符
     程序段1
#else
     程序段2
#endif

功能:如果标识符已经被#define定义过,则对程序段1进行编译,否则对程序段2进行编译。如果没有程序断2,#else可以没哟。

 

2、

 

#ifndef 标识符
         程序段1
#else
         程序段2
#endif

它的功能是跟上面恰恰相反的,也就是说如果标识符没有被#define定义过,则对程序段1进行编译,否则对程序段2进行编译。

 

3、

 

#if  常量表达式
        程序段1;
#else
        程序段2;
#endif

功能:根据常量表达式的真假去判断执行哪个程序段,如果为真则执行程序段1,否则执行程序段2。

 

条件预处理可以用来调试程序,比如:

 

#define DEBUG     //此时#ifdefDEBUG为真
//#define DEBUG 0    //此时为假
int main(void)
{
#ifdef DEBUG
         printf("Debugging\n");
#else
         printf("Notdebugging\n");
#endif
         printf("Running\n");
         return0;
}

这样我们就可以实现debug功能,每次要输出调试信息前,只需要#ifdefDEBUG判断一次。不需要了就在文件开始定义#define DEBUG

 

 

条件预处理指示常用于源代码的配置管理,例如:

 

#if MACHINE == arm
   int x;
#elif MACHINE == x64
   long x;
#else   /* all others */
   #error UNKNOWN TARGET MACHINE
#endif

假设这段程序是为多种平台编写的,在arm平台上需要定义x为int型,在x64平台上需要定义x为long型,对其它平台暂不提供支持,就可以用条件预处理指示来写。如果在预处理这段代码之前,MACHINE被定义为arm,则包含intx;这段代码;否则如果MACHINE被定义为x64,则包含long x;这段代码;否则(MACHINE没有定义,或者定义为其它值),包含#error UNKNOWN TARGET MACHINE这段代码,编译器遇到这个预处理指示就报错退出,错误信息就是UNKNOWN TARGET MACHINE。

 如果要为x64平台编译这段代码,有几种可选的办法:

 1、手动编辑代码,在前面添一行#defineMACHINE x64。这样做的缺点是难以管理,如果这个项目中有很多源文件都需要定义MACHINE,每次要为x64平台编译就得把这些定义全部改成x64,每次要为arm平台编译就得把这些定义全部改成arm。

 2、在所有需要配置的源文件开头包含一个头文件,在头文件中定义#define MACHINE x64,这样只需要改一个头文件就可以影响所有包含它的源文件。通常这个头文件由配置工具生成,比如在Linux内核源代码的目录下运行make menuconfig命令可以出来一个配置菜单,在其中配置的选项会自动转换成头文件include/linux/autoconf.h中的宏定义。

 举一个具体的例子,在内核配置菜单中用回车键和方向键进入Device Drivers ---> Network device support,然后用空格键选中Networkdevice 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, intoptlen)
{
......
#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 peerphone number of a connected
* isdn networkinterface */
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定义,比如我们可以用gcc的-D选项定义一个宏NDEBUG。对于上面的例子,我们需要给MACHINE定义一个值,可以写成类似这样的命令:gcc -c -DMACHINE=x64 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,因此条件成立。

 

文件包含

文件包含是C预处理程序的另一个重要功能。文件包含命令行的一般形式为:

   #include "文件名"

我们曾经多次用此命令包含过库函数的头文件。例如:

#include "stdio.h"
#include "math.h"


文件包含命令的功能是把指定的文件插入该命令行位置取代该命令行,从而把指定的文件和当前的源程序文件连成一个源文件

 在程序设计中,文件包含是很有用的。一个大的程序可以分为多个模块,由多个程序员分别编程。有些公用的符号常量或宏定义等可单独组成一个文件,在其它文件的开头用包含命令包含该文件即可使用。这样,可避免在每个文件开头都去书写那些公用量,从而节省时间,并减少出错。

 对文件包含命令还要说明以下几点:

包含命令中的文件名可以用双引号括起来,也可以用尖括号括起来。例如以下写法都是允许的:

#include "stdio.h"
#include <math.h>

但是这两种形式是有区别的:

使用尖括号表示在包含文件目录中去查找(包含目录是由用户在设置环境时设置的),而不在源文件目录去查找;

使用双引号则表示首先在当前的源文件目录中查找,若未找到才到包含目录中去查找。用户编程时可根据自己文件所在的目录来选择某一种命令形式。

一个include命令只能指定一个被包含文件,若有多个文件要包含,则需用多个include命令。

文件包含允许嵌套,即在一个被包含的文件中又可以包含另一个文件。

 

 

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

尹成

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值