您的位置:首页 > 其它

预处理:头文件、宏定义、条件编译

2015-08-30 21:13 232 查看
一:预处理过程

预处理器将进行宏替换、条件编译和包含指定的文件。以“#”开头的命令行就是预处理器处理的对象。这些命令行可以出现在任何地方,其作用可延续到所在翻译单元的末尾。每一行都会单独进行分析。预处理过程,在逻辑上可划分为下面几个连续的阶段:

1:进行三字符序列替换

三字符组(trigraph)与双字符组(Digraph)是3个或者2个字符的序列,在编译器预扫描源程序时被替换为单个字符。以解决某些键盘不能输入某些编程必须的字符问题。

C语言的源程序的字符集是基于7位ASCII码字符集,是ISO
646-1983 不变代码集的一个超集。因此某些国家的键盘就难以输入C语言的一些运算符。
为解决上述的C语言源代码输入问题,C语言标准规定预处理器在扫描处理C语言源文件时,替换下述的3字符出现为1个字符:

三字符组
替换为
??=
#
??/
\
??'
^
??(
[
??)
]
??!
|
??<
{
??>
}
??-
~
比如代码:printf("??=\n");将会输出”#”。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) “\n”);		//输出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\n", 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\n", STR(A));
这行会被展开为:
printf("stris : %s\n",  “A”);
 
printf("%s\n", CONS(A, A));  
这一行被展开为:
printf("%s\n", (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\n", STR(A));       //输出 str is 2
printf("%d\n",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\n");
#else
         printf("DEF\n");
#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\n", __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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: