写在前面:
大家都知道,我们在编译器中建好一个**.c或.cpp 文件**,经过编译之后就可以运行了,也就是说我们写的.c 文件最后会变成一个可执行程序,那么 .c 或者 .cpp 文件是如何变成一个可执行程序的呢?
主要有以下四大步骤:
1、预编译
2、编译
3、汇编
4、链接
接下来我将会详细解释 编译链接 每一步
一、编译
编译过程分为两个阶段 编译和汇编,编译主要就是 读取源程序(字符流),对之进行词法和语法的分析,将高级语言指令转换为功能等效的汇编代码。
1.1 编译
编译又可以细分为两个小的步骤:预编译 和 编译
1.1.1 预编译
因为需要做词法和语法的分析,因此需要预先处理我们编写的源程序中的 伪指令和特殊符号进行处理,也就是大家比较熟知的 宏替换(#define A 10) 、头文件(#include<stdio.h>) 展开,还有一些特殊符号的处理。
1.1.1.1 伪指令是什么?
1.宏定义指令
例如 #define MaxNum 32 ,预编译要做的就是将源程序中的 MaxNum 全部替换成 32 ,但不会替换字符串常量"MaxNum" ,#undef 是取消某个宏定义,使在预编译时不会替换 MaxNum 为 32
例如:
2.条件编译指令
例如 #ifdef #else #endif #ifndef ,俗称条件编译,举个例子,现在我们有如下程序:
#include <stdio.h>
#define MaxNum 32
#undef MaxNum
#define HUAWEI
void OpenDraw()
{
#ifdef HUAWEI
printf("HuaWei");
#else
printf("notHuaWei");
#endif
}
int main()
{
OpenDraw();
return 0;
}
结果显而易见会打印出 Huawei
而如果没有
#define HUAWEI
那么显而易见 会打印
这样我们就可以宏来控制那一段代码会被编译,那一段不会编译,从而实现不同的需求
3.头文件包含指令
也就是大家都很熟悉的 #include<xxxxxx.h>,在预编译这个阶段会将头文件展开,意思就是 假如
#include <stdio.h>
int main()
{
return 0;
}
假如头文件 <stdio.h> 有 1000 行的内容,那么这 1000行内容 都会展开并加到这个文件中进行编译。
例如 如下代码:
预编译之后查看 点 .i 文件 如下:
最后几行
1.1.1.2 特殊符号是什么?
看下面一段代码:
#include <stdio.h>
int main()
{
printf("%d\n", __LINE__);
printf("%s\n", __FILE__);
printf("%s\n", __DATE__);
return 0;
}
输出:
如上代码所示 __LINE__、__FILE__、__DATE__ 都是特殊符号,与此相同的还有
__FILE__ 包含当前程序文件名的字符串
__LINE__ 表示当前行号的整数
__DATE__ 包含当前日期的字符串
__STDC__ 如果编译器遵循ANSI C标准,它就是个非零值
__TIME__ 包含当前时间的字符串
这些特殊的符号会在预编译的时候替代
经过上面的预编译对伪指令和特殊符号处理,生成了一个没有伪指令、没有特殊符号的文件,接下来进行编译和优化阶段
1.1.2 编译
编译阶段的工作内容就是通过词法分析和语法分析,并做一些代码优化,详细的词法分析和语法分析不在此阐述,有兴趣的可以看 《编译原理》 这本书,在确认所有的指令都符合语法规则之后,将其翻译成等价的中间代码表示或汇编代码。
查看 .s 文件
1.2 汇编
大家都知道计算机最底层其实是二进制的世界,而汇编就是将经过预编译、编译处理之后的文件转为二进制文件,例如linux 下经过逐步编译生成的二进制文件打开时这样的
linux 下生成的 .o 文件即为 二进制文件
二、链接
我们知道一般一个项目中会有很多很多的.cpp .h 还包括其他的第三方的一些库等等其他一切程序在执行中需要用到的东西,上述的预编译、编译、汇编,只是将一个个的.cpp 或者 .c 文件编译成 .o 二进制文件,这些还不能直接执行,就好比,一个汽车,虽然把每个零件都已经生产好了,但是没有组装,整合起来,这样这个汽车才能跑起来。
链接的工作就是将该程序相关的所有文件都链接起来
链接一般可以分为静态链接和动态链接,这和我们常说的动态库、静态库 又有什么关系呢
2.1 静态库与动态库
而库呢 什么叫库,我们写了一个打印 “helloworld” 功能的函数,并编译成 二进制代码,别人只需要在自己的文件中 #include 头文件,然后调用 函数 就可以 打印出 helloworld 了
什么时候我们会用到库呢?
一种情况是某些代码需要给别人使用,但是我们不希望别人看到源码,就需要以库的形式进行封装,只暴露出头文件。
另外一种情况是,对于某些不会进行大的改动的代码,我们想减少编译的时间,就可以把它打包成库,因为库是已经编译好的二进制了,编译的时候只需要 Link 一下,不会浪费编译时间。
2.1.1 静态库
静态库即静态链接库(Windows 下的 .lib,Linux 和 Mac 下的 .a)例如:
2.1.2 动态库
动态库即动态链接库 (Windows 下的 .dll,Linux 下的 .so,Mac 下的 .dylib)
例如:
2.1 静态链接
1、静态库的链接
把调要调用的函数或者过程直接链接到可执行文件(dll或exe)中,成为可执行文件的一部分。该执行文件中包含了运行所需的全部代码(也就是说相当于把静态库中的全部代码拷过来了一样)。
优点:
链接该静态库的可执行文件(dll或exe等)使用时,无需再需要该静态库。
缺点:
a)当多个程序都要调用相同函数时,内存中就会存在这个函数的多个复制,存在资源浪费。
b)当静态库发生修改时,不仅该静态库要从新编译,引用该静态库的模块都需要从新编译。
2.2 动态链接
2、动态库的链接
动态链接调用的函数代码并没有被复制到应用程序的可执行文件中去,而是仅仅在其中加入了所调用函数的描述信息(往往是一些重定位信息)。
仅当应用程序被装入内存开始运行时,在操作系统的管理下,才在应用程序与相应的动态链接库(dynamic link library,dll)之间建立链接关系。
当要执行调用.dll文件中的函数时,根据链接产生的重定位信息,操作系统才转去执行.dll文件中相应的函数代码。
优点:
当修改动态库的代码,但重定位信息没有变化,引用该动态库的模块无需从新编译。当然,重定位信息发生改变,两者都需要从新编译。
2.3 动态库的链接方式
3、动态库链接的方式
1)LoadLibrary(根据路径加载动态库)、GetProcAddress(根据函数名获取函数指针,可能为空,但只能在运行时发现)、FreeLibrary(卸载动态库)
2)具有导出项的(DLL)动态链接库可根据导出的lib和头文件隐式链接,在编译阶段就可检查头文件和lib是否配套。运行时再保证动态库和lib配套即可。