前言:
一、详解编译与链接:
1.程序的翻译环境与执行环境:
在研究程序的编译与链接细节之前,我们首先要了解我们程序的翻译以及执行环境,我们要知道,在 ANSI C 的任何一种实现中,都存在着两种环境:
第1种是翻译环境,在这个环境中源代码被转换为可执行的机器指令。
第2种是执行环境,它用于实际执行代码。
2.翻译环境:
在翻译环境中执行的操作,简单来说可以分为三个步骤:
组成一个程序的每个源文件通过编译过程分别转换成目标代码**(.obj)。
每个目标文件由链接器捆绑在一起,形成一个单一而完整的可执行程序。
链接器同时也会引入链接库(标准C函数库)中任何被该程序所用到的函数,而且它可以搜索程序员个人 的程序库,将其需要的函数也链接到程序中。
每次编译运行完成以后,我们都可以在所在文件里发现.exe文件和.obj文件
3.翻译阶段:
翻译阶段又可以分为两个阶段,即编译与链接:
①.编译:
预编译:
首先我们先一段普通的代码:
然后用gcc做以下操作:
预处理 选项 gcc -E test.c -o test.i
预处理完成之后就停下来,预处理之后产生的结果都放在test.i文件中
就会发现下面的情况:
预处理的作用:
头文件的包含、#define 定义符号的替换、注释的删除,文本操作。
编译:
用gcc选项 gcc -S test.c
编译完成之后就停下来,结果保存在test.s中,我们便会发现汇编代码
编译的作用:
编译阶段就是将 C 语言的代码翻译成汇编代码,其中包含了语法分析、词法分析、语义分析、符号汇总(后面会重点讲到)等。
汇编
用gcc选项gcc -c test.c
汇编完成之后就停下来,结果保存在test.o中,出现乱码(2进制码以文本形式无法看懂)
汇编的作用:
将汇编代码翻译成二进制的指令(存放到目标文件)符号汇总后**:形成符号表
符号表:
一个类似于这样的表格:
在这里,我们的符号表里是该程序里所有的函数名和每个函数所在的的地址,(对于使用的其他文件的函数,地址都先计为0x00,在后面链接阶段在修改该地址)
②.链接:
编译的作用是:
合并段表
合并符号表
重定位符号表
这里就是对所有文件的一个汇总,对于一个程序里,一个源文件使用了另一个源文件的函数,通过链接,就会将函数的地址汇总,并且改掉地址为0的函数。
4.运行环境:
在这个环境下,我们的程序就真正进入了运行阶段。我们程序的执行过程可以简述为下面四个步骤:
①. 程序载入内存中:在有操作系统的环境中该步骤通常由操作系统完成;而在独立环境中则必须由我们自己手动完成;独立环境中的程序也有可能通过执行可执行代码置入只读内存来完成。
②. 调用 main 函数:程序正式开始执行。
③. 顺序执行程序代码:在这个阶段中,我们的程序会使用一个运行时堆栈来存储函数的局部变量和返回地址。同时程序也可以使用静态内存来存储变量,并且这些存储于静态变量中的变量在整个程序的执行过程中将始终保留它们的值。
④. 终止程序:一般情况下会正常终止 main 函数,但我们的程序也有可能会意外终止。
二、预处理详解:
1.预定义符号:
__FILE__ //进行编译的源文件 __LINE__ //文件当前的行号 __DATE__ //文件被编译的日期 __TIME__ //文件被编译的时间 __STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义
这些预定义符号都是语言内置的。
举个栗子:
通过:
printf("time:%s %s\n", __DATE__, __TIME__);
你就可以知道编译这段代码的当前时间
2.#define:
#define 的用处非常多,就比如我们常用的定义标识符常量、定义宏等等,而在这个过程中,也有一些细节值得我们去注意。
①. #define 定义标识符:
我们常常会使用 #define 去定义一些标识符来方便我们的代码书写:
语法: #define name stuff
定义了之后,只有我们看到 name 这个东西,就给他一比一替换成 stuff这个东西就行即可
举个栗子:
#define MAX 1000 #define reg register //为 register这个关键字,创建一个简短的名字 #define do_forever for(;;) //用更形象的符号来替换一种实现 #define CASE break;case //在写case语句的时候自动把 break写上。 // 如果定义的 stuff过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠(续行符)。 #define DEBUG_PRINT printf("file:%s\tline:%d\t \ date:%s\ttime:%s\n",\ __FILE__,__LINE__,\ __DATE__,__TIME__)
注意:
在 #define 的最后,最好不要加上分号 “ ; ” 加上可能会出现两个;号,可能会导致语法错误:
②.#define 定义宏:
在#define 的机制中,包括了一个规定,这个规定允许把参数替换到文本中,这种实现通常称为定义宏(或简称为宏)。
宏的申明方式为:
#define name( parament-list ) stuff
其中的 parament-list 是一个由逗号隔开的符号列表,且可能出现在 stuff 中
注意:
注意:
参数列表的左括号必须与name紧邻。
如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分
易错点:
初学者在学习宏时很容易出现以下问题:
举个栗子:
#define SQUARE( x ) x * x int main { int a = 5; printf("%d\n" ,SQUARE( a + 1) ); }
乍一看,你可能觉得这段代码将打印36这个值。
事实上,它将打印11.
为什么?
我们前面说过了,标识符的替换是一比一替换!
替换文本时,参数x被替换成a + 1,所以这条语句实际上变成了:
printf (“%d\n”,a + 1 * a + 1 ),所以结果是11。
所以我们可以对这个宏进行修改:
#define SQUARE(x) (x) * (x)
由此可见,我们在进行宏定义的时候,一定要舍得加括号!避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用。
③. #define 替换规则:
#define 在进行符号替换时,遵循以下规则:
调用宏时首先对参数进行检查,检查是否包含任何由 #define 定义的符号。如果有,它们将首先被替换。
替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
最后,再次对结果文件进行扫描,看看它是否包含任何由 #define 定义的符号。如果有,就重复上述处理过程。
注意:
宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。
总结:
这就是程序环境和预处理的相关知识。当然,这里对于宏的讲解是往往不够的!宏的相关知识将在下一篇文章中进行详细讲解!更新不易,辛苦各位小伙伴们动动小手,👍三连走一走💕💕 ~ ~ ~ 你们真的对我很重要!最后,本文仍有许多不足之处,欢迎各位认真读完文章的小伙伴们随时私信交流、批评指正!