当前位置: 首页 > news >正文

C语言:编译,链接和预处理详解

目录

一.翻译环境和运行环境

二.翻译环境

​编辑 1.预处理(预编译)

(1).#和##运算符

①.#运算符

②## 运算符 

(2).#undef

(3).条件编译 

 ①单分支的条件编译

②多个分支的条件编译 

③判断是否被定义 

​编辑 ④嵌套指令

2.编译

(1).词法分析 

(2).语法分析

(3).语义分析

 3.汇编

4.链接

三.运行环境

 


 

在这之前,我们已经学习了很多的c语言的知识,学习了进行代码的编写和运行,那么,是什么让我们编写完的代码成功的运行和输出我们想要的结果呢?这就是我们今天要讲的内容:编译,链接和预处理详解。

首先,在我们进行对它们进行深入了解之前。我们要先知道什么是翻译环境和运行环境。

一.翻译环境和运行环境

在ANSI C的任何一种实现中,存在两个不同的环境,他们就是翻译环境和运行环境,而我们今天要学习的编译和链接就处在翻译环境当中。

在我们进行代码编写后,我们会形成一个或多个的(.c)文件,计算机会将这些(.c)文件放入到翻译环境中,经过编译,链接,将源代码转换为计算机可执行的机器指令即:二进制指令。之后,放入到Windows环境下(运行环境),他会形成(.exe)为后缀的可执行程序,最后进行输出。

84efa8c19b444894ba2cbb6bfc8e14a7.png 我们一下面图片中的代码为例:

c898c3f0c4414952bee06e85fd5a25d9.png

运行完后,我们在我们文件中就可以发现我们生成的(.exe)文件.

f72b5f5813be47cc99692ff4f604ec21.png

二.翻译环境

那翻译环境是怎么将源代码转换为可执行的机器指令的呢?这里我们就得深入了解一下翻译环境所 做的事情。 

