文章目录

⭕前言
前面我们重点为大家介绍了vim编辑器,它是一款轻量、高效的编辑器,可以为我们快速的谱写代码,而想要让一个文件变成一个可执行程序,还需要经过预处理、编译、汇编、链接 步骤,才能生成对应的可执行程序,才能跑起来。
那么,这些步骤都由谁来做呢?就是我们这篇文章的主角:gcc/g++ ,也就是说gcc/g++负责帮我们从一个编写好的程序 处理成为一个可执行程序。
cpp
//假设我们所编写的文件为main.c,想要将其可执行程序文件命名为main
gcc main.c -o mian//main就为我们所要执行的程序
🆙一、编译器gcc/g++
1.1背景知识
想要了解gcc/g++,就先要明白对文件进行预处理、编译、汇编、链接都干了些什么
①预处理
1.展开所有头文件
#include<stdio.h> #include"xxx.h"
把头文件中的所有代码原样复制 插到当前源码里,合并成一个文件
2.宏替换
# define MAX 100
代码中的MAX会被全部替换成100
3.处理条件编译
#ifdef、#ifndef、#else、#endif
满足条件的代码保留,不满足的直接删掉不参与后续编译
4.删除所有注释
单行注释 //、多行注释 /* */
预处理阶段全部清空,后面编译阶段看不到注释
预处理完成后,源码变成了无注释、头文件全部展开、宏全部替换完毕的 .i 文件,接下来就进入编译、汇编、链接阶段。
cpp
//想要让文件编译成为.i文件,使用-E选项
gcc -E main.c -o main.i//mian.c是我们的文件,-E为形成对应.i文件的选项,里面为预处理后的文件内容
②编译
- 编译阶段:把预处理后的 .i 文件,翻译成汇编代码
- 进入编译的文件:
main.i - 核心目的:1. 语法/语义检查 :检查变量未定义、类型不匹配、语法错误这些问题(大部分编译报错都出在这一步)2. 代码优化 :根据你加的-O/-O2等选项做优化 3. 生成对应平台的汇编指令(不同CPU架构,汇编指令不一样)
cpp
//生成汇编语言.s的命令
gcc -S main.i -o main.s
//执行后main.s的文件内容就是纯汇编代码
③汇编(转成cpu能够识别的指令集)
- 汇编阶段:把汇编代码翻译成机器指令(二进制),生成目标文件
- 进入汇编的文件:main.s
- 核心目的:1. 将汇编指令翻译成机器能直接执行的二进制指令 2. 生成「可重定位目标文件」(.o / .obj),里面包含机器指令和符号表,但还不能直接运行
cpp
//生成可重定位目标文件.o的命令
gcc -c main.s -o main.o
//或者直接从.c跳转到.o的文件(跳过中间步骤)
gcc -c main.c -o main.o
④链接
- 链接链接阶段:把多个目标文件和库文件合并,生成最终的可执行程序
- 进入链接的文件:main.o + 其他.o文件 + 库文件(比如libc.so)
- 核心目的: 1. 符号解析 :把你代码里调用的函数(比如printf)和库文件里的定义关联起来 2. 重定位 :把目标文件里的地址修正为最终运行时的内存地址 3. 合并段:把多个目标文件的代码段、数据段合并成一个
cpp
//生成可执行文件的命令
gcc main.o -o main
//执行后就可以./main运行程序了
cpp
//main.c
#include<stdio.h>
#define MAX 100
int main()
{
printf("我是 MAX: %d\n",MAX);
return 0;
}
//1.预处理 gcc -E main.c -o main.i
//2.编译 gcc -S main.i -o main.s
//3.汇编 gcc -c main.s -o main.o
//4.链接 gcc -o main main.o
1.2动态链接和静态链接
在实际开发中,我们不会把所有代码都写在一个 .c 文件里,而是按功能拆分成多个源文件,比如 main.c、calc.c、utils.c。这些文件之间会互相调用函数,比如 main.c 里会用到 calc.c 里写好的加法函数。
当所有 .c 文件都编译成 .o 目标文件后,链接阶段的任务,就是把这些分散的目标文件 "拼" 成一个完整的可执行程序,解决它们之间的函数调用问题,让程序运行时能找到每一个函数的定义。
静态链接
编译链接时,会把你源文件中所用到的方法,从库中拷贝进我们的可执行程序
- 结果:生成的文件体积更大,但它自带了所有依赖的库代码,拷贝到任何同架构的 Linux 机器上都能直接运行,不依赖系统环境。
- 缺点:多个程序用同一个库时,每个程序都带一份库代码,会重复占用磁盘和内存空间。
动态链接
并不会把方法拷贝进可执行程序中,而是需要用到什么方法,记录下库的路径和符号,到指定库中查找即可
- 结果:生成的 app_dynamic 文件体积很小,运行时系统会把共享库加载到内存里,多个程序可以共用同一份库代码,节省资源。
- 缺点:程序运行时必须找到对应的系统库,如果环境里没有这个库(或者版本不对),程序就会报错跑不起来。
以读书举例:
动态链接就像是去图书中翻阅,而静态链接则是把整本书借走
🆒二、自动化构建-make/Makefile
2.1背景
- 会不会写makefile,从一个侧面说明了一个人是否具备完成大型工程的能力
- 一个工程的源文件不计其数,其按类型、功能、模块分别放在若干个目录中,makefile定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作
- makefile带来的好处就是-
自动化编译,一旦写好了makefile,只需要一个make命令,整个工程完全自动编译,极大的提高了软件开发的效率 - make是一个命令工具,是一个解释makefile中指令的命令工具,一般来说,大多数的IDE都有这个命令
make是一个命令,makefile是一个文件,两者搭配使用,完成项目自动化构建
2.2基本使用
实例代码
cpp
//假设这个代码存放在文件myproc.c中
#include<stdio.h>
int main()
{
printf("hello makefile\n");
return 0;
}
Makefile文件
cpp
myproc:myproc.c
gcc-o myproc myproc.c
.PHONY:clean
clean:
rm -rf myproc
有了源文件和makefile,我们只要在命令行中输入make,系统就会执行gcc -o myproc myproc.c,当前目录下就会生成myproc文件;在命令行中输入make clean,系统就会执行rm -rf myproc,myproc文件就会被删除
- 依赖关系
上面的文件myproc,它依赖myproc.c,有了myproc.c才能生成myproc - 依赖方法
gcc -o myproc myproc.c,就是与之对应的依赖关系 - 项目清理
①工程是需要被清理的
②像clean,没有被第一个目标文件直接或间接关联,那么它后面所定义的命令将不会被自动执行,不过,我们可以显示要make执行。即命令make clean,以此来清楚所有的目标文件,以便重编译
③但是一般我们这种clean的目标文件,我们将它设置为伪目标,用.PHONY修饰,伪目标的特性是,总是被执行的
2.3什么叫总是被执行
我随机罗列一个文件的属性,请看:
cpp
File: main.c
Size: 215 Blocks: 8 IO Block: 4096 regular file
Device: 805h/2053d Inode: 263214 Links: 1
Access: (0644/-rw-r--r--) UID: (1000/user) GID: (1000/user)
Access: 2026-05-09 16:30:22.184257345 +0800
Modify: 2026-05-09 16:20:15.221356789 +0800
Change: 2026-05-09 16:20:15.221356789 +0800
Birth: -
- Access时间:常指的是文件最近一次被访问的时间。在linux早期版本,每当文件被访问时,其atime都会更新。但这种机制会导致大量的IO操作。导致效率低下。
- Modify时间:记录最近一次文件内容变更的时间
- Change时间:记录最近一次文件属性变更的时间
为什么改文件内容,Modify时间会改变(内容变了,Modify时间也会改变),Change时间也会改变???
这是因为修改内容,文件大小会发生变化,文件大小也是属性,属性一旦发生变化,Change时间也随之发生变化
知识铺垫完之后,我们再来看看
拿myproc:myproc.c举例。
我们这里的目标文件myproc是依赖源文件myproc.c生成的,假设我们的myproc.c没发生改变,我们还需要再次生成一个myproc吗?
①显然是不需要的,你myproc.c都没发生改变,那么你通过gcc编译后的myproc也必定没有发生改变,那我还要进行make生成它干嘛?浪费资源罢了,所以再次进行make系统会报错,不让你这么干!
②也就是说,myproc.c里的内容改变了,make之后重新生成的myproc才有意义。也就是说myproc.c的Modify时间如果比myproc的Modify时间早 (说明在make之后,myproc.c无修改),那么make就无意义;myproc.c的Modify时间如果比myproc的Modify时间晚(说明在make之后,myproc.c做了修改),那么make就有意义
本质:比较的是两者Modify时间的前后

