开发工具链(上):gcc、make
gcc:GNU C 编译器
gcc(GNU Compiler Collection)是 Linux 下最常用的 C/C++ 编译器,也是计算机专业学生必须掌握的工具。
基础编译
# 编译单个源文件,生成可执行文件 a.out
gcc hello.c
# 指定输出文件名(-o output)
gcc hello.c -o hello
# 运行
./hello
编译四阶段
C 程序编译分为四个阶段:预处理 → 编译 → 汇编 → 链接。
# 只预处理(-E)
gcc -E hello.c -o hello.i
# 预处理并编译成汇编(-S)
gcc -S hello.c -o hello.s
# 编译成目标文件(-c,不链接)
gcc -c hello.c -o hello.o
# 链接目标文件生成可执行文件
gcc hello.o -o hello
# 一步到位(默认执行全部四阶段)
gcc hello.c -o hello
常用编译选项
| 选项 | 作用 | 示例 |
|---|---|---|
-o | 指定输出文件名 | gcc hello.c -o hello |
-c | 只编译不链接 | gcc -c hello.c |
-S | 只编译到汇编 | gcc -S hello.c |
-E | 只预处理 | gcc -E hello.c |
-g | 生成调试信息 | gcc -g hello.c -o hello |
-Wall | 开启所有警告 | gcc -Wall hello.c |
-Werror | 警告视为错误 | gcc -Werror hello.c |
-O0 | 不优化(默认) | gcc -O0 hello.c |
-O2 | 优化级别 2 | gcc -O2 hello.c |
-O3 | 优化级别 3 | gcc -O3 hello.c |
-I | 指定头文件搜索路径 | gcc -I./include hello.c |
-L | 指定库文件搜索路径 | gcc -L./lib hello.c |
-l | 链接指定库 | gcc hello.c -lm |
-static | 静态链接 | gcc -static hello.c |
-D | 定义宏 | gcc -DDEBUG hello.c |
-U | 取消定义宏 | gcc -UNDEBUG hello.c |
-v | 显示编译过程 | gcc -v hello.c |
--save-temps | 保存中间文件 | gcc --save-temps hello.c |
多文件编译
# 编译多个源文件
gcc main.c utils.c -o program
# 分别编译,再链接(推荐,只改一个文件时不用全编译)
gcc -c main.c -o main.o
gcc -c utils.c -o utils.o
gcc main.o utils.o -o program
# 修改 utils.c 后,只需重新编译 utils.c 和链接
gcc -c utils.c -o utils.o
gcc main.o utils.o -o program
make:自动化构建工具
手动编译多文件项目很繁琐,make 通过 Makefile 定义编译规则,自动判断哪些文件需要重新编译。
Makefile 基础结构
# 目标: 依赖
# [Tab]命令
program: main.o utils.o
gcc main.o utils.o -o program
main.o: main.c utils.h
gcc -c main.c -o main.o
utils.o: utils.c utils.h
gcc -c utils.c -o utils.o
clean:
rm -f *.o program
注意:命令前必须是 Tab 字符,不能是空格。
make 工作原理
- 检查目标文件是否存在
- 检查依赖文件是否比目标文件新
- 如果依赖更新,执行命令重新生成目标
- 递归检查所有依赖
# 编译(默认执行第一个目标)
make
# 执行指定目标
make clean
# 显示要执行的命令但不执行(-n dry-run)
make -n
# 忽略错误继续执行(-k keep-going)
make -k
# 并行编译(-j jobs)
make -j4
变量和自动变量
# 定义变量
CC = gcc
CFLAGS = -Wall -g -O2
TARGET = program
SRCS = main.c utils.c
OBJS = $(SRCS:.c=.o)
# 使用变量
$(TARGET): $(OBJS)
$(CC) $(OBJS) -o $(TARGET)
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
clean:
rm -f $(OBJS) $(TARGET)
.PHONY: clean
自动变量
| 变量 | 含义 | 示例 |
|---|---|---|
$@ | 目标文件名 | program |
$< | 第一个依赖文件名 | main.c |
$^ | 所有依赖文件名(去重) | main.c utils.c |
$+ | 所有依赖文件名(保留重复) | main.c utils.c main.c |
$? | 比目标新的依赖文件 | utils.c |
$* | 目标的主干名(不含后缀) | main |
模式规则
# 通用编译规则:所有 .o 文件都依赖对应的 .c 文件
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
# 等价于:
# main.o: main.c
# $(CC) $(CFLAGS) -c main.c -o main.o
# utils.o: utils.c
# $(CC) $(CFLAGS) -c utils.c -o utils.o
本篇小结
gcc:GNU C 编译器,编译四阶段:预处理(-E)→ 编译(-S)→ 汇编(-c)→ 链接-o指定输出,-g生成调试信息,-Wall开启所有警告,-O2推荐优化级别-I头文件路径,-L库文件路径,-l链接库(如-lm数学库)make:自动化构建工具,通过 Makefile 定义目标和依赖关系- Makefile 结构:
目标: 依赖 [换行][Tab]命令 - 自动变量:
$@目标,$<第一个依赖,$^所有依赖,$?更新的依赖 - 模式规则:
%.o: %.c通用编译规则
动手实践
单文件编译:
cat > hello.c #include <stdio.h> int main() { printf("Hello, GCC!\n"); return 0; } # Ctrl+D gcc hello.c -o hello ./hello查看编译四阶段:
gcc -E hello.c -o hello.i # 预处理 gcc -S hello.c -o hello.s # 编译到汇编 gcc -c hello.c -o hello.o # 编译到目标文件 gcc hello.o -o hello # 链接 ls -lh hello.*多文件项目:
cat > utils.c #include <stdio.h> void greet(const char* name) { printf("Hello, %s!\n", name); } # Ctrl+D cat > utils.h void greet(const char* name); # Ctrl+D cat > main.c #include "utils.h" int main() { greet("Linux"); return 0; } # Ctrl+D cat > Makefile program: main.o utils.o gcc main.o utils.o -o program main.o: main.c utils.h gcc -c main.c utils.o: utils.c utils.h gcc -c utils.c clean: rm -f *.o program # Ctrl+D make ./program make clean使用变量优化 Makefile:
CC = gcc CFLAGS = -Wall -g SRCS = main.c utils.c OBJS = $(SRCS:.c=.o) TARGET = program $(TARGET): $(OBJS) $(CC) $(OBJS) -o $(TARGET) %.o: %.c $(CC) $(CFLAGS) -c $< -o $@ clean: rm -f $(OBJS) $(TARGET) .PHONY: clean编译带警告的程序:
cat > warn.c int main() { int x; printf("%d\n", x); // 未初始化变量 return 0; } # Ctrl+D gcc warn.c -o warn # 无警告 gcc -Wall warn.c -o warn # 显示警告 gcc -Werror warn.c -o warn # 警告变错误,编译失败思考:为什么
make比手动执行gcc命令更高效?如果修改了utils.h但没有修改utils.c,make会重新编译哪些文件?为什么?