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


一:预处理过程

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

         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 ## defint 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

智能推荐

注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
© 2014-2019 ITdaan.com 粤ICP备14056181号  

赞助商广告