前面我们学习了:
结构体
动态内存
文件操作
预处理与头文件
多文件编程
现在假设项目有:
main.c
student.c
student.h
file.c
file.h
以前我们可能直接:
gcc main.c student.c file.c -o student_manager文件少的时候还好。
但是如果项目变成:
main.c
student.c
student.h
file.c
file.h
menu.c
menu.h
utils.c
utils.h
config.c
config.h
每次都手动输入:
gcc ...就会越来越麻烦。
而且修改一个文件之后,通常只需要重新编译相关部分,不需要每次手动判断全部文件。
这就是:
Makefile 的作用。
一、什么是 Makefile
Makefile 是一个用于描述:
如何编译项目
的文件。
我们可以在里面写:
哪些文件需要编译
依赖什么文件
使用什么命令
生成什么结果
然后只需要执行:
make就可以自动完成编译。
二、最简单的 Makefile
假设有:
main.c
里面:
#include <stdio.h>
int main() {
printf("Hello World!\n");
return 0;
}Makefile:
main: main.c
gcc main.c -o main
然后:
make就相当于执行:
gcc main.c -o main三、Makefile 的基本格式
最核心的格式:
目标: 依赖
命令
例如:
main: main.c
gcc main.c -o main
这里:
main
↓
目标
main.c
↓
依赖
gcc main.c -o main
↓
命令
四、为什么命令前面必须是 Tab
例如:
main: main.c
gcc main.c -o main
gcc 前面必须是:
Tab
不能简单使用空格。
也就是说:
main: main.c
[Tab]gcc main.c -o main
如果把 Tab 换成空格,很多 make 实现会报错。
例如常见:
missing separator
所以学习 Makefile 时要特别注意:
命令行开头使用 Tab。
五、目标和依赖
例如:
main: main.c
gcc main.c -o main
意思可以理解成:
如果 main.c 有变化
↓
重新执行 gcc
↓
生成 main
如果:
main.c
没有变化,而:
main
已经存在:
make通常就不会重复执行编译命令。
这就是 Makefile 最重要的功能之一:
根据文件时间自动判断是否需要重新构建。
六、多文件项目
假设:
project/
├─ main.c
├─ student.c
└─ student.h
以前:
gcc main.c student.c -o mainMakefile 可以写:
main: main.c student.c
gcc main.c student.c -o main
执行:
make即可。
七、三个文件的项目
再复杂一点:
main.c
student.c
file.c
Makefile:
main: main.c student.c file.c
gcc main.c student.c file.c -o main
这样:
make就可以:
main.c
student.c
file.c
↓
gcc
↓
main
八、为什么还要目标文件 .o
实际上 C 项目的编译过程通常是:
main.c
↓
main.o
student.c
↓
student.o
file.c
↓
file.o
main.o
+
student.o
+
file.o
↓
链接
↓
main
所以我们可以让 Makefile 更合理地管理:
.c
↓
.o
↓
可执行文件
九、编译一个 .c 为 .o
GCC:
gcc -c main.c -o main.o这里:
-c
表示:
只编译,不进行最终链接。
例如:
main.c
↓
main.o
十、链接多个 .o
例如:
gcc main.o student.o file.o -o main就是:
main.o
student.o
file.o
↓
链接
↓
main
这和上一章学习的:
编译
+
链接
完全对应。
十一、Makefile 管理 .o
可以写:
main: main.o student.o file.o
gcc main.o student.o file.o -o main
main.o: main.c
gcc -c main.c -o main.o
student.o: student.c student.h
gcc -c student.c -o student.o
file.o: file.c file.h student.h
gcc -c file.c -o file.o
执行:
makeMake 会自动判断:
main.c
↓
main.o
student.c + student.h
↓
student.o
file.c + file.h + student.h
↓
file.o
最后
↓
main
十二、依赖关系的重要性
例如:
student.o: student.c student.h
表示:
student.o
依赖
student.c
student.h
所以只要:
student.c
或者:
student.h
发生修改:
student.o
就需要重新生成。
十三、头文件为什么也要写进依赖
例如:
student.c
student.h
student.c:
#include "student.h"如果你修改:
student.h
比如把:
typedef struct {
char name[50];
int age;
} Student;改成:
typedef struct {
char name[100];
int age;
double score;
} Student;那么:
student.c
依赖的头文件已经发生变化。
因此:
student.o
也应该重新编译。
所以写:
student.o: student.c student.h
很重要。
十四、Makefile 的完整基本结构
可以写:
main: main.o student.o file.o
gcc main.o student.o file.o -o main
main.o: main.c
gcc -c main.c -o main.o
student.o: student.c student.h
gcc -c student.c -o student.o
file.o: file.c file.h student.h
gcc -c file.c -o file.o
这是非常值得手写一遍的基础版本。
十五、变量
Makefile 可以定义变量。
例如:
CC = gcc
之后:
$(CC)
就代表:
gcc
例如:
CC = gcc
main: main.o
$(CC) main.o -o main
十六、CFLAGS
还可以定义:
CFLAGS = -Wall -Wextra
这样:
$(CC) $(CFLAGS)
就会变成:
gcc -Wall -Wextra例如:
CC = gcc
CFLAGS = -Wall -Wextra
main: main.o
$(CC) $(CFLAGS) main.o -o main
十七、LDFLAGS 和 LDLIBS
大型项目中还会看到:
LDFLAGS
LDLIBS
例如:
LDLIBS = -lm
表示链接数学库。
不过当前学习阶段:
CC
CFLAGS
先掌握即可。
十八、自动变量
Makefile 里有几个非常重要的特殊变量:
$@
$<
$^
需要重点掌握。
十九、$@
$@ 表示:
当前目标。
例如:
main.o: main.c
gcc -c main.c -o $@
这里:
$@
就是:
main.o
相当于:
gcc -c main.c -o main.o二十、$<
$< 表示:
第一个依赖文件。
例如:
main.o: main.c
gcc -c $< -o $@
这里:
$<
↓
main.c
$@
↓
main.o
所以:
gcc -c main.c -o main.o二十一、$^
$^ 表示:
所有依赖文件。
例如:
main: main.o student.o file.o
gcc $^ -o $@
那么:
$^
↓
main.o student.o file.o
$@
↓
main
相当于:
gcc main.o student.o file.o -o main二十二、三个自动变量总结
$@
↓
目标
$<
↓
第一个依赖
$^
↓
所有依赖
这个非常重要。
二十三、使用自动变量重写 Makefile
前面的:
main: main.o student.o file.o
gcc main.o student.o file.o -o main
main.o: main.c
gcc -c main.c -o main.o
student.o: student.c student.h
gcc -c student.c -o student.o
file.o: file.c file.h student.h
gcc -c file.c -o file.o
可以变成:
CC = gcc
CFLAGS = -Wall -Wextra
main: main.o student.o file.o
$(CC) $(CFLAGS) $^ -o $@
main.o: main.c
$(CC) $(CFLAGS) -c $< -o $@
student.o: student.c student.h
$(CC) $(CFLAGS) -c $< -o $@
file.o: file.c file.h student.h
$(CC) $(CFLAGS) -c $< -o $@
已经更加规范了。
二十四、模式规则
如果每个 .c 文件都写:
main.o: main.c
gcc -c main.c -o main.o
student.o: student.c
gcc -c student.c -o student.o
file.o: file.c
gcc -c file.c -o file.o
其实非常重复。
可以使用:
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
意思是:
任何 .o
↓
如果存在对应的 .c
↓
按照这个规则编译
例如:
main.o
↓
main.c
student.o
↓
student.c
file.o
↓
file.c
都可以匹配。
二十五、使用模式规则
完整:
CC = gcc
CFLAGS = -Wall -Wextra
main: main.o student.o file.o
$(CC) $(CFLAGS) $^ -o $@
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
执行:
makeMake 会自动:
main.c
↓
main.o
student.c
↓
student.o
file.c
↓
file.o
最后链接
↓
main
二十六、clean
编译后可能产生:
main
main.o
student.o
file.o
可以添加:
clean:
rm -f main main.o student.o file.o
然后:
make clean就可以清理编译产生的文件。
Windows 环境下,如果使用的是 GNU Make,rm
是否可用取决于你的环境;为了跨环境更稳定,也可以专门写适合当前 Shell
的清理命令。
二十七、.PHONY
clean 实际上不是一个真正的文件。
但是假设目录里面刚好存在一个叫:
clean
Make 可能认为:
clean
↓
已经存在
导致不执行。
因此常见写法:
.PHONY: clean
然后:
clean:
rm -f main main.o student.o file.o
表示:
clean
不是文件
是一个命令目标
二十八、最终推荐的基础 Makefile
对于当前学习阶段,可以使用:
CC = gcc
CFLAGS = -Wall -Wextra
TARGET = main
OBJS = main.o student.o file.o
$(TARGET): $(OBJS)
$(CC) $(CFLAGS) $^ -o $@
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
.PHONY: clean
clean:
rm -f $(TARGET) $(OBJS)
这里:
CC
↓
编译器
CFLAGS
↓
编译选项
TARGET
↓
最终程序名字
OBJS
↓
目标文件列表
二十九、执行 make
假设:
Makefile
main.c
student.c
student.h
file.c
file.h
执行:
make可能会得到:
main.o
student.o
file.o
main
流程:
main.c
↓
main.o
│
student.c
↓
student.o
│
file.c
↓
file.o
│
└──────┐
↓
链接
↓
main
三十、第二次执行 make
假设:
make第一次已经构建完成。
然后再次:
make如果源文件都没有变化:
不需要重新编译
因为 Make 会检查文件的修改时间和依赖关系。
这就是 Makefile 比手动:
gcc main.c student.c file.c -o main方便的地方之一。
三十一、修改一个源文件
假设你修改:
student.c
然后:
makeMake 会发现:
student.c
比 student.o 新
于是:
student.c
↓
重新编译
↓
student.o
然后:
main.o
+
student.o
+
file.o
↓
重新链接
↓
main
而:
main.c
file.c
如果没有变化,就不必重新编译。
三十二、修改头文件
假设修改:
student.h
因为:
student.o: student.c student.h
所以:
student.h
↓
影响 student.o
↓
重新编译 student.c
如果其他文件也依赖:
student.h
它们也应该重新构建。
这就是为什么:
依赖关系必须写正确。
三十三、make 命令
最常见:
make构建默认目标。
清理:
make clean也可以指定目标:
make main三十四、默认目标
例如:
main: main.o student.o file.o
$(CC) $(CFLAGS) $^ -o $@
clean:
rm -f main main.o student.o file.o
执行:
make时,Make 默认会处理:
第一个目标
也就是:
main
所以一般把最终程序的目标写在前面。
三十五、多个目标
例如:
all: main
clean:
rm -f main *.o
可以:
make all也可以:
make clean通常会把:
all
clean
都声明为:
.PHONY: all clean
三十六、通配符
Makefile 可以看到:
*.o
例如:
clean:
rm -f *.o main
表示删除所有:
.o
文件。
三十七、一个更完整的项目
例如:
student_manager/
│
├─ Makefile
├─ main.c
│
├─ student.c
├─ student.h
│
├─ file.c
└─ file.h
Makefile:
CC = gcc
CFLAGS = -Wall -Wextra
TARGET = student_manager
OBJS = main.o student.o file.o
$(TARGET): $(OBJS)
$(CC) $(CFLAGS) $^ -o $@
main.o: main.c student.h file.h
$(CC) $(CFLAGS) -c $< -o $@
student.o: student.c student.h
$(CC) $(CFLAGS) -c $< -o $@
file.o: file.c file.h student.h
$(CC) $(CFLAGS) -c $< -o $@
.PHONY: clean
clean:
rm -f $(TARGET) $(OBJS)
三十八、你现在应该理解的完整流程
把上一章和这一章连接起来:
main.c
student.c
file.c
student.h
file.h
↓
预处理
↓
编译
↓
main.o
student.o
file.o
↓
链接
↓
student_manager
而 Makefile 做的是:
告诉 make:
↓
这些文件有什么依赖
↓
怎么编译
↓
怎么链接
↓
怎么清理
三十九、Makefile 本身不是 C 代码
例如:
CC = gcc
和:
int a = 10;完全不是一种语言。
Makefile 是给:
make
这个构建工具使用的。
它主要负责:
编译流程管理
而不是程序本身的功能。
四十、Makefile 与 GCC 的关系
可以理解成:
你
↓
make
↓
Makefile
↓
告诉 make 应该执行什么
↓
gcc
↓
编译 C 程序
所以:
GCC
↓
真正负责 C 的编译和链接
Make
↓
负责自动管理这些编译命令
四十一、本章练习
练习 1
创建:
main.c
Makefile
Makefile 实现:
make可以编译:
main.c
练习 2
创建:
main.c
hello.c
hello.h
Makefile
要求:
make自动完成:
main.o
hello.o
hello
练习 3
给项目添加:
student.c
student.h
让 Makefile 自动处理:
main.o
student.o
hello.o
最后生成:
student_manager
练习 4
加入:
.PHONY: clean
clean:
...
实现:
make clean删除:
.o
可执行文件
练习 5
自己使用:
$@
$<
$^
重写编译规则。
例如:
main: main.o student.o
$(CC) $^ -o $@
练习 6
加入:
CFLAGS = -Wall -Wextra
然后:
$(CC) $(CFLAGS)
编译。
观察 GCC 出现警告时的效果。
练习 7
完成你自己的项目:
course_manager/
│
├─ Makefile
├─ main.c
├─ course.c
├─ course.h
├─ file.c
├─ file.h
└─ config.h
要求:
make完成整个项目的编译。
然后:
make clean清理构建文件。
Comments NOTHING