结论
①源文件是否需要被重新编译?源文件和可执行谁更新?看两个文件的修改时间
②判断文件新旧,根据文件Mod时间判断的!
所以,.PHONY的本质就是忽略Mod时间新旧的对比
如果在myproc前面加上.PHONY的话,也就是忽略myproc和myproc.c的新旧时间对比,系统根本不检查、不对比内容有没有发生改变,强制重新编译 ,没必要!
如果不加.PHONY,系统会检查对比时间,没变化的话直接跳过编译 ,提升效率
举个例子:
书包收拾好了,明天东西没变动,不用重新检查(不加.PHONY)
不管书包收没收拾好,强制整理一遍(加了.PHONY)
2.4推导过程
Makefile文件
cpp
myproc:myproc.o
gcc myproc.o -o myproc
myproc.o:myproc.s
gcc -c myproc.s -o myproc.o
myproc.s:myproc.i
gcc -S myproc.i -o myproc.s
myproc.i:myproc.c
gcc -E myproc.c -o myproc.i
.PHONY:clean
clean:
rm -rf *.i *.s *.o myproc
推导:
myproc依赖于myproc.o
myproc.o在哪呢?
myproc.o依赖于myproc.s
myproc.s在哪呢?
myproc.s依赖于myproc.i
myproc.i在哪呢?
myproc.i依赖于myproc.c
myproc.c就是我们所写的源文件
编译
cpp
$ make
gcc -E myproc.c -o myproc.i
gcc -S myproc.i -o myproc.s
gcc -c myproc.s -o myproc.o
gcc myproc.i -o myproc

