文章目录
一、编译过程
1. 概述
编译是将源代码转换为目标代码的过程。它是在编译器的帮助下完成的。编译器检查源代码是否存在语法或结构错误,如果源代码没有错误,则生成目标代码。
编译过程可分为预处理、编译、汇编和链接四个步骤。
2. 预处理
- 概念:预处理器(
cpp
)处理以 # 开头的指令,如 #include、#define 和 #ifdef。它通过展开宏和包含头文件来准备源代码进行编译。 - 细节:将源代码转换为预处理后的中间形式。源代码是在文本编辑器中编写的代码,源代码文件的扩展名为 .c。此源代码首先传递给预处理器,然后预处理器扩展此代码。展开代码后,扩展的代码将传递给编译器。
- 产物:
.i
(生成的预处理后的源文件)。
3. 编译
- 概念:编译器 (
gcc
,clang
) 将预处理后的源代码翻译成特定于目标体系结构的汇编语言。 - 细节:执行词法分析、语法分析、语义分析和优化。由预处理器扩展的代码将传递给编译器。编译器将此代码转换为汇编代码。或者我们可以说 C 编译器将预处理的代码转换为汇编代码。
- 产物:
.s
(生成的汇编代码文件)。
4. 汇编
- 概念:汇编器 (
as
)将汇编代码转换为可重定位的机器码(目标代码)。 - 细节:将汇编指令转换为机器码指令。使用汇编程序将汇编代码转换为目标代码。汇编程序生成的目标文件的名称与源文件的名称相同。在 DOS 中,目标文件的扩展名是
.obj
,在 UNIX 中,扩展名是.o
。如果源文件的名称为hello.c
,则目标文件的名称将为hello.obj
/hello.o
。 - 产物:
.o
(生成的目标文件)。
5. 链接
- 概念:链接器 (
ld
) 将多个目标文件组合在一起,并解决外部引用,生成最终的可执行代码。 - 细节:执行符号解析、为变量和函数分配最终地址,并链接必要的库文件。主要是,所有用 C 编写的程序都使用库函数。这些库函数是预先编译的,并且这些库文件的目标代码以 .lib(或 .a)扩展名存储。链接器的主要工作是将库文件的目标代码与我们程序的目标代码相结合。有时,当我们的程序引用其他文件中定义的函数时,会出现这种情况;那么 linker 在这方面起着非常重要的作用。它将这些文件的目标代码链接到我们的程序。因此,我们得出结论,链接器的工作是将程序的目标代码与库文件和其他文件的目标代码链接起来。链接器的输出是可执行文件。可执行文件的名称与源文件的名称相同,但仅在扩展名上有所不同。在DOS中,可执行文件的扩展名为 .exe,而在UNIX中,可执行文件可以命名为 a.out。例如,如果我们在程序中使用 printf() 函数,则链接器会将其关联的代码添加到输出文件中。
- 产物:
.exe
/.out
(生成的可执行文件)。
五、编译相关问题辨析
1. 头文件的作用
- 调用库:通过头文件来调用库功能出于对源代码保密的考虑,源代码不便向用户公布,只要向用户提供头文件和二进制的库即可用户只需要按照头文件中的接口声明来调用库功能,而不必关心接口是怎么实现的编译器会从库中提取相应的代码。
- 安全检查:头文件能加强类型安全检查。当某个接口被实现或被使用时,其方式与头文件中的声明不一致,编译器就会指出错误。
2. #include <filename.h>
和#include "filename.h"
对于
#include <filename.h>
,编译器先从标准库路径开始搜索 filename.h。而对于#include "filename.h"
,编译器先从用户的工作路径开始搜索filename.h,然后去寻找系统路径。
3. 在头文件中定义静态变量是否可行
不可行,如果在头文件中定义静态变量,会造成资源浪费的问题,同时也可能引起程序错误。因为这会导致在使用了该头文件的每个 C 语言文件中都定义该静态变量从而会引起空间浪费或者程序错误所以。不推荐在头文件中定义任何变量,当然也包括静态变量。
4. 宏定义中#
和##
的用法
a)#
字符串化操作符
作用:
#
可以把一个宏参数直接转换成相应的字符串。
比如有下面这个宏:
则进行如下的调用时:
最后执行效果和下图等效:
b) ##
符号连接操作符
作用:将宏定义的多个形参转换成一个实际参数名。
则下面第一个图的代码和第二个图的代码等价:
5. extern C
的作用
extern C
的主要作用就是为了能够正确实现 C++代码调用其他 C 语言代码。加上extern C
后,会指示编译器这部分代码按 C 语言的进行编译,而不是 C++的。
6. 静态链接和动态链接
a)静态链接
函数和数据被编译进一个二进制文件(
.a
)。在使用静态库的情况下,在编译链接可执行文件时,链接器从库中复制这些函数和数据并把它们和应用程序的其它模块组合起来创建最终的可执行文件。
- 优点
- 运行速度快:在可执行程序中已经具备了所有执行程序所需要的任何东西,在执行的时候运行速度快。
- 缺点
- 空间浪费:因为每个可执行程序中对所有需要的目标文件都要有一份副本,所以如果多个程序对同一个目标文件都有依赖,会出现同一个目标文件都在内存存在多个副本。
- 更新困难:每当库函数的代码修改了,这个时候就需要重新进行编译链接形成可执行程序。
b)动态链接
动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。
- 优点
- 共享库:即使需要每个程序都依赖同一个库,但是该库不会像静态链接那样在内存中存在多份副本,而是这多个程序在执行时共享同一份副本。
- 更新方便:更新时只需要替换原来的目标文件,而无需将所有的程序再重新链接一遍。当程序下一次运行时,新版本的目标文件会被自动加载到内存并且链接起来,程序就完成了升级的目标。
- 缺点
- 性能损耗:动态链接是把链接推迟到了程序运行时,所以每次执行程序都需要进行链接,所以性能会有一定损失。
注:链接静态库时,只需要在编译器指定库名及其搜索路径;而链接动态库,还要在运行期间指定库的搜索路径。详见C++:静态库、动态库、头文件。