一:预处理过程
预处理器将进行宏替换、条件编译和包含指定的文件。以“#”开头的命令行就是预处理器处理的对象。这些命令行可以出现在任何地方,其作用可延续到所在翻译单元的末尾。每一行都会单独进行分析。预处理过程,在逻辑上可划分为下面几个连续的阶段:
1:进行三字符序列替换
三字符组(trigraph)与双字符组(Digraph)是3个或者2个字符的序列,在编译器预扫描源程序时被替换为单个字符。以解决某些键盘不能输入某些编程必须的字符问题。
C语言的源程序的字符集是基于7位ASCII码字符集,是ISO 646-1983 不变代码集的一个超集。因此某些国家的键盘就难以输入C语言的一些运算符。
为解决上述的C语言源代码输入问题,C语言标准规定预处理器在扫描处理C语言源文件时,替换下述的3字符出现为1个字符:
三字符组 |
替换为 |
??= |
# |
??/ |
|
??' |
^ |
??( |
[ |
??) |
] |
??! |
| |
??< |
{ |
??> |
} |
??- |
~ |
比如代码:printf("??= ");将会输出”#”。GCC需要-trigraphs选项,才支持三字符组。但会给出编译警告。
1994年公布了一项C语言标准的修正案,引入了更具有可读性的5个双字符组。这也包括进了C99标准。
双字符组 |
替换为 |
<: |
[ |
:> |
] |
<% |
{ |
%> |
} |
%: |
# |
不同于三字符组在源文件的任何出现都会被预处理器替换,双字符如果出现在字符串字面值、字符常量、程序注释中将不被替换。双字符组的替换发生在编译器对源程序的tokenization阶段(即识别出关键字、标识符等,类似于自然语言的“断词”),仅当双字符组作为一个token或者token的组成部分时(如%:%:被替换为预处理运算符##),双字符组才被替换为单字符。(以上内容出自维基百科)
b:将以反斜杠””结尾的指令行中,末尾的””,和其后的换行符删除掉,从而可以把若干指令行合并为一行。
c:将程序分成用空白符分隔的记号。注释将被替换为一个空白符。接着执行预处理指令,进行宏扩展。
d:将字符常量和字符串字面值中的转义字符序列,替换为等价字符,然后,把相邻的字符串字面值连接起来。
e:收集必要的程序和数据,并将外部函数和对象的引用与其定义相连接,翻译经过以上处理得到的结果,并进行链接过程。
二:文件包含
#include “filename” or #include <filename>
如果文件名用引号引起来,则在源文件所在的位置查找该文件;如果在该位置没有找到文件,或者如果文件名使用尖括号<与>括起来的,则将根据相应的规则查找该文件。
如果需要查看编译时查找头文件的默认搜索路径,可以使用gcc的-v选项,或者直接使用命令cpp -v。cpp就是预编译器的名字,当前预编译器多数情况下已经集成到编译器中了(the "real" cpp is nowadays integrated into the 'cc1','cc1plus' etc. "real" compilers)。比如下面的例子:
#include <sys/select.h> #include <stdio.h> int main() { int a = 3; }
编译:gcc -v -o 2 2.c,输出:
......
/usr/lib/gcc/i686-linux-gnu/4.9/cc1 -quiet -v -imultiarch i386-linux-gnu 2.c -quiet -dumpbase 2.c-mtune=generic -march=i686 -auxbase 2 -version -fstack-protector-strong-Wformat -Wformat-security -o /tmp/cc8VGFlp.s
......
GGC heuristics: --param ggc-min-expand=100--param ggc-min-heapsize=131072
ignoring nonexistent directory"/usr/local/include/i386-linux-gnu"
ignoring nonexistent directory"/usr/lib/gcc/i686-linux-gnu/4.9/../../../../i686-linux-gnu/include"
#include "..." search starts here:
#include <...> search starts here:
/usr/lib/gcc/i686-linux-gnu/4.9/include
/usr/local/include
/usr/lib/gcc/i686-linux-gnu/4.9/include-fixed
/usr/include/i386-linux-gnu
/usr/include
End of search list.
......
省略了其他阶段的输出,主要展示了编译器搜索头文件时的路径。
三:宏替换
#define 名字 替换文本
其中,名字与变量名的命名方式相同,替换文本可以是任意字符串。通常#define指令占一行,替换文本是#define指令行尾部的所有剩余内容,但是也可以通过反斜杠将一个较长的宏定义分成若干行。
用#define指令定义同一名字是错误的,除非第二次定义的替换文本与第一相同。
#define指令定义的名字,它的作用域从其定义点开始,到被编译的源文件的末尾处结束。宏定义也可以使用前面出现的宏定义。宏替换对于字符串中的记号不起作用,比如如果YES是通过#define定义过的名字,则在printf(“YES”)中,不执行宏替换。
可以通过#undef指令,取消名字的宏定义。将#undef用于未知标示符(也就是未用#define指令定义的标示符),并不会导致错误。
1:#将参数字符串化。在替换文本中,如果参数名以”#”作为前缀,则结果将被扩展为:由实际参数替换该参数的带引号的字符串。比如:
#define STR(s) #s printf(STR(pele) “ ”); //输出pele
如果实参中有双引号或反斜杠,则将会替换为”或\。所以,替换后的字符串是合法的字符串常量。
注意,#后面必须跟宏参数,比如下面就是错误的:
#define STR(ARG) #arg //error: '#' is not followed by a macro parameter
正确的写法是:
#define STR(ARG) #ARG
b:##是连接符,如果替换文本中的参数与##相邻,则该参数被实际参数替换时,##与前后的空白符都将删除,比如:
#define paste(front, back) front##back paste(name, 1)将替换为name1
#define VAR(argu) abc ## 3 ## def int VAR(L) = 4; printf("abc3def is %d ", abc3def); //abc3def is 4
注意,如果在宏定义中,使用##连接字符串是不对的,比如:
#define PERROR(ARG) perror(#ARG ##"ERROR") PERROR(SOCKET) //error: pasting""SOCKET"" and "" ERROR"" does not givea valid preprocessing token
正确的写法是:
#define PERROR(ARG) perror(#ARG "ERROR")
c:注意:凡宏定义里有用'#'或'##'的地方,宏参数是不会再展开的。
#define A 2 #define STR(s) #s #define CONS(a,b) (int)(a##e##b) printf("stris : %s ", STR(A)); 这行会被展开为: printf("stris : %s ", “A”); printf("%s ", CONS(A, A)); 这一行被展开为: printf("%s ", (int)(AeA)); //编译错误
A不会再被展开,解决这个问题的方法很简单,多加一层中间转换宏。加这层宏的用意是把所有宏的参数在这层里全部展开,那么在转换宏里的那一个宏(_STR)就能得到正确的宏参数。
#define A 2 #define _STR(s) #s #define STR(s) _STR(s) #define _CONS(a,b) (int)(a##e##b) #define CONS(a,b) _CONS(a,b) printf("stris : %s ", STR(A)); //输出 str is 2 printf("%d ",CONS(A, A)); //输出:200
四:条件编译
可以使用条件语句对预处理过程进行控制,条件语句的值是在预处理执行的过程中进行计算。整型常量表达式指的是表达式中的操作数都是整数类型的。
每个条件编译指令(#if, #elif,#else, #endif)在程序中均独占一行。
#if语句,对其中的常量整形表达式(其中,不能包含sizeof,类型转换运算符或enum常量)进行求值。若该表达式的值不等于0,则包含其后的各行,直到遇到#endif、#elif或#else语句为止。
在#if中,也可以使用表达式”defined(名字)” 或者”defined 名字”,如果名字已经定义,则其值为1,否则为0。比如为了防止头文件重复包含,可以用下面的形式:
#if !defined(HDR) #define HDR ... #endif
还可以是下面这种形式:
#if ABC printf("ABC "); #else printf("DEF "); #endif
如果之前没有定义宏ABC,或者定义宏ABC为0,则打印DEF,否则,打印ABC
C中,专门定义了两个预处理语句#ifdef和#ifndef,因此,上面的例子也可以用这种形式:
#ifndef(HDR) #define HDR ... #endif
五:其他
#line 常量 “文件名” 或 #line 常量
这样的命令,将使编译器认为:下一行源代码的行号是“常量“,并且,当前的输入文件名是”文件名”。比如下面的代码,将输出:” the file is hh, line is 100” :
#line 100"hh" printf("the file is %s, line is %d ", __FILE__, __LINE__);
#error [用户自定义的错误消息]
当预处理器预处理到#error命令时,将停止编译并输出用户自定义的错误消息。比如下面的代码:
#ifndef A #error no defineA #endif
在编译时,会输出:”error:#error no define A”
__LINE__ 源文件行数
__FILE__ 源文件名字
__DATE__ 编译日期,形式为”Mmm dd yyyy”,比如Oct 272014
__TIME__ 编译时间,形式为”hh:mm:ss”,比如21:46:19
__STDC__ 整型常量1,只有在遵循标准的实现中,该标示符才被定义为1.
参考:
https://gcc.gnu.org/ml/gcc-help/2007-09/msg00205.html
https://gcc.gnu.org/onlinedocs/cpp/Concatenation.html#Concatenation