• 程序的编译(预处理操作)+链接


    目录

    前言:

    1. 程序的翻译环境和执行环境:

     2. 详解编译+链接

    翻译环境:

    1.预处理:(文本操作)

    2.编译:

    3.汇编:

    4.链接:

    运行环境

    预处理详解 :

    #define的详解 

    #define定义宏:

     #define的替换规则:

    #和## 

    带有副作用的宏参数

    宏和函数的对比

    条件编译:

     文件的包含

    头文件被包含的方式

    总结:


    前言:

    编译是将源代码转换为目标代码的过程,目标代码是二进制指令集,可以直接在计算机上执行。编译器将源代码逐行翻译成目标代码,常见的编译器有gcc、clang等。

    链接是将编译器生成的目标代码合并成可执行文件的过程。当多个源文件进行编译后,会生成多个目标文件,链接器将这些目标文件合并成一个可执行文件,并处理各种符号表和重定向等问题,使得程序能够正常运行。常见的链接器有ld、lld等。

    在编译时,编译器通常需要使用头文件和库文件。头文件是一系列函数和变量的声明,通常以.h为后缀,可以让编译器知道这些函数和变量的定义。库文件是一些二进制代码的集合,通常以.a或.so为后缀,包含了很多已经编译好的函数和变量的实现,可以加速编译和链接的过程。

    编译和链接是程序开发过程中的重要步骤,它们决定了程序是否能够正常运行和性能是否达到要求。

    1. 程序的翻译环境和执行环境

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

    1.翻译环境,在这个环境中源代码被转换为可执行的机器指令。

    2.执行环境,用于实际执行代码。

    程序的翻译环境是指将程序源代码转换为可执行文件的过程中使用的工具、程序和规则,也称为编译器。翻译环境主要包括以下几个方面:

    1. 编辑器:用于编辑程序代码的工具,如记事本、Sublime Text、Visual Studio等。

    2. 预处理器:处理代码中的预处理指令,如#include、#define等。

    3. 编译器:将源代码转换为目标代码的程序,如GCC、Clang等。

    4. 链接器:将目标代码与库文件链接成可执行文件,如ld、MSVC Linker等。

    程序的执行环境是指程序在运行时所依赖的系统环境,包括硬件平台、操作系统、运行时库等。执行环境主要包括以下几个方面:

    1. 操作系统:提供程序运行所需的系统调用和资源管理,如Windows、Linux、macOS等。

    2. 运行时库:提供程序所需的函数和服务,如C++标准库、OpenGL库等。

    3. 硬件平台:提供程序运行所需的处理器、内存、输入输出设备等,如x86、ARM等。

    4. 外部设备:提供程序所需的输入输出接口,如鼠标、键盘、显示器等。

     2. 详解编译+链接

    翻译环境:

    1.组成一个程序的每个源文件通过编译过程分别转换成目标代码(object code)。

    2.每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序。

    3.链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人 的程序库,将其需要的函数也链接到程序中。  

    windows标准环境下生成的目标文件是xxx.obj

    linux环境下生产的目标文件是xxx.o

    连接器同事回应如标准C函数库中任何被该程序调用的函数,而且可以搜索程序员个人的程序库,将其需要的函数也链接到程序中。

    1.预处理:(文本操作)

    1.将程序中的注释转换为空格。

    2.头文件的包含。

    3.#define符号的替换。

    2.编译:

    将C语言代码翻译成汇编代码。

    1.词法分析

    2.语法分析

    3.语义分析

    4.符号汇总

    3.汇编:

    把汇编代码翻译成二进制的指令,生成目标文件和符号表。

    4.链接:

    链接目标文件和链接库生成可执行程序(二进制程序)

    1.合并段表

    2.符号表的合并和重定位

    运行环境

    程序执行的过程:

     1. 程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序 的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。

    2. 程序的执行便开始。接着便调用main函数。

    3. 开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回 地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程 一直保留他们的值。

    4. 终止程序。正常终止main函数;也有可能是意外终止。

    预处理详解 :

    1.预定义符号:

    __FILE__ 进行编译的源文件

    __LINE__文件当前行号

    __DATE__文件被编译的日期

    __TIME__文件被编译的时间

    __STDC__如果遵循ANSIC,其值为1,否则不存在。

    以上均是语言内置的。

    举例:

    1. #define _CRT_SECURE_NO_WARNINGS 1
    2. #include
    3. int main()
    4. {
    5. printf("file:%s\nline:%d\ndate:%s\ntime:%s\n",
    6. __FILE__,
    7. __LINE__,
    8. __DATE__,
    9. __TIME__);
    10. return 0;
    11. }

    输出结果:

    #define的详解 

    语法: #define name stuff

    是一种预处理指令

    内容包括:

    #define定义常量

    #define定义宏

    在我们之前实现通讯录,扫雷和三子棋中,都运用了#define定义一些变量,这些就是#define定义常量。

    博客可以参考:

    运用动态内存实现通讯录(增删查改+排序)-CSDN博客

    C语言实现《扫雷》_无双@的博客-CSDN博客

    C语言实现《三子棋》游戏-CSDN博客

    #define定义宏:

    它是一种预处理指令,用于将标识符与特定的文本替换关联起来。这允许您在代码中创建符号常量或简单的代码替换。#define的一般语法如下:

    #define 宏名称 替换文本

    宏名称是您要定义的标识符,替换文本是您希望在代码中使用该标识符时进行替换的文本。

    下面是一个示例,演示如何使用#define定义宏:

    1. #include
    2. #define PI 3.14159265
    3. #define SQUARE(x) (x * x)
    4. int main()
    5. {
    6. double radius = 5.0;
    7. double area = PI * SQUARE(radius);
    8. printf("圆的面积是:%lf\n", area);
    9. return 0;
    10. }

    在这个示例中,#define用于定义两个宏:PISQUARE(x)PI被定义为一个替换文本,每次在代码中出现PI时,它都会被替换为3.14159265。SQUARE(x)是一个宏函数,它接受一个参数x,并返回x的平方。

    main函数中,我们使用这些宏来计算圆的面积,而不是直接使用硬编码的值。这样,如果需要更改精度或其他参数,只需更新宏定义,而不必在整个代码中查找和更改每个引用。

    注意:宏替换是简单的文本替换,没有类型检查,因此要谨慎使用宏,确保替换文本不会导致不希望的副作用。

    1.宏参数和#define定义中可以出现其它#define定义的符号,但是对于宏,不能出现递归。

    2.当预处理搜索#define定义的符号时,字符串常量的内容并不被搜索。

    这里要注意的是,我们在定义宏时,最好对每一个变量进行括号操作。

    代码如下:

    1. #define SQUARE(X) (X*X)
    2. int main()
    3. {
    4. printf("%d\n", SQUARE(5));
    5. printf("%d\n", SQUARE(5+1));
    6. return 0;
    7. }

     我们预期运行printf(“%d\n”,SQUARE(5+1))答案应当是36。

    可是最终结果却是:11

    因为实际上宏完成的操作是:

    5+1 * 5+1 = 11

    所以我们对以上代码进行修改,应当为:

    1. #define SQUARE(X) (X)*(X)//(X*X)
    2. int main()
    3. {
    4. printf("%d\n", SQUARE(5));
    5. printf("%d\n", SQUARE(5+1));
    6. return 0;
    7. }

     如此输出结果:

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

     #define的替换规则:

    在程序中扩展#define定义符号和宏时,需要涉及几个步骤。

    1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先 被替换。

    2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值替换。

    3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。

    如果是,就重复上 述处理过程。

    #和## 

    在编程和计算机领域中,"#" 和 "##" 通常用于表示不同的概念,具体取决于编程语言或上下文。以下是它们可能的用途和区别:

    1. "#"(井号):

      • 在许多编程语言中,"#" 通常用于表示注释。注释是程序中的文本,通常不会被编译或执行,用于解释代码的目的。例如,在Python中,您可以使用 "#" 来添加注释。
      • 在一些编程语言中,"#" 还可以用于预处理指令,例如在C和C++中,用于包含头文件。
    2. "##":

      • "##" 通常用于宏定义和预处理器指令中。预处理器是在代码编译之前执行的一种处理步骤,通常用于代码生成或替换。在C和C++等语言中,"##" 用于将两个标识符连接在一起,通常用于宏定义。
      • 例如,在C中,以下是一个使用"##" 的宏定义的示例:#define CONCAT(x, y) x##y 这将使 CONCAT(a, b) 展开为 ab。#define CONCAT(x, y) x##y 这将使 CONCAT(a, b) 展开为 ab。

    1. #define CONCAT(x, y) x##y
    2. 这将使 CONCAT(a, b) 展开为 ab。

    总之,"#" 通常用于注释和一些预处理指令,而 "##" 主要用于宏定义和预处理器中的标识符连接。具体用法和含义可能会因编程语言而异,因此请查阅特定编程语言的文档以获取详细信息。

    举例:

    1. #define PRINT(n, format) printf("the value of "#n" is "format"\n",n)
    2. int main()
    3. {
    4. int a = 10;
    5. PRINT(a,"%d");
    6. float f = 4.5f;
    7. PRINT(f, "%f");
    8. return 0;
    9. }

    以上是关于#号的

    关于##:把位于它两边的符号合成一个符号

    例子:

    1. #define CAT(v,n) v##n
    2. int main()
    3. {
    4. int value10 = 100;
    5. printf("%d\n", CAT(value, 10));
    6. return 0;
    7. }

     输出结果:

    带有副作用的宏参数

     当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能 出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。

    例子:

    1. #define MAX(x,y) ((x)>(y)?(x):(y))
    2. int main()
    3. {
    4. int a = 3;
    5. int b = 5;
    6. int m = MAX(a++, b++);
    7. //int m = MAX(((a++)>(b++)?(a++):(b++)));
    8. printf("%d\n", m);
    9. printf("%d\n", a);
    10. printf("%d\n", b);
    11. return 0;
    12. };

    我们预期的输出结果应当为:

    5

    3

    5

    可是最终的输出结果:

     

    这个的原因好理解,在这里我不做过多的赘述。

    因此带有副作用的参数,会对程序最后的值产生较大的影响。

    宏和函数的对比

    1. 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比 函数在程序的规模和速度方面更胜一筹。

    2. 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之 这个宏怎可以适用于整形、长整型、浮点型等可以用于>来比较的类型。宏是类型无关的。

    当然和宏相比函数也有劣势的地方:

    1. 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序 的长度。

    2. 宏是没法调试的。

    3. 宏由于类型无关,也就不够严谨。

    4. 宏可能会带来运算符优先级的问题,导致程容易出现错。 宏有时候可以做函数做不到的事情。比如:宏的参数可以出现类型,但是函数做不到。

    为了区分函数和宏,

    我们在日后写宏和函数时,应当运用全部大写写宏。

    函数名则不需要全部大写。

    #undef是移除一个宏定义。

    条件编译:

    条件编译是一种编译技术,它可以在编译代码时根据指定的条件选择性地包含或排除某些代码块。条件编译通常使用预处理指令,在代码编译之前进行处理。这些指令可以根据定义的条件来判断是否编译某些代码,从而实现对不同操作系统、硬件平台或其他环境的适配,或者实现对不同版本或功能的控制。常见的条件编译指令包括 #ifdef、#ifndef、#if、#else、#elif 和 #endif 等。条件编译可以帮助程序员实现更好的程序控制和代码重用。

    下面是一个代码示例,其中使用了条件编译指令来控制编译器在编译时包含哪些代码:

    1. #include
    2. #define DEBUG_MODE 1
    3. int main() {
    4. int x = 6;
    5. int y = 2;
    6. #ifdef DEBUG_MODE
    7. printf("Debug mode is ON\n");
    8. #endif
    9. #ifndef DEBUG_MODE
    10. printf("Debug mode is OFF\n");
    11. #endif
    12. #if DEBUG_MODE
    13. printf("x = %d, y = %d\n", x, y);
    14. #endif
    15. #if defined(DEBUG_MODE) && DEBUG_MODE == 1
    16. printf("The sum of x and y is %d\n", x + y);
    17. #endif
    18. return 0;
    19. }

    在上面的代码中,我们使用了 #ifdef#ifndef,和 #if 等条件编译指令来控制编译器在编译时是否包含哪些代码。在 main 函数中,我们使用了 #ifdef 指令来检查 DEBUG_MODE 宏是否已经定义,如果是,就会输出 "Debug mode is ON";如果 DEBUG_MODE 没有被定义,我们就使用 #ifndef 指令来输出 "Debug mode is OFF"。另外,我们还使用了 #if#endif 指令来控制是否输出变量 xy 的值,以及它们的和。#if 指令可以接受一个表达式,只有当表达式为真时,才会编译所包含的代码。在这个例子中,我们使用了 defined 运算符来检查 DEBUG_MODE 是否已经被定义,并将其与 1 进行比较。如果两者相等,我们就会输出变量 xy 的和。

    这个例子中,条件编译指令可以让我们根据需要选择需要编译的代码。如果 DEBUG_MODE 宏已经被定义,我们就会得到更多的输出信息,从而方便调试程序。否则,我们只会得到必要的输出信息,节省了程序的运行时间。

     文件的包含
     

     我们已经知道, #include 指令可以使另外一个文件被编译。就像它实际出现于 #include 指令的地方 一样。 这种替换的方式很简单: 预处理器先删除这条指令,并用包含文件的内容替换。 这样一个源文件被包含10次,那就实际被编译10次。

    头文件被包含的方式

    本地文件包含

    #include"XXX.h"

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

    库文件包含

    #include

    查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。 这样是不是可以说,对于库文件也可以使用 “” 的形式包含?

    答案是肯定的,可以。

    但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了。

    嵌套文件包含是指在一个文件中包含了另一个文件,而被包含的文件中又包含了另一个文件,以此类推,形成了多层的文件包含关系。

    例如,假设有一个文件A.php,其中包含了文件B.php,而文件B.php又包含了文件C.php,那么就形成了三层文件包含关系,即A.php -> B.php -> C.php。

    嵌套文件包含的好处是可以实现代码的复用,减少重复的代码编写,提高开发效率。但是,如果嵌套层数太多,会导致代码的可读性变差,也可能会影响性能,因为每次文件包含都会增加文件读取和解析的开销。因此,在实际开发中,需要根据具体情况来决定是否使用嵌套文件包含。

    为了避免头文件的重复引入,我们可以运用头文件

    #pragma once

    总结:

    本文对C语言程序的编译和链接以及之中的预处理进行了初步的了解。

    学习完后可以复习一下,并对其中进行精读。

    记住

    做而言不如起而行。

    Action speak louder than words

    以下是本文的代码:

    Pretreatment_CSDN/Pretreatment_CSDN/test.c · 无双/test_c_with_X1 - Gitee.com

  • 相关阅读:
    21天学习挑战赛-线性表(下)
    ACWing每日一题.3511
    大数据ClickHouse进阶(八):ClickHouse的with子句
    网络安全——使用Linux系统命令对后门端口进行查杀
    NPDP产品经理知识(产品创新管理)
    DNS欺骗实验过程和分析
    努力前行,平凡的我们也能画出一条星光闪耀的轨迹——人大女王金融硕士项目
    线程-API
    Ipa Guard软件介绍:启动界面和功能模块全解析,保护你的iOS应用源码
    怎么做口碑营销?口碑营销有哪些方式?
  • 原文地址:https://blog.csdn.net/weixin_72917087/article/details/133900455