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

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

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命令。

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

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

  1. C语言包含头文件时用引号和尖括号的区别

    用尖括号 #include <>:    一般用于包含标准的库头文件,编译器会去系统配置的库环境变量和者用户配置的路径去搜索,而不会在项目的当前目录去查找 用双引号 #include &q ...

  2. Linux学习---条件预处理的应用

    预处理的使用: ⑴包含头文件 #include ⑵宏定义 #define    替换,不进行语法检查 ①常量宏定义:#define 宏名 (宏体) (加括号为防止不进行语法检查而出现的错误) eg:# ...

  3. C语言的本质(23)——C标准库之输入与输出(上)

    1..文件的基本概念 所谓"文件"是指一组相关数据的有序集合.这个数据集有一个名称,叫做文件名.实际上在前面的各章中我们已经多次使用了文件,例如源程序文件.目标文件.可执行文件.库 ...

  4. C预编译, 预处理, C/C++头文件, 编译控制,

    在所有的预处理指令中,#Pragma 指令可能是最复杂的了,它的作用是设定编译器的状态或者是指示编译器完成一些特定的动作.#pragma指令对每个编译器给出了一个方法,在保持与C和C++语言完全兼容的 ...

  5. c语言头文件和源文件不在同一个目录

    http://www.cnblogs.com/ShaneZhang/archive/2013/05/20/3088688.html 从工程上讲,头文件的文件名应该与对应的源文件名相同便于维护,如果头文 ...

  6. 为什么C语言会有头文件

    前段时间一个刚转到C语言的同事问我,为什么C会多一个头文件,而不是像Java和Python那样所有的代码都在源文件中.我当时回答的是C是静态语言很多东西都是需要事先定义的,所以按照惯例我们是将所有的定 ...

  7. C语言头文件、库文件的查找路径

    在 程序设计中,文件包含是很有用的.一个大的程序可以分为多个模块,由多个程序员分别编程.有些公用的符号常量或宏定义等可单独组成一个文件,在其它文件的开头用包含命令包含该文件即可使用.这样,可避免在每个 ...

  8. C++头文件,预处理详解

    C++遵循先定义,后使用的原则.就拿函数的使用来举例吧. 我看过有些人喜欢这样写函数. #include<iostream> using namespace std; int add(in ...

  9. C语言的本质(19)——预处理之一:宏定义

    我们在写代码时已多次使用过以"#"号开头的预处理命令.如包含命令#include,宏定义命令#define等.在源程序中这些命令都放在函数之外,而且一般都放在源文件的前面,它们称为 ...

随机推荐

  1. 全 Javascript 的 Web 开发架构:MEAN

    http://developer.51cto.com/art/201404/434759.htm 全 Javascript 的 Web 开发架构:MEAN 引言 最近在Angular社区的原型开发者间 ...

  2. yii安装配置

    Yii 的安装由如下两步组成: 从 yiiframework.com 下载 Yii 框架. 将 Yii 压缩包解压至一个 Web 可访问的目录. 提示: 安装在 Web 目录不是必须的,每个 Yii ...

  3. JavaScript加密解密7种方法总结分析

    原文地址:http://wenku.baidu.com/view/9048edee9e31433239689357.html 本文一共介绍了七种javascript加密方法: 在做网页时(其实是网页木 ...

  4. java Socket 使用注意

    Socket s = new Socket(ia, port); BufferedOutputStream bufOut = new BufferedOutputStream(s.getOutputS ...

  5. shell脚本书写总结

    1.shell脚本,如果重定向到文件中,则在脚本中不可以sed -i,如果用了sed -i ,则自打用了sed -i之后的打印都不能再重定向到输出文件了. 2.shell脚本中,如果将命令写在字符串里 ...

  6. 一种基于Qt的可伸缩的全异步C/S架构server实现(二) 网络传输

    二.网络传输模块 模块相应代码命名空间    (namespace ZPNetwork) 模块相应代码存储目录    (\ZoomPipeline_FuncSvr\network) 2.1 模块结构 ...

  7. 杭电 3887 Counting Offspring

    根据上篇翻译的文章以及很多个帖子,都讲述了树状数组最基本的功能就是tree[i]保存的是位置i左边小于等于a[i]的数的个数. 这样也就可以解释代码中为什么有f[i]=getsum(sd[i-1])- ...

  8. UESTC-888-Absurdistan Roads(kruskal+floyd)

    The people of Absurdistan discovered how to build roads only last year. After the discovery, every c ...

  9. centos上node.js的安装

    CentOS 下安装 Node.js 1.下载源码,你需要在https://nodejs.org/en/download/下载最新的Nodejs版本,本文以v0.10.24为例: cd /usr/lo ...

  10. Android图片编译报错

    一. AAPT err(1118615418): ERROR: 9-patch image icon_item_bottom_line.9.png malformed No marked region ...