• 【C语言 |预处理指令】预处理指令详解(包括编译与链接)


    目录

    一、编译与链接

     1.翻译环境

              -预处理

              -编译

              -汇编

              -链接

    2.执行环境

    二、预定义符号

    三、#define定义常量

    四、#define定义宏

    五、带有副作用的宏参数

    六、宏替换的规则

    七、 宏函数的对比

    八、#和##

    1.#运算符

    2.##运算符

    九、命名约定

    十、#undef

    十一、 命令行定义

    十二、 条件编译

    十三、 头文件的包含

    1.本地头文件包含

    2.库文件包含

    十四、 其他预处理指令


    一、编译与链接

    在ANSI C的任何⼀种实现中,存在两个不同的环境

    第一种呢就是翻译环境,顾名思义就是将源代码被转换为可执行的二进制指令

    第二种呢就是执行环境,可用于实际执行代码,并且输出结果

    然后我们再来说翻译环境,是如何将一段代码转换为可执行的二进制指令的呢

    其实编译这一部分又分为了预处理(预编译),编译,汇编 

     在一个程序中可能会有多个.c文件,这些文件会单独的经过编译处理生成对应的目标文件

    Windows环境下生成的目标文件后缀为.obj,Linux环境下生成的目标文件为.o

    多个目标文件跟链接库一起经过链接器的处理最终生成可执行程序

    链接库呢,它是指运行时库(支持程序运行的基本函数集合)第三方库

    知道了上面的操作,我们就可以展开,成为了以下这个过程

     1.翻译环境

              -预处理

    在预处理阶段,源文件和头文件会被处理为.i为后缀的文件,处理规则如下

    • 将所有的#define删除,并且展开所有宏定义
    • 处理所有的条件编译指令
    • 处理#include 预编译指令,将包含的头文件的内容插⼊到该预编译指令的位置
    • 删除所有的注释
    • 添加行号文件名标识, ⽅便后续编译器生成调试信息等
    • 或保留所有的#pragma的编译器指令,编译器后续会使用
    经过预处理后的.i文件中不再包含宏定义,因为宏已经被展开并且包含的头文件都被插入到.i文件中

              -编译

    编译过程就是将预处理后的文件进行一系列的:词法分析、语法分析、语义分析及优化,⽣成相应的汇编代码⽂件
    1. 将源代码程序被输⼊扫描器进行词法分析,把代码中的字符分割成⼀系列 的记号(关键字、标识符、字⾯量、特殊字符等)
    2. 接下来语法分析器,将对扫描产⽣的记号进⾏语法分析,从而产生语法树。这些语法树是以表达式为节点的树
    3. 语义分析器来完成语义分析,即对表达式的语法层⾯分析。静态语义分析通常包括声明和类型的匹配,类型的转换等。这个阶段会报告错误的语法信息。

              -汇编

    汇编器是将汇编代码转转变成机器可执行的指令,每⼀个汇编语句⼏乎都对应⼀条机器指令
    就是根据汇编指令和机器指令的对照表⼀⼀的进行翻译,也不做指令优化

              -链接

    链接是⼀个复杂的过程,链接的时候需要把一堆文件链接在一起才生成可执行程序
    链接过程主要包括:地址和空间分配,符号决议和重定位等这些步骤。
    链接解决的是⼀个项⽬中多个文件、多模块之间互相调⽤的问题
    地址修正的过程也被叫做:重定位

    2.执行环境

    • 程序必须载⼊内存中。在有操作系统的环境中:⼀般这个由操作系统完成。
    • 在独⽴的环境中,程序的载⼊必须由手工安排,也可能是通过可执⾏代码置⼊只读内存成。
    • 程序的执⾏便开始。接着便调⽤main函数。
    • 开始执⾏程序代码。这个时候程序将使⽤⼀个运⾏时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使⽤静态(static)内存,存储于静态内存中的变量在程序的整个执行过程⼀直保留他们的值。
    • 终⽌程序。正常终⽌main函数;也有可能是意外终止

    二、预定义符号

    C语⾔设置了⼀些预定义符号,可以直接使用,预定义符号也是在预处理期间处理的,所以运行速度更快
    1. __FILE__ //进⾏编译的源⽂件
    2. __LINE__ //⽂件当前的⾏号
    3. __DATE__ //⽂件被编译的⽇期
    4. __TIME__ //⽂件被编译的时间
    5. __STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义


    三、#define定义常量

    基本语法 

    # define name stuff

    当然所定义的类型没有限制,可以为了定义值,可以为了替换复杂名字,也可以为了省事,下面这几种都是正确的定义方法: 

    1. #define MAX 1000
    2. #define float f //为 float这个关键字,创建⼀个简短的名字
    3. #define forever for(;;) //⽤更形象的符号来替换⼀种实现(死循环)
    4. #define CASE break;case //在写case语句的时候⾃动把 break写上
    5. #define DEBUG_PRINT printf("file:%s\tline:%d\t \
    6. date:%s\ttime:%s\n" ,\
    7. __FILE__,__LINE__ , \
    8. __DATE__,__TIME__ );

     /如果定义的过长,可以分成几行写,除了最后一行外,后⾯都加⼀个反斜杠  \  (续行符)

     

     所以宏的定义可以各式各样,给了我们很大的自由度,使我们能尽情去发挥自己想象

    那么还有一个问题在定义定义的标识符的时候需不需要加;呢,答案是否定的,就比如说

    1. #define PR printf("hehe");
    2. int main()
    3. {
    4. PR; //加了分号就相当于 printf("hehe");; 容易发生错误
    5. return 0;
    6. }

    为了避免上述的这个错误,我们定义的标识符的时候不需要加;


    四、#define定义宏

    #define 机制包括了⼀个规定,允许把参数替换到⽂本中,这种实现通常称为宏(macro)或定义宏(define macro)
     #define name( list ) stuff

    举个例子来说明

    1. #define SUPP( x ) x * x
    2. int main()
    3. {
    4. SUPP(2,3);
    5. return 0;
    6. }

    SUPP就是我们定义的一个宏,将宏置于函数内部(等预处理的时候,会自动替换成表达式 x * x)

    但同时会存在一些问题

    1. #define SUPP( x ) x * x
    2. int main()
    3. {
    4. SUPP(2); // 2 * 2 == 4
    5. SUPP(2+1); // 2 + 1 * 2 + 1 == 5
    6. 10 * SUPP(5+2); //10 * 5 + 2 * 5 + 2 == 62
    7. return 0;
    8. }

    因为被定义的宏是预处理阶段所进行的,在预处理的时候直接替换函数中的表达式,所以难免会有许多操作符,优先级之类的问题,这个解决问题的方法就是在表达式加上对括号就解决了

    #define SUPP( x) ( ( x ) + ( x ) )
    所以用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用。

    五、带有副作用的宏参数

    副作⽤就是表达式求值的时候出现的永久性效果,就好比说
    1. a = 1;
    2. //a赋值的同时自己的值也改变了
    3. b = a++; // a = 2,b = 1;
    4. a+1;//不带副作⽤
    5. a++;//带有副作⽤

    拿下面这个例子来举例子

    1. #define MAX(a, b) ( (a) > (b) ? (a) : (b) )
    2. ...
    3. x = 5;
    4. y = 8;
    5. z = MAX(x++, y++);
    6. printf("x=%d y=%d z=%d\n", x, y, z);//输出的结果是什么?

    还记得我说了,宏是在预处理阶段替换宏为函数中的表达式:替换完了为

    1. x = 5,y = 8;
    2. MAX(x++,y++) 替换为 ((x++)>(y++)?(x++)(y++))
    3. x=6 y=9 y=10
    4. 5 > 8假,执行y++

    六、宏替换的规则

    1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
    2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
    3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。
    宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
    当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索

     


    七、 宏函数的对比

    宏通常被应用于执行简单的运算,比如在两个数中找出较⼤的⼀个时,更有优势⼀些
    1. #define MAX(a, b) ((a)>(b)?(a):(b))
    2. //定义宏和函数的两种方式
    3. int Max(a,b)
    4. {
    5. return ((a)>(b)?(a):(b));
    6. }

     我们从以下几个方面来分析宏 和 函数 的优缺点:

    1. 代码长度:#define所定义的宏,每次使用的时候都会被插入到程序中,除了特别小的宏以外,程序的长度会大幅度增长;而函数的代码只出现一个地方,调用都用同一份
    2. 执行速度:#define所定义的宏更快;二函数存在着调用和返回等额外的步骤速度会慢一些
    3. 操作符优先级:#define所定义的宏求值是在上下文的环境中,结果不可预测 会存在着很多的问题;而函数的参数只在函数调用时候将结果传给函数,表达式课预测
    4. 带有副作用的参数:#define所定义的宏参数可能会被替换多个位置,多次被计算,对值有着不可预测的结果;函数只在传参的时候求值易控制
    5. 参数类型:#define所定义的宏与类型无关,只要操作是合法的,可以适用于任何参数类型;函数的参数是与类型有关,如果类型不同,所需的函数也不同
    6. 调试:宏是不能调试的;函数是可以逐句逐条调试
    7. 递归:宏是不能递归的;函数是可以递归的

    八、#和##

    1.#运算符

    先给大家补充一个知识点,字符串中包含的字符串两个会合成一个字符串

    1. printf("haha""hehe");
    2. //两个输出的结果相同
    3. printf("hahahehe");
    #运算符将宏的⼀个参数转换为字符串字面量。仅允许出现在带参数的宏的替换列表中“字符串化
    1. #define PRI(n) printf("the value of "#n " is %d", n);
    2. int main()
    3. {
    4. PrT(6); //printf("the value of "#n " is %d", n);
    5. return 0;
    6. }

    结果为the value of n is 6

    不难发现#n,将转换成了一个字符串


    2.##运算符

    把位于它两边的符号合成⼀个符号,它允许宏定义从分离的文本片段创建标识符。
    ## 被称为记号粘合
    大家想想看如果要实现一个结果,不同的数据类型就得写不同的函数
    1. int int_max(int x, int y)
    2. {
    3. return x>y?x:y;
    4. }
    5. float float_max(float x, float y)
    6. {
    7. return x>yx:y;
    8. }

    那如果使用##,一切都会变的很简单

    1. #define GENERIC_MAX(type) \
    2. type type##_max(type x,typey) \
    3. { \
    4. return (x>y?x:y); \
    5. } \
    1. GENERIC_MAX(int); //替换到宏体内后int##_max ⽣成了新的符号 int_max做函数名
    2. GENERIC_MAX(float); //替换到宏体内后float##_max ⽣成了新的符号 float_max做函数名

    在预处理阶段,预处理中的所有type全部被替换 


    九、命名约定

    把宏名全部⼤写   ,函数名不要全部⼤写

    十、#undef

    这条指令用于移除⼀个宏定义
    1. #undef NAME
    2. //如果现存的⼀个名字需要被重新定义,那么它的旧名字⾸先要被移除。

    可以看到 定义了一个MAX,正常打印完毕以后,#undef 移除这个宏定义,再次打印MAX就会报错


    十一、 命令行定义

    许多C 的编译器提供了⼀种能力,允许在命令行中定义符号。用于启动编译过程

    十二、 条件编译

    在编译⼀个程序的时候我们如果要将⼀条语句(⼀组语句)编译或者放弃是很⽅便的。因为我们有条件编译指令
    1. #if 常量表达式
    2. //...
    3. #endif
    4. //常量表达式由预处理器求值。
    5. 如:
    6. #define __DEBUG__ 1
    7. #if __DEBUG__
    8. //..
    9. #endif
    10. #if 常量表达式
    11. //...
    12. #elif 常量表达式
    13. //...
    14. #else
    15. //...
    16. #endif
    17. #if defined(symbol)
    18. #ifdef symbol
    19. #if !defined(symbol)
    20. #ifndef symbol

    十三、 头文件的包含

    1.本地头文件包含

    #include "filename"

    拿双引号引用

    查找策略:先在源⽂件所在⽬录下查找,如果该头文件未找到,编译器就像查找库函数头文件⼀样在标准位置查找头文件,如果找不到就提示编译错误。

    2.库文件包含

    #include 

    拿单尖括号引用

    查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。
    也可以用双引号引用库文件,但是查找的效率就低些,这样也不容易区分是库文件还是本地文件
    当一个文件中包含多个头文件,重复的头文会被在预处理的时候多次替换,大大降低了效率,所以我们在声明一个头文件的前面不妨可以加上这段代码
    1. #ifndef __TEST_H__ \\当未定义这个头文件时才会执行下面
    2. #define __TEST_H__
    3. .....
    4. #endif
    #pragma once

    这两种都可以避免头文件重复引进


    十四、 其他预处理指令

    # error
    # pragma
    # line
    ...
    后面再给大家介绍

    希望对你有帮助 

  • 相关阅读:
    golang将pcm格式音频转为mp3格式
    Date/Timestamp/String/LocalDate/LocalDateTime
    运动想象 EEG 信号分析
    一次关于引入自定义JAR包JDK版本问题总结
    Docker 安装Minio
    数组反转(LeetCode)
    国网云(华为组件)使用
    领悟《信号与系统》之 信号与系统概论
    第二十八章 车道线检测中的论文梳理(车道线感知)
    前端架构师技术之Sass
  • 原文地址:https://blog.csdn.net/YKX0000/article/details/138164293