目录
一. 编译的四个阶段
1.1 预处理
1.1.1 代码示例
1.1.2 命令行宏定义
1.1.3 条件编译的用处
1.2 编译
1.2.1 代码示例
1.3 汇编
1.3.1 代码示例
1.4 链接
1.4.1 代码示例
二. 动态库和静态库
2.1 什么是库
2.2 库文件的命名规则
2.3 动静态库
2.4 动态链接和静态链接
2.4.1 动态链接
2.4.2 静态链接
2.4.3 优缺点
三. 实践操作
3.1 ldd
3.2 file
3.3 使用静态链接编译
3.3.1 静态库的安装
3.3.2 验证
3.4 动态库
3.5 在技术上理解库
一. 编译的四个阶段
1.1 预处理
预处理就是在真正编译之前,先把头文件展开、宏替换、条件编译等工作处理好,为后面的正式编译做准备。
gcc -E code.c -o code.i #不指定输出文件名,那么默认输出到标准输出 gcc → 使用 GCC 编译工具 -E → 只进行预处理 → 预处理完成后就停止,不继续编译 code.c → 要处理的 C 源文件 -o → 指定输出文件名 code.i → 预处理后的结果文件1.1.1 代码示例
1.1.2 命令行宏定义
命令行宏定义指的是:在编译程序时,通过 GCC/G++ 的命令行参数-D来定义宏,而不是在源代码中使用#define定义。
g++ code.cpp -DM -o code g++ → 使用 C++ 编译器 code.cpp → 要编译的源文件 -DM → 在命令行中定义宏 M -o code → 指定生成的可执行文件名1.1.3 条件编译的用处
1.2 编译
在汇编阶段,编译器会对代码进行词法分析、语法分析、语义分析以及代码优化等操作,检查代码是否符合语言规则,并将高级语言代码转换为对应的汇编代码。
gcc -S code.i -o code.s #不指定文件名时,则会默认生成 code.s 文件 gcc → 使用 GCC 编译工具 -S → 只编译生成汇编代码,不继续进行汇编和链接 code.i → 预处理后的 C 语言文件 -o code.s → 指定输出文件名为 code.s1.2.1 代码示例
1.3 汇编
在汇编阶段,汇编器会将汇编指令转换成对应的机器码,并生成目标文件(通常为.o文件)。目标文件中已经包含机器指令,但通常还不能直接运行,因为其中可能存在尚未解析的函数引用、外部符号等信息,需要等待后续的链接阶段进行处
汇编生成的目标文件已经是二进制文件,所以我们想打开的话大部分是乱码。
gcc -c code.s -o code.o #不指定文件名时,则会默认生成 code.o 文件 gcc → 使用 GCC 工具链 -c → 只生成目标文件,不进行链接 code.s → 编译阶段生成的汇编文件 -o code.o → 指定输出文件名为 code.o1.3.1 代码示例
1.4 链接
经过汇编阶段后,我们已经得到了目标文件(.o),其中包含机器指令。但是,目标文件中可能还存在尚未解析的外部符号引用,例如调用其他源文件中的函数,或者调用标准库中的printf()函数。
因此,需要经过链接阶段,由链接器将相关的目标文件和所需的库关联起来,处理函数、变量的引用及地址等信息,最终生成可执行程序。
gcc code.o -o mycode #不指定 -o 时,默认生成名为 a.out 的可执行文件。 gcc → 使用 GCC 工具链 code.o → 输入汇编阶段之后生成的目标文件 -o mycode → 指定输出文件名为 mycode1.4.1 代码示例
二. 动态库和静态库
2.1 什么是库
库是将已经实现的功能代码组织起来,供其他程序重复使用的代码集合。在 C/C++ 开发中,我们通常通过头文件获得函数声明,再通过链接使用库中提供的函数实现。根据链接方式的不同,常见的库可以分为静态库和动态库。
2.2 库文件的命名规则
在 Linux 中,库文件通常采用下面的命名方式:
lib + 库名 + 文件后缀库文件名 | 去掉前缀和后缀 | 对应库名 |
|---|---|---|
|
| m |
|
| m |
|
| c |
|
| stdc++ |
|
| calc |
2.3 动静态库
静态库是指编译链接时,把库文件的代码全部加入到可执行文件中,因此生成的文件比较大,但在运行时也就不再需要库文件了。其后缀名⼀般为".a"
动态库与之相反,在编译链接时并没有把库文件的代码加入到可执行文件中,而是在程序执行时由运行时链接文件加载库,这样可以节省系统的开销。动态库⼀般后缀名为“.so”,如 libc.so.6 就是动态库。gcc在编译时默认使用动态库。完成了链接之后,gcc就可以生成可执行文件,如下所示。
gcc hello.o –o hello2.4 动态链接和静态链接
2.4.1 动态链接
在链接阶段,不把动态库中的代码复制到我们的可执行文件里,而是记录“我需要哪个动态库、哪个函数”;等程序运行时,再由系统找到并加载这个动态库,然后确定函数的实际地址并调用。
2.4.2 静态链接
在链接阶段,就把程序需要的静态库代码复制到最终的可执行文件中。
2.4.3 优缺点
| 对比 | 静态库.a | 动态库.so |
|---|---|---|
| 程序大小 | 较大,因为库代码被放进可执行文件 | 较小,不需要把库代码完整放进去 |
| 运行依赖 | 依赖较少,链接完成后通常不需要原来的静态库 | 依赖动态库,运行时动态库不能缺失 |
| 内存占用 | 多个程序分别带一份库代码,可能产生重复 | 多个程序可以共享同一个动态库,节省内存 |
| 更新维护 | 库更新后,通常需要重新链接程序 | 库更新后,兼容的情况下通常不需要重新编译程序 |
| 启动/运行 | 不需要运行时加载对应静态库 | 运行时需要加载动态库 |
三. 实践操作
3.1 ldd
ldd是 Linux 中用于查看可执行文件依赖哪些动态库的命令。
在 Linux 下使用 GCC/G++ 进行正常编译并最终生成可执行文件时,默认通常采用动态链接。
3.2 file
file是 Linux 中用来查看文件类型的命令。
ELF ↓ Linux 可执行文件格式 x86-64 ↓ 适用于 x86_64 CPU dynamically linked ↓ 采用动态链接 ld-linux-x86-64.so.2 ↓ 运行时负责加载、解析动态库3.3 使用静态链接编译
在前面我们知道使用 g++/gcc 编译时,默认使用动态库,如果我们想要静态库就要额外加选项 -static
g++ code.cpp -o mycode_static -static3.3.1 静态库的安装
但是不排除有的系统下并没有安装静态库,这时候就需要我们手动安装静态库。
C程序 sudo apt update sudo apt install libc6-dev C++程序 sudo apt install libstdc++-dev3.3.2 验证
我们会发现:使用静态链接编译的可执行文件大小比动态链接编译的可执行文件大很多,这也印证了我们之前的结论。
3.4 动态库
动态库又称为共享库,为什么呢?
当可执行文件
mycode依赖动态库时,程序运行过程中不仅会加载mycode本身,也会加载它依赖的动态库。动态库不是独立运行的,而是被加载到进程地址空间中,供程序调用其中的函数。若后续还有多个可执行文件依赖同一个动态库,操作系统通常可以共享该动态库的代码部分,因此不必在物理内存中重复保存多份相同的库代码,从而节省内存。
3.5 在技术上理解库
- 在 C/C++ 项目中,多个源文件通常会分别经过预处理、编译和汇编,生成对应的
.o目标文件,然后由链接器将它们组合起来,形成可执行程序。- 这样不仅方便模块化开发、提高编译效率,还可以在不直接提供源代码的情况下,将目标文件交给其他开发者使用。
- 但如果目标文件数量很多,管理起来就会比较繁琐,因此可以将多个目标文件组织成库。其中,静态库通常就是多个目标文件归档打包形成的,而动态库则通过进一步链接生成。