一、编译的四个阶段
1.预处理(进行宏替换)
预处理功能主要包括宏定义,文件包含,条件编译,去注释等。
预处理指令是以#号开头的代码行。
bash
gcc -E code.c -o code.i
2.编译(生成汇编)
在这个阶段中,gcc首先要检查代码的规范性、是否有语法错误等,以确定代码的实际要做的工作,
在检查无误后,gcc 把代码翻译成汇编语言。
bash
gcc --S hello.i --o hello.s
3.汇编(生成机器可识别代码)
将汇编代码转换为二进制机器码。
bash
gcc --c hello.s --o hello.o
4.连接(生成可执行文件或库文件)
链接器(Linker)将多个目标文件和库文件合并成一个完整的可执行文件。它会解决所有未定义的符号引用,将它们指向正确的地址。链接器还会将程序使用的标准库函数(如 printf)合并进来。
bash
gcc hello.o --o hello
二、实际项目应用
在真实的软件开发中,一个项目通常由多个源文件(.c文件)组成。我们通常会分别将每个源文件编译为目标文件,而不是一次性编译整个项目。这可以通过以下方式实现:
bash
# 分别编译每个源文件为目标文件
gcc -c file1.c -o file1.o
gcc -c file2.c -o file2.o
gcc -c file3.c -o file3.o
或:
bash
gcc -c file1.c
gcc -c file2.c
gcc -c file3.c
这会产生对应的 file1.o、file2.o、file3.o文件。这种分离编译的方式有以下几个优点:
-
当只修改其中一个源文件时,只需重新编译该文件,而不用重新编译整个项目,提高了编译效率。
-
便于团队协作,不同开发者可以并行编译各自负责的模块。
-
便于将代码组织成库文件。
最后一起链接:
bash
# 将所有目标文件链接成一个可执行文件
gcc file1.o file2.o file3.o -o myprogram
当只修改其中一个源文件时,只需重新编译该文件,而不用重新编译整个项目,提高了编译效率。
操作系统是怎么做到的??
看时间戳
Linux 里每个文件都有 3 个时间,用 stat 文件名 可以看到:
atime :访问时间(cat、vim 打开看一下,就变)
mtime :修改时间(内容改了才变)
ctime :状态改变时间(权限、改名等)
操作系统只关心一个:mtime(修改时间)
只查看文件 → mtime 不变,make 就认为文件没动 → 不会重新编译
修改文件、保存文件 → mtime 变 → 会重新编译
判断流程:
比较目标文件和依赖文件的 mtime
如果依赖文件比目标文件更新(mtime 更大)→ 执行命令
如果目标文件不存在 → 执行命令
否则 → 跳过("目标已是最新的")
三、静态链接 vs 动态链接
在实际开发中,多个源文件之间存在依赖关系,需要将它们产生的目标文件进行链接。链接方式有两种:
1.静态链接
在编译时,将库文件的代码全部复制到可执行文件中。
优点:
可执行文件独立,不依赖外部库文件。
启动速度快(代码在进程地址空间内)
缺点:
浪费空间(多个程序包含相同库的副本)
更新困难(库更新需重新编译整个程序)
可执行文件体积较大
2.动态链接
在程序运行时才加载库文件。程序启动稍慢,但内存占用少。
例如:
我们的C程序中调用的 printf()函数,实现在哪里?
C标准只定义了函数声明(在 stdio.h中),函数实现位于系统的C标准库中:
Linux: libc.so.6(动态库)或 libc.a(静态库)
Windows: msvcrt.dll或 libcmt.lib
核心:让程序找到库中方法的地址!
四、静态库/动态库
1.静态库
静态库是指编译链接时,把库文件的代码全部加入到可执行文件中,其后缀名⼀般为".a"
静态库本身是一个文件(例如 libmymath.a),它本质上是一个或多个目标文件(.o文件)的打包集合 。创建它的目的是为了代码复用。
2.动态库
动态库与之相反,在编译链接时并没有把库文件的代码加入到可执行文件中,而是在程序执行时由
运行时链接文件加载库,这样可以节省系统的开销。动态库⼀般后缀名为".so"
gcc默认生成的二进制程序,是动态链接的。gcc 在编译时默认使用动态库。
可以使用 -static 强制全部静态链接。