理解编译预处理

在将一个C源程序转换为可执行程序的过程中编译预处理是最初的步骤这一步骤是由预处理器(preprocessor)来完成的在源流程序被编译器处理之前预处理器首先对源程序中的"(macro)"进行处理.

初学者可能对预处理器没什么概念这是情有可原的一般的C编译器都将预处理汇编编译连接过程集成到一起了编译预处理往往在后台运行在有的C编译器中这些过程统统由一个单独的程序来完成编译的不同阶段实现这些不同的功能可以指定相应的命令选项来执行这些功能有的C编译器使用分别的程序来完成这些步骤可单独调用这些程序来完成gcc进行编译预处理的程序被称为CPP, 它的可执行文件名为cpp.

编译预处理命令的语法与C语言的语法是完全独立的比如你可以将一个宏扩展为与C语法格格不入的内容但该内容与后面的语句结合在一个若能生成合法的C语句也是可以正确编译的.


(预处理命令简介

理解编译预处理

预处理命令由#(hash字符)开头它独占一行#之前只能是空白符#开头的语句就是预处理命令不以#开头的语句为C中的代码行常用的预处理命令如下:

#define              定义一个预处理宏
#undef               取消宏的定义

#include           包含文件命令
#include_next   #include相似但它有着特殊的用途

#if                     编译预处理中的条件命令相当于C语法中的if语句
#ifdef               判断某个宏是否被定义若已定义执行随后的语句
#ifndef            #ifdef相反判断某个宏是否未被定义
#elif                 #if, #ifdef, #ifndef或前面的#elif条件不满足则执行#elif之后的语句相当于C语法中的else-if
#else               #if, #ifdef, #ifndef对应若这些条件不满足则执行#else之后的语句相当于C语法中的else
#endif             #if, #ifdef, #ifndef这些条件命令的结束标志.
defined           #if, #elif配合使用判断某个宏是否被定义

#line                标志该语句所在的行号
#                      将宏参数替代为以参数值为内容的字符窜常量
##                   将两个相邻的标记(token)连接为一个单独的标记
#pragma        说明编译器信息

#warning       显示编译警告信息
#error            显示编译错误信息


(预处理的文法

理解编译预处理

预处理并不分析整个源代码文件它只是将源代码分割成一些标记(token), 识别语句中哪些是C语句哪些是预处理语句预处理器能够识别C标记文件名空白符文件结尾标志.

预处理语句格式   #command name(...) token(s)

1, command预处理命令的名称它之前以#开头#之后紧随预处理命令标准C允许#两边可以有空白符但比较老的编译器可能不允许这样若某行中只包含#(以及空白符), 那么在标准C中该行被理解为空白整个预处理语句之后只能有空白符或者注释不能有其它内容.
2, name代表宏名称它可带参数参数可以是可变参数列表(C99).
3, 语句中可以利用"/"来换行.

e.g.
 define  ONE 
等价于#define ONE 1

#define err(flag, msg) if(flag) /
    printf(msg)
等价于#define err(flag, msg) if(flag) printf(msg)


(预处理命令详述

理解编译预处理

1, #define
#define命令定义一个宏:
#define MACRO_NAME(args) tokens(opt)
之后出现的MACRO_NAME将被替代为所定义的标记(tokens). 宏可带参数而后面的标记也是可选的.

对象宏
不带参数的宏被称为"对象宏(objectlike macro)"

#define经常用来定义常量此时的宏名称一般为大写的字符串这样利于修改这些常量.
e.g.
#define MAX 100
int a[MAX];

#ifndef __FILE_H__
#define __FILE_H__
#include "file.h"
#endif
#define __FILE_H__ 中的宏就不带任何参数也不扩展为任何标记这经常用于包含头文件.

要调用该宏只需在代码中指定宏名称该宏将被替代为它被定义的内容.

函数宏
带参数的宏也被称为"函数宏". 利用宏可以提高代码的运行效率子程序的调用需要压栈出栈这一过程如果过于频繁会耗费掉大量的CPU运算资源所以一些代码量小但运行频繁的代码如果采用带参数宏来实现会提高代码的运行效率.

函数宏的参数是固定的情况

函数宏的定义采用这样的方式#define name( args tokens
其中的argstokens都是可选的它和对象宏定义上的区别在于宏名称之后不带括号.

注意name之后的左括号(必须紧跟name, 之间不能有空格否则这就定义了一个对象宏它将被替换为 (开始的字符串但在调用函数宏时name(之间可以有空格.

e.g.
#define mul(x,y) ((x)*(y))

注意函数宏之后的参数要用括号括起来看看这个例子:
e.g.
#define mul(x,y) x*y
"mul(1, 2+2);" 将被扩展为1*2 2
同样整个标记串也应该用括号引用起来:
e.g.
#define mul(x,y) (x)*(y)
sizeof mul(1,2.0) 将被扩展为 sizeof 2.0

调用函数宏时候传递给它的参数可以是函数的返回值也可以是任何有意义的语句:
e.g.
mul (f(a,b), g(c,d));

e.g.
#define insert(stmt) stmt
insert a=1; b=2;)  相当于在代码中加入 a=1; b=2 .
insert a=1, b=2;)  就有问题了预处理器会提示出错函数宏的参数个数不匹配预处理器把","视为参数间的分隔符 
insert ((a=1, b=2;)) 可解决上述问题.

在定义和调用函数宏时候要注意一些问题:
1, 我们经常用{}来引用函数宏被定义的内容这就要注意调用这个函数宏时的";"问题.
example_3.7:
#define swap(x,y) unsigned long _temp=x; x=y; y=_tmp}
如果这样调用它"swap(1,2);" 将被扩展为unsigned long _temp=1; 1=2; 2=_tmp};
明显后面的;是多余的我们应该这样调用swap(1,2)
虽然这样的调用是正确的但它和C语法相悖可采用下面的方法来处理被{}括起来的内容:

#define swap(x,y) /
    do unsigned long _temp=x; x=y; y=_tmp} while (0)
swap(1,2); 将被替换为:
do unsigned long _temp=1; 1=2; 2=_tmp} while (0);
Linux内核源代码中对这种do-while(0)语句有这广泛的应用.

2, 有的函数宏是无法用do-while(0)来实现的所以在调用时不能带上";", 最好在调用后添加注释说明.
eg_3.8:
#define incr(v, low, high) /
    for ((v) (low),; (v) <= (high); (v)++)
只能以这样的形式被调用incr(a, 1, 10)  

函数宏中的参数包括可变参数列表的情况
C99标准中新增了可变参数列表的内容不光是函数函数宏中也可以使用可变参数列表.

#define name(args, ...) tokens
#define name(...) tokens
"..."代表可变参数列表如果它不是仅有的参数那么它只能出现在参数列表的最后调用这样的函数宏时传递给它的参数个数要不少于参数列表中参数的个数(多余的参数被丢弃).
通过__VA_ARGS__来替换函数宏中的可变参数列表注意__VA_ARGS__只能用于函数宏中参数中包含有"..."的情况.

e.g.
#ifdef DEBUG
#define my_printf(...) fprintf(stderr, __VA_ARGS__)
#else
#define my_printf(...) printf(__VA_ARGS__)
#endif

tokens中的__VA_ARGS__被替换为函数宏定义中的"..."可变参数列表.

注意在使用#define时候的一些常见错误:
#define MAX 100
#define MAX 100;
=, 的使用要值得注意再就是调用函数宏是要注意不要多给出";".

注意函数宏对参数类型是不敏感的你不必考虑将何种数据类型传递给宏那么如何构建对参数类型敏感的宏呢参考本章的第九部分关于"##"的介绍.


关于定义宏的另外一些问题
(1) 宏可以被多次定义前提是这些定义必须是相同的这里的"相同"要求先后定义中空白符出现的位置相同但具体的空白符类型或数量可不同比如原先的空格可替换为多个其他类型的空白符可为tab, 注释...
e.g.
#define NULL 0
#define NULL      0
上面的重定义是相同的但下面的重定义不同:
#define fun(x) x+1
#define fun(x) #define fun(y) y+1
如果多次定义时再次定义的宏内容是不同的gcc会给出"NAME redefined"警告信息.

应该避免重新定义函数宏不管是在预处理命令中还是C语句中最好对某个对象只有单一的定义gcc若宏出现了重定义gcc会给出警告.

(2) gcc可在命令行中指定对象宏的定义:
e.g.
gcc -Wall -DMAX=100 -o tmp tmp.c
相当于在tmp.c中添加#define MAX 100".

那么如果原先tmp.c中含有MAX宏的定义那么再在gcc调用命令中使用-DMAX, 会出现什么情况呢?
----DMAX=1, 则正确编译.
----DMAX的值被指定为不为1的值那么gcc会给出MAX宏被重定义的警告MAX的值仍为1.

注意若在调用gcc的命令行中不显示地给出对象宏的值那么gcc赋予该宏默认值(1), -DVAL == -DVAL=1

(3) #define所定义的宏的作用域
宏在定义之后才生效若宏定义被#undef取消#undef之后该宏无效并且字符串中的宏不会被识别
e.g.
#define ONE 1
sum ONE TWO    
#define TWO 2
sum ONE TWO      
#undef ONE
sum ONE TWO    

char c[] "TWO"   

(4) 宏的替换可以是递归的所以可以嵌套定义宏.
e.g.
define ONE NUMBER_1
define NUMBER_1 1
int ONE  

2, #undef
#undef用来取消宏定义它与#define对立:
#undef name
如够被取消的宏实际上没有被#define所定义针对它的#undef并不会产生错误.
当一个宏定义被取消后可以再度定义它.

3, #if, #elif, #else, #endif
#if, #elif, #else, #endif用于条件编译:

#if 常量表达式1
    语句...
#elif 常量表达式2
    语句...
#elif 常量表达式3
    语句...
...
#else
    语句...
#endif

#if#else分别相当于C语句中的if, else. 它们根据常量表达式的值来判别是否执行后面的语句#elif相当于C中的else-if. 使用这些条件编译命令可以方便地实现对源代码内容的控制.
else之后不带常量表达式但若包含了常量表达式gcc只是给出警告信息.

使用它们可以提升代码的可移植性---针对不同的平台使用执行不同的语句也经常用于大段代码注释.
e.g.
#if 0
{
    一大段代码;
}
#endif

常量表达式可以是包含宏算术运算逻辑运算等等的合法C常量表达式如果常量表达式为一个未定义的宏那么它的值被视为0.
#if MACRO_NON_DEFINED  == #if 0
在判断某个宏是否被定义时应当避免使用#if, 因为该宏的值可能就是被定义为0. 而应当使用下面介绍的#ifdef#ifndef.

注意#if, #elif, #else之后的宏只能是对象宏如果name为名的宏未定义或者该宏是函数宏那么在gcc中使用"-Wundef"选项会显示宏未定义的警告信息.

4, #ifdef, #ifndef, defined.
#ifdef, #ifndef, defined用来测试某个宏是否被定义
#ifdef name  或 #ifndef name

它们经常用于避免头文件的重复引用:
#ifndef __FILE_H__
#define __FILE_H__
#include "file.h"
#endif

defined(name): 若宏被定义,则返回1, 否则返回0.
它与#if, #elif, #else结合使用来判断宏是否被定义乍一看好像它显得多余因为已经有了#ifdef#ifndef. defined用于在一条判断语句中声明多个判别条件:

#if defined(VAX) && defined(UNIX) && !defined(DEBUG)

#if, #elif, #else不同#indef, #ifndef, defined测试的宏可以是对象宏也可以是函数宏gcc中使用"-Wundef"选项不会显示宏未定义的警告信息.

5, #include #include_next
#include用于文件包含#include 命令所在的行不能含有除注释和空白符之外的其他任何内容.
#include "headfile"
#include <headfile>
#include 预处理标记
前面两种形式大家都很熟悉"#include 预处理标记"预处理标记会被预处理器进行替换替换的结果必须符合前两种形式中的某一种.

实 际上真正被添加的头文件并不一定就是#include中所指定的文件#include"headfile"包含的头文件当然是同一个文件#include <headfile>包包含的"系统头文件"可能是另外的文件但这不值得被注意感兴趣的话可以查看宏扩展后到底引入了哪些系统头文件.

关于#include "headfile"#include <headfile>的区别以及如何在gcc中包含头文件的详细信息参考本blogGCC笔记.

相 对于#include, 我们对#include_next不太熟悉#include_next仅用于特殊的场合它被用于头文件中(#include既可用于头文件中又可用于.c文件中)来包含其他的头文件而且包含头文件的路径比较特殊从当前头文件所在目录之后的目录来搜索头文件.
比如头文件的搜索路径一次为A,B,C,D,E. #include_next所在的当前头文件位于B目录那么#include_next使得预处理器从C,D,E目录来搜索#include_next所指定的头文件.

可参考cpp手册进一步了解#include_next

6, 预定义宏
标准C中定义了一些对象宏这些宏的名称以"__"开头和结尾并且都是大写字符这些预定义宏可以被#undef, 也可以被重定义.

下面列出一些标准C中常见的预定义对象宏(其中也包含gcc自己定义的一些预定义宏:
__LINE__             当前语句所在的行号10进制整数标注.
__FILE__             当前源文件的文件名以字符串常量标注.
__DATE__           程序被编译的日期"Mmm dd yyyy"格式的字符串标注.
__TIME__           程序被编译的时间"hh:mm:ss"格式的字符串标注该时间由asctime返回.

__STDC__            如果当前编译器符合ISO标准那么该宏的值为1
__STDC_VERSION__    如果当前编译器符合C89, 那么它被定义为199409L, 如果符合C99, 那么被定义为199901L.
                    我用gcc, 如果不指定-std=c99, 其他情况都给出__STDC_VERSION__未定义的错误信息咋回事呢?
__STDC_HOSTED__        如果当前系统是"本地系统(hosted)", 那么它被定义为1. 本地系统表示当前系统拥有完整的标准C.


gcc定义的预定义宏:
__OPTMIZE__            如果编译过程中使用了优化那么该宏被定义为1.
__OPTMIZE_SIZE__    同上但仅在优化是针对代码大小而非速度时才被定义为1.
__VERSION__            显示所用gcc的版本号.
可参考"GCC the complete reference".
要想看到gcc所定义的所有预定义宏可以运行cpp -dM /dev/null

7, #line
#line用来修改__LINE____FILE__.
e.g.
  printf("line: %d, file: %s/n", __LINE__, __FILE__);
#line 100 "haha"
  printf("line: %d, file: %s/n", __LINE__, __FILE__);
  printf("line: %d, file: %s/n", __LINE__, __FILE__);

显示:
line: 34, file: 1.c
line: 100, file: haha
line: 101, file: haha

8, #pragma, _Pragma
#pragma用编译器用来添加新的预处理功能或者显示一些编译信息#pragma的格式是各编译器特定的gcc的如下:
#pragma GCC name token(s)

#pragma之后有两个部分GCC和特定的pragma name. 下面分别介绍gcc中常用的.

(1) #pragma GCC dependency
dependency测试当前文件(既该语句所在的程序代码)与指定文件(#pragma语句最后列出的文件)的时间戳如果指定文件比当前文件新则给出警告信息.
e.g.
demo.c中给出这样一句:
#pragma GCC dependency "temp-file"
然后在demo.c所在的目录新建一个更新的文件touch temp-file编译gcc demo.c 会给出这样的警告信息 warning: current file is older than temp-file
如果当前文件比指定的文件新则不给出任何警告信息.

还可以在在#pragma中给添加自定义的警告信息.
e.g.
#pragma GCC dependency "temp-file" "demo.c needs to be updated!"
1.c:27:38: warning: extra tokens at end of #pragma directive
1.c:27:38: warning: current file is older than temp-file
注意后面新增的警告信息要用""引用起来否则gcc将给出警告信息.

(2) #pragma GCC poison token(s)
若源代码中出现了#pragma中给出的token(s), 则编译时显示警告信息它一般用于在调用你不想使用的函数时候给出出错信息.
e.g.
#pragma GCC poison scanf
scanf("%d", &a);
warning: extra tokens at end of #pragma directive
error: attempt to use poisoned "scanf"
注意如果调用了poison中给出的标记那么编译器会给出的是出错信息关于第一条警告我还不知道怎么避免""token(s)引用起来也不行.

(3) #pragma GCC system_header
#pragma GCC system_header直到文件结束之间的代码会被编译器视为系统头文件之中的代码系统头文件中的代码往往不能完全遵循C标准所以头文件之中的警告信息往往不显示(除非用 #warning显式指明).
(这条#pragma语句还没发现用什么大的用处)

由于#pragma不能用于宏扩展所以gcc还提供了_Pragma:
e.g.
#define PRAGMA_DEP #pragma GCC dependency "temp-file"
由于预处理之进行一次宏扩展采用上面的方法会在编译时引发错误要将#pragma语句定义成一个宏扩展应该使用下面的_Pragma语句:
#define PRAGMA_DEP _Pragma("GCC dependency /"temp-file/"")
注意()中包含的""引用之前引该加上/转义字符.

9, #, ##
###用于对字符串的预处理操作所以他们也经常用于printf, puts之类的字符串显示函数中.
#用于在宏扩展之后将tokens转换为以tokens为内容的字符串常量.
e.g.
#define TEST(a,b) printf( #a "<" #b "=%d/n", (a)<(b));
注意#只针对紧随其后的token有效!
##用于将它前后的两个token组合在一起转换成以这两个token为内容的字符串常量注意##前后必须要有token.
e.g.
#define TYPE(type, n) type n

之后调用
TYPE(int, a) 1;
TYPE(long, b) 1999;
将被替换为:
int 1;
long 1999;

(10) #warning, #error
#warning, #error分别用于在编译时显示警告和错误信息格式如下:
#warning tokens
#error tokens
e.g.
#warning "some warning"
注意#error#warning后的token要用""引用起来!
(gcc如果给出了warning, 编译继续进行但若给出了error, 则编译停止若在命令行中指定了 -Werror, 即使只有警告信息也不编译)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值