C语言:编译,链接和预处理详解
目录
一.翻译环境和运行环境
二.翻译环境
编辑 1.预处理(预编译)
(1).#和##运算符
①.#运算符
②## 运算符
(2).#undef
(3).条件编译
①单分支的条件编译
②多个分支的条件编译
③判断是否被定义
编辑 ④嵌套指令
2.编译
(1).词法分析
(2).语法分析
(3).语义分析
3.汇编
4.链接
三.运行环境
在这之前,我们已经学习了很多的c语言的知识,学习了进行代码的编写和运行,那么,是什么让我们编写完的代码成功的运行和输出我们想要的结果呢?这就是我们今天要讲的内容:编译,链接和预处理详解。
首先,在我们进行对它们进行深入了解之前。我们要先知道什么是翻译环境和运行环境。
一.翻译环境和运行环境
在ANSI C的任何一种实现中,存在两个不同的环境,他们就是翻译环境和运行环境,而我们今天要学习的编译和链接就处在翻译环境当中。
在我们进行代码编写后,我们会形成一个或多个的(.c)文件,计算机会将这些(.c)文件放入到翻译环境中,经过编译,链接,将源代码转换为计算机可执行的机器指令即:二进制指令。之后,放入到Windows环境下(运行环境),他会形成(.exe)为后缀的可执行程序,最后进行输出。
我们一下面图片中的代码为例:
运行完后,我们在我们文件中就可以发现我们生成的(.exe)文件.
二.翻译环境
那翻译环境是怎么将源代码转换为可执行的机器指令的呢?这里我们就得深入了解一下翻译环境所 做的事情。
其实翻译环境是由编译和链接两个大的过程组成的,而编译又可以分解成:预处理(有些书也叫预编译、编译、汇编三个过程。
并且在编译和链接中还存在着看不见的编译器(cl.exe)和链接器(link.exe),编译器会将(.c)文件生成对应的(.obj)目标文件(windows环境下)或(.o)目标文件(Linux环境下)。之后目标文件会和链接库在链接器的作用下生成(.exe)的可执行程序。
我们还是一上面的代码为例,去查看是否生成了(.obj)文件:
我们发现我们不仅成功找到了(.obj)文件,还在寻找的过程中发现了 Add.c文件和test.c文件
总结:
- 多个.c文件单独经过编译器,编译处理生成对应的目标文件。
- 注:在Windows环境下的目标文件的后缀是 .obj ,Linux环境下目标文件的后缀是 .o
- 多个目标文件和链接库一起经过链接器处理生成最终的可执行程序。
- 链接库是指运行时库(它是支持程序运行方的基本函数集合)或者第三库。
在刚才我们也提到了编译也分为3个部分: 预处理(有些书也叫预编译、编译、汇编三个过程。
1.预处理(预编译)
在预处理阶段,源文件和头文件会被处理成为 .i 为后缀的文件。 在 gcc 环境下想观察⼀下,对 test.c 文件预处理后的.i文件,命令如下:
gcc -E test.c -o test.i
预处理阶段主要处理那些源文件中#开始的预编译指令。比如:#include,#define,处理的规则如下:
- 将所有的 #define 删除,并展开所有的宏定义。
- 处理所有的条件编译指令,如: #if、#ifdef、#elif、#else、#endif 。
- 处理#include 预编译指令,将包含的头文件的内容插入到该预编译指令的位置。这个过程是递归进行的,也就是说被包含的头文件也可能包含其他文件。
- 删除所有的注释
- 添加行号和文件名标识,方便后续编译器生成调试信息等。
- 或保留所有的#pragma的编译器指令,编译器后续会使用。
我们发现我们的#define,注释都被删除,宏定义也都被展开。
(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
这条指令用于移除一个宏定义。
我们发现当我们用#undef修饰M后,M就不能够使用了,这就是#undef移除宏定义的作用。
(3).条件编译
在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件编译指令。
①单分支的条件编译
#if 常量表达式
//...
#endif
#include<stdio.h>
#define M 5
int main()
{
#if M==5printf("%d", M);
#endifreturn 0;
}
②多个分支的条件编译
#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;
}
③判断是否被定义
有两种写法
第一种:#if defined(symbol)
第二种:#ifdef symbol
第一种:
#include<stdio.h>
#define M
int main()
{
#if defined (M)printf("haha\n");
#endifreturn 0;
}
第二种:
#include<stdio.h>
#define M
int main()
{
#ifdef Mprintf("haha\n");
#endifreturn 0;
}
当然,这两个种条件编译的反义也有两种形式
第一种:#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;
}
第二种:
#include<stdio.h>int main()
{
#ifdef M printf("hehe\n");
#endif
#ifndef Mprintf("haha\n");
#endifreturn 0;
}
④嵌套指令
#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).语法分析
接下来语法分析器,将对扫描产生的记号进行语法分析,从而产生语法树。这些语法树是以表达式为节点的树。
(3).语义分析
由语义分析器来完成语义分析,即对表达式的语法层面分析。编译器所能做的分析是语义的静态分 析。静态语义分析通常包括声明和类型的匹配,类型的转换等。这个阶段会报告错误的语法信息。
3.汇编
汇编器是将汇编代码转转变成机器可执行的指令,每一个汇编语句几乎都对应一条机器指令。就是根 据汇编指令和机器指令的对照表一一的进行翻译,也不做指令优化。 汇编的命令如下:
gcc -c test.s -o test.o
4.链接
链接是一个复杂的过程,链接的时候需要把一堆文件链接在一起才生成可执行程序。 链接过程主要包括:地址和空间分配,符号决议和重定位等这些步骤。 链接解决的是一个项目中多文件、多模块之间互相调用的问题。
一这段代码为例:test.c 经过编译器处理⽣成 test.o add.c 经过编译器处理⽣成 add.o
test.c 经过编译器处理生成 test.o
Add.c 经过编译器处理生成 Add.o
在这之后他会生成两个符号表,然后将 test.c 中所有引用到 Add 的指令重新修正,让他们的目标地址为真正的 Add 函数的地址,这个地址修正的过程也被叫做:重定位。
三.运行环境
- 程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
- 程序的执行便开始。接着便调用main函数。
- 开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。
- 终止程序。正常终止main函数;也有可能是意外终止。