【C语言】编译链接 _Linux下操作 _#define详解 [进阶篇 _复习总结]

1.翻译环境和执行环境

1.1翻译环境

翻译环境又可以分为编译和链接,形成的可执行程序test.exe通过执行环境显示运行结果。

把源代码转换为可执行的机器指令(二进制指令),由编译器完成。

在这里插入图片描述

每个源文件经过编译器生成目标文件(windows下命名为xxx.obj,Linux下命名为xxx.o),目标文件生成后由链接器统一处理,并且会加上一些链接来的库,最后由链接器经过链接过程生成可执行程序。

在这里插入图片描述

库函数以来的库文件,都属于第三方库;如我们经常使用的printf、scanf函数都属于库函数。

1.2执行环境

程序执行的过程:

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

实际执行代码,由操作系统完成。我经常使用的VS2019是一个集成开发环境,其中既有翻译环境,又有执行环境;

VS2019中的编译器是cl.exe,链接器是link.exe;这里我们可以看一下VS2019中的链接器(link.exe)是确实存在的:
在这里插入图片描述

2.编译本身的几个阶段

在这里插入图片描述

VS2019是集成开发环境,不方便观察每个细节;这里我们使用linux gcc来演示编译和链接的过程。

2.1 预处理(预编译)

在Linux环境下,创建两个文件test.c和add.c。

//test.c
#include <stdio.h>
extern int Add(int a, int b);
//定义一个宏NUM
#define NUM 100
int main()
{
    
    
    int n = NUM;
	int a = 10;
	int b = 20;
	int c = Add(a, b);
	printf("%d\n", c);
    
    #ifdef __DEBUG__
	printf("这是一个条件编译,debug下才会执行\n");
	#endif 
	return 0;
}
//add.c
int Add(int x, int y)
{
    
    
    return x + y;
}

想要知道预处理之中做了什么,就需要在预处理之后停下来,Linux下指令 gcc test.c -E -o test.i,gcc add.c -E -o add.i可以生成的test.i,add.i就是预处理之后的文件;下面通过观察test.i来看预处理阶段做了什么。

下面在Linux环境下进行操作:

在这里插入图片描述

下面打开test.i文件进行观察:

在这里插入图片描述

1.int n = NUM直接进行了替换,说明预处理阶段进行了宏替换

2.注释在预处理阶段之后就没了,说明预处理阶段去掉了注释

3.debug条件编译,因为gcc默认是release,所以看不到,说明预处理阶段处理了条件编译

还有一点就是我们#include <stdio.h>包含的头文件这里没有了,但是上面多了几百行的代码:

在这里插入图片描述

Linux环境下,头文件放在/usr/include中,那么我们对比一下库中的stdio.h:

在这里插入图片描述

对比可以得出预处理阶段的第四个处理功能:

4.头文件包含。

2.2 编译

想要知道编译阶段做了什么,同样需要在编译阶段后停下来;Linux下指令:gcc test.i -S -o test.s,gcc add.i -S -o add.s;生成的test.s,add.s就是编译阶段之后的文件。

下面来看一下test.s文件:

在这里插入图片描述

test.s中是一些汇编指令,需要进行语法分析、词法分析、语义分析、符号汇总; 《编译原理》-编译器的工作原理。

编译的主要作用就是把C语言代码转化为汇编代码。

编译的过程还是很复杂的,在这里给大家简单的介绍一下符号汇总(链接时需要用到):

汇总时只汇总全局的符号:main Add

在这里插入图片描述

2.3 汇编

在汇编阶段后停下来;Linux下指令:gcc test.s -c -o test.o,gcc add.s -c -o add.o;生成的test.o,add.o就是汇编阶段之后的文件。

下面观察下test.o文件:

在这里插入图片描述

可以发现,test.o中全是一些二进制指令,所以汇编阶段是把汇编指令转化为二进制指令,供计算机识别。

其中一个重要的阶段是形成符号表,每一个文件都形成自己的符号表。

Linux环境下:test.o 可执行程序的格式:elf,可以使用readelf工具进行读取。

readelf test.o -s (-s选项的作用就是显示符号表)

在这里插入图片描述

readelf add.o -s

在这里插入图片描述

形成的符号表(在链接时会使用到):

在这里插入图片描述

3.链接过程

1.合并段表

二进制文件,会被分成很多段;test.o,add.o都会被分为很多段,在合并段表的时候会把相同的段合并到一起。

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

test.o中Add的地址是一个无效地址,add.o中Add的地址0x300是一个有效地址,合并的时候Add会合并为有效地址,而main函数是一个有效地址,不需要改变。

在这里插入图片描述

4.预处理详解

4.1#define定义标识符

#define NUM 100
#define STR "abcdef"

#define定义标识符还是很容易理解的,但是在写的时候需要注意:后面不要加分号,加分号可能会出现语法错误。

#defien NUM 100;
int main()
{
	int num = 0;
	if(1)
		num = NUM;
	else
		num = -1;
	return 0;
}

这样写在编译的时候就会报错:

在这里插入图片描述

if语句中没有{}时只能有一条语句,NUM带上分号之后if语句中是两条语句,出现了语法错误。

建议:在定义宏的时候后面不要加分号。

4.2#define定义宏

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

//#define定义宏的语法:
#define name( parament-list ) stuff
#define MAX(x, y) (x>y?x:y)

注意:参数列表的左括号必须与name紧邻,如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分 。

下面看这样一段代码:

#define SQUARE(x) x*x
int main()
{
	int a = 9;
	int r = SQUARE(a+1);
	printf("%d\n", r);//19
	return 0;
}

这里有很多人就会有疑问了,为什么结果不是100,而是19呢?

这里我们通过得到预处理之后的代码来看一下:
在这里插入图片描述

可以看到的是在参数a+1传入宏中时,直接进行了替换。

在定义的时候可以改进一下:

#define SQUARE(x) (x)*(x)

下面再举一个例子:

#define DOUBLE(x) (x)+(x)
int main()
{
	int ret = 3*DOUBLE(20);//替换为3*(20)+(20)  80
	return 0;
}

总结一下:在使用#define定义宏的时候一定要检查一下括号,不使用括号得到的结果可能就和期望的结果不一样。

4.3#define的替换规则

在程序中扩展#define定义符号和宏时,需要涉及几个步骤。
1.在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先
被替换。
2.替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
3.最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上
述处理过程。
注意:

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

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

猜你喜欢

转载自blog.csdn.net/qq_63179783/article/details/128364731