其实翻译环境是由编译和链接两个大的过程组成的,而编译又可以分解成:预处理(有些书也叫预编译、编译、汇编三个过程。

并且在编译和链接中还存在着看不见的编译器(cl.exe)和链接器(link.exe),编译器会将(.c)文件生成对应的(.obj)目标文件(windows环境下)或(.o)目标文件(Linux环境下)。之后目标文件会和链接库在链接器的作用下生成(.exe)的可执行程序

44a9ee26164849cf9dfe1dbe6585c442.png

我们还是一上面的代码为例,去查看是否生成了(.obj)文件:

f9341701205b42f59d44f3187cc17eda.png

 00ffd86883e241b5a4e114dc81177d75.png

我们发现我们不仅成功找到了(.obj)文件,还在寻找的过程中发现了 Add.c文件和test.c文件

总结:

  • 多个.c文件单独经过编译器,编译处理生成对应的目标文件。
  • 注:在Windows环境下的目标文件的后缀是 .obj ,Linux环境下目标文件的后缀是 .o
  • 多个目标文件和链接库一起经过链接器处理生成最终的可执行程序。
  • 链接库是指运行时库(它是支持程序运行方的基本函数集合)或者第三库。

在刚才我们也提到了编译也分为3个部分: 预处理(有些书也叫预编译、编译、汇编三个过程。

f6f3f1b70b3b407eafdb3666f34b2abe.png 1.预处理(预编译)

在预处理阶段,源文件和头文件会被处理成为 .i 为后缀的文件。 在 gcc 环境下想观察⼀下,对 test.c 文件预处理后的.i文件,命令如下:

gcc -E test.c -o test.i

预处理阶段主要处理那些源文件中#开始的预编译指令。比如:#include,#define,处理的规则如下:

  • 将所有的 #define 删除,并展开所有的宏定义。
  • 处理所有的条件编译指令,如: #if、#ifdef、#elif、#else、#endif 。
  • 处理#include 预编译指令,将包含的头文件的内容插入到该预编译指令的位置。这个过程是递归进行的,也就是说被包含的头文件也可能包含其他文件。
  • 删除所有的注释
  • 添加行号和文件名标识,方便后续编译器生成调试信息等。
  • 或保留所有的#pragma的编译器指令,编译器后续会使用。

 我们发现我们的#define,注释都被删除,宏定义也都被展开。939fdbca90b34e98889da4e2dd444f81.png

(1).#和##运算符

①.#运算符

#运算符将宏的一个参数转换为字符串字面量。它仅允许出现在带参数的宏的替换列表中。 #运算符所执行的操作可以理解为“字符串化”

当我们有⼀个变量 int a = 10; 的时候,我们想打印出: the value of a is 10 . 就可以写:

#define PRINT(n) printf("the value of "#n " is %d", n);

而代码就会被预处理为: 

printf("the value of ""a" " is %d", a);

②## 运算符 

## 可以把位于它两边的符号合成一个符号,它允许宏定义从分离的文本片段创建标识符。 ## 被称 为记号粘合

 这样的连接必须产生以个合法的标识符。否则其结果就是未定义的

//宏定义
#define GENERIC_MAX(type) \
type type##_max(type x, type y)\
{ \return (x>y?x:y); \
}
GENERIC_MAX(int) //替换到宏体内后int##_max ⽣成了新的符号 int_max做函数名
GENERIC_MAX(float) //替换到宏体内后float##_max ⽣成了新的符号 float_max做函数名
int main()
{//调⽤函数int m = int_max(2, 3);printf("%d\n", m);float fm = float_max(3.5f, 4.5f);printf("%f\n", fm);return 0;
}

(2).#undef

这条指令用于移除一个宏定义。 

 cc985e73f0f04a778bbc42ad70be947b.png

我们发现当我们用#undef修饰M后,M就不能够使用了,这就是#undef移除宏定义的作用。 

(3).条件编译 

在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件编译指令。

 ①单分支的条件编译

#if 常量表达式

      //...

#endif


#include<stdio.h>
#define M 5
int main()
{
#if M==5printf("%d", M);
#endifreturn 0;
}

877a594fa51c402d9852c1f02f201157.png

②多个分支的条件编译 

#if 常量表达式

      //...

#elif 常量表达式

      //...

#else

     //...

#endif

#include<stdio.h>
#define M 5
int main()
{
#if M==1printf("haha\n" );
#elif M==2printf("hehe\n");
#elif M==5printf("heihei\n");
#endifreturn 0;
}

ac4ab14eb6774a5da40c0c2c9eea98c9.png

③判断是否被定义 

有两种写法

第一种:#if defined(symbol)

第二种:#ifdef symbol

 第一种:

#include<stdio.h>
#define M
int main()
{
#if defined (M)printf("haha\n");
#endifreturn 0;
}

2ec424b36f4144a992c09baf6eef08d4.png

第二种:


#include<stdio.h>
#define M
int main()
{
#ifdef Mprintf("haha\n");
#endifreturn 0;
}

d94e1b02b0e34134862287c18981f1b8.png

当然,这两个种条件编译的反义也有两种形式 

第一种:#if !defined(symbol)

第二种:#ifndef symbol

第一种: 

#include<stdio.h>int main()
{
#if defined(M)printf("hehe\n");
#endif
#if !defined (M)printf("haha\n");
#endifreturn 0;
}

6508cd14a3b347208637ea680b869803.png

第二种: 

#include<stdio.h>int main()
{
#ifdef M printf("hehe\n");
#endif
#ifndef Mprintf("haha\n");
#endifreturn 0;
}

f4535a4b4dfa46b5b85e7fda6f8a5af3.png ④嵌套指令

#if defined(OS_UNIX)#ifdef OPTION1unix_version_option1();#endif#ifdef OPTION2unix_version_option2();#endif
#elif defined(OS_MSDOS)#ifdef OPTION2msdos_version_option2();#endif
#endif

2.编译

 编译过程就是将预处理后的文件进行一系列的:词法分析、语法分析、语义分析及优化,生成相应的汇编代码文件。 编译过程的命令如下:

gcc -S test.i -o test.s

下面以这段代码为例进行词法分析、语法分析、语义分析及优化:

array[index] = (index+4)*(2+6);

(1).词法分析 

将源代码程序被输⼊扫描器,扫描器的任务就是简单的进行词法分析,把代码中的字符分割成一系列的记号(关键字、标识符、字⾯量、特殊字符等)。 上面程序进行词法分析后得到了16个记号:

记号类型
array标识符
[左方括号
index标识符
]右方括号
=赋值
(左圆括号
index标识符
+加号
4数字
)右圆括号
*乘号
(左圆括号
2数字
+加号
6数字
)右圆括号

(2).语法分析

接下来语法分析器,将对扫描产生的记号进行语法分析,从而产生语法树。这些语法树是以表达式为节点的树。

74fc4e59fc2f4b54ab7a636be6e6775d.png

(3).语义分析

由语义分析器来完成语义分析,即对表达式的语法层面分析。编译器所能做的分析是语义的静态分 析。静态语义分析通常包括声明和类型的匹配,类型的转换等。这个阶段会报告错误的语法信息。

c98e8802e6d045379e685b57e0d90e3a.png

 3.汇编

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

 gcc -c test.s -o test.o

4.链接

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

一这段代码为例:test.c 经过编译器处理⽣成 test.o add.c 经过编译器处理⽣成 add.o

ea4fddce55134357a0406288e61c6cef.png

 test.c 经过编译器处理生成 test.o

 Add.c 经过编译器处理生成 Add.o

 在这之后他会生成两个符号表,然后将 test.c 中所有引用到 Add 的指令重新修正,让他们的目标地址为真正的 Add 函数的地址,这个地址修正的过程也被叫做:重定位。

e9e58a47dd8c444081644a120a9db42d.png

 

三.运行环境

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

 

 

 


http://www.mrgr.cn/news/35657.html

相关文章:

  • mac 外接键盘
  • HBase DDL操作代码汇总(namespace+table CRUD操作)
  • 【多系统萎缩患者的活力重启计划】 5大运动,让生活更精彩!‍♀️
  • 爆了!知识产权做实缴不想省钱都难!
  • 经验分享-IT施工注意事项
  • Cubieboard2(六)RTL8188ETV 驱动
  • 变压吸附制氧机的应用范围
  • 校园自助打印系统小程序的设计
  • yolov8训练和预测
  • Spring Cloud :Hystrix实现优雅的服务容错
  • 数据库学习2
  • 基于SpringBoot - Netty框架的云快充协议(充电桩协议)
  • JavaScript (JS)与TypeScript (TS)------讲清楚二者的区别与联系 + 市场主流趋势
  • golang学习笔记32——哪些是用golang实现的热门框架和工具
  • 关系数据库标准语言SQL(3,4)
  • sql中的having与where对比
  • 洛谷P1205 [USACO1.2] 方块转换
  • Java服务端服务发现:Nacos与Eureka的高级特性
  • Oracle(145)如何进行数据库的日常维护?
  • 【Go语言】Go语言结构体全面解析