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


    一:预处理过程

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

             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

  • 相关阅读:
    关于程序与语言
    最新笔记请查看
    MySQL 性能优化
    k8s flannel无法跨主机ping通pod的解决方案
    k8s 使用kubeadm部署k8s集群初体验
    MySQL 锁和可重复读的隔离级别结合起来的一个示例(来自MySQL45讲第8章)
    MySQL 可重复读 vs 读提交
    Jenkins配置Linux节点,通过ssh方式在Linux节点自动拉取github代码并执行
    AppScan 使用
    Linux 动态链接库和静态库示例
  • 原文地址:https://www.cnblogs.com/gqtcgq/p/7247126.html
Copyright © 2020-2023  润新知