2.5拓展语法
cpp
CC=gcc # 编译器
BIN=myproc # 最终生成的可执行文件
RM=rm -rf # 删除命令
SRC=$(wildcard *.c) # 匹配当前目录所有 .c 文件
OBJ=$(SRC:.c=.o) # 将 .c 文件替换为 .o 目标文件
| 符号 | 含义 |
|---|---|
| *.c | 匹配所有 .c 后缀文件 |
| $@ | 规则中的目标文件名 |
| $^ | 规则中的所有依赖文件 |
| $< | 规则中的第一个依赖文件 |
| %.o:%.c | 模式规则:所有 .o 文件依赖同名 .c 文件 |
cpp
# 1. 链接生成可执行文件(最终目标)
$(BIN):$(OBJ)
$(CC) -o $@ $^ # 编译命令,必须以 Tab 开头!
# 2. 编译 .c 生成 .o 目标文件
%.o:%.c
$(CC) -c $<
# 3. 清理编译产物
clean:
$(RM) $(OBJ) $(BIN)
# 声明伪目标,避免和同名文件冲突
.PHONY:clean
🅱️总结
我个人认为gcc/g++没有什么东西可以学的,因为底层工作上层已经帮我们完成了,知道怎么用和ESc选项生成对应.i,.s,.o文件即可;至于makefile,很重要,多接触,多敲吧。