Makefile 入门 - 知乎
https://zhuanlan.zhihu.com/p/149346441
今天抽空研究了下 Makefile,在这里整理一下各处搜到的资料,以备将来复习时快速上手,同时也帮助和我一样的初学者们节约时间(全文阅读时间不超过20分钟)。
首先,假设我们有如下几个代码文件:main.cpp functions.h function1.cpp function2.cpp (代码来自:Using make and writing Makefile(in C++ or C))。
- -- functions.h ---
// functions.h
void print_hello();
int factorial(int n);
- -- function1.cpp ---
// function1.cpp
#include "functions.h"int factorial(int n){
if (n!=1){
return n*factorial(n-1);
else return 1;
}
- -- function2.cpp ---
//function2.cpp
#include<iostream>#include "functions.h"
void print_hello(){
std::cout<<"Hello World"<<std::endl;
}
- -- main.cpp ---
//main.cpp
# include<iostream>
# include "functions.h"
int main(){
print_hello();
std::cout << "this is main" << std::endl;
std::cout << "The factorial of 5 is " << factorial(5) << std::endl;
return 0;
}
不用 makefile 如何编译?
如果不用 makefile,则需要按照下面的方式编译上述代码:
g++ -c function1.cpp
g++ -c function2.cpp
g++ -c main.cpp
g++ -o hello main.o function1.o function2.o
其中,g++ -c function1.cpp 会将源码编译成名为 function1.o 对象文件。如果不想采用默认的命名,也可以自定义文件名,例如:g++ -c function1.cpp -o fun1.o。
也可以用一行命令整合编译、链接的步骤:
g++ -o hello main.cpp function1.cpp function2.cpp
这种方式有很多弊端,例如:
- 每次编译、链接都需要手动敲的很多命令。
- 当工程量很大时,编译整个工程需要花很久。而我们往往并不是每次都修改了所有源文件,因此希望程序自动编译那些被修改的源码,而没被修改的部分不要浪费时间重新编译。
为了解决上述第一个问题,我们可以把所有编译需要的命令保存到文件中,编译时一键执行。针对第二个问题,我们希望有一个软件,自动检测哪些源文件被修改过,然后自动把它们挑出来选择性地编译。而 make 命令通过检测代码文件的时间戳,决定是否编译它。
第一版 Makefile
首先需要确定 Makefile 的名字,需要设置成 Makefile 或者 makefile,而不能是其它版本(MakeFile, Make_file, makeFile,... )。其次,需要注意的是 Makefile 是缩进敏感的,在行首一定不能随便打空格。下面我们看一下第一版 Makefile。
# Makefile (井号为注释)
all:
g++ -o hello main.cpp function1.cpp function2.cpp
clean:
rm -rf *.o hello
(注意上面代码片段的缩进,是一个
其中 all 、clean的术语为 target,我也可以随意指定一个名字,例如 abc,真正执行编译的是它下面缩进行的命令。我们可以看到,这个命令和我们在命令行中手动敲的没有任何区别。因此,通过这个简单的 Makefile,就可以省去了每次手动敲命令的痛苦:只需要在命令行敲下 make 回车,即可完成编译。
clean 表示清除编译结果,它下方就是普通的命令行删除文件命令。命令行输入 make 将默认执行第一个 target (即 all)下方的命令;如要执行清理操作,则需要输入 make clean,指定执行 clean 这个 target 下方的命令。
这个 Makefile 虽然可以省去敲命令的痛苦,却无法选择性编译源码。因为我们把所有源文件都一股脑塞进了一条命令,每次都要编译整个工程,很浪费时间。第二版 Makefile 将解决这个问题。
第二版 Makefile
既然我们希望能够选择性地编译源文件,就不能像上一节那样把所有源文件放在一条命令里编译了,而是要分开写:
all: hello
hello: main.o function1.o function2.o
g++ main.o function1.o function2.o -o hello
main.o: main.cpp
g++ -c main.cpp
function1.o: function1.cpp
g++ -c function1.cpp
function2.o: function2.cpp
g++ -c function2.cpp
clean:
rm -rf *.o hello
上面的 Makefile 包含了一条重要的语法:
顺着代码捋一下逻辑:
- 命令行输入 make ,将默认执行 all 这个 target;
- 而 all 这个 target 依赖于 hello,hello 在当前目录下并不存在,于是程序开始往下读取命令..……终于找到了 hello 这个 target;
- 正待执行 hello 这个 target 的时候,却发现它依赖于 main.o,function1.o,function2.o 这三个文件,而它们在当前目录下都不存在,于是程序继续向下执行;
- 遇到 main.o target,它依赖于 main.cpp。而 main.cpp 是当前目录下存在的文件,终于可以编译了,生成 main.o 对象文件。后面两个函数以此类推,都编译好之后,再回到 hello target,连接各种二进制文件,生成 hello 文件。
第一次编译的时候,命令行会输出:
g++ -c main.cpp
g++ -c function1.cpp
g++ -c function2.cpp
g++ main.o function1.o function2.o -o hello
证明所有的源码都被编译了一遍。假如我们对 main.cpp 做一点修改,再重新 make(重新 make 前不要 make clean),则命令行只会显示:
g++ -c main.cpp
g++ main.o function1.o function2.o -o hello
这样,我们就发挥出 Makefile 选择性编译的功能了。下面,将介绍如何在 Makefile 中声明变量(declare variable)。
第三版 Makefile
我们希望将需要反复输入的命令整合成变量,用到它们时直接用对应的变量替代,这样如果将来需要修改这些命令,则在定义它的位置改一行代码即可。
CC = g++
CFLAGS = -c -Wall
LFLAGS = -Wall
all: hello
hello: main.o function1.o function2.o
$(CC) $(LFLAGS) main.o function1.o function2.o -o hello
main.o: main.cpp
$(CC) $(CFLAGS) main.cpp
function1.o: function1.cpp
$(CC) $(CFLAGS) function1.cpp
function2.o: function2.cpp
$(CC) $(CFLAGS) function2.cpp
clean:
rm -rf *.o hello
上面的 Makefile 中,开头定义了三个变量:CC,CFLAGS,和 LFLAGS。其中 CC 表示选择的编译器(也可以改成 gcc);CFLAGS 表示编译选项,-c 即 g++ 中的 -c,-Wall 表示显示编译过程中遇到的所有 warning;LFLAGS 表示链接选项,它就不加 -c 了。这些名字都是自定义的,真正起作用的是它们保存的内容,因此只要后面的代码正确引用,将它们定义成阿猫阿狗都没问题。容易看出,引用变量名时需要用 $() 将其括起来,表示这是一个变量名。
第四版 Makefile
第三版的 Makefile 还是不够简洁,例如我们的 dependencies 中的内容,往往和 g++ 命令中的内容重复:
hello: main.o function1.o function2.o
$(CC) $(LFLAGS) main.o function1.o function2.o -o hello
我们不想敲那么多字,能不能善用
例如我们有 target: dependencies 对:all: library.cpp main.cpp
- $@ 指代 all ,即 target
- $< 指代 library.cpp, 即第一个 dependency
- $^ 指代 library.cpp 和 main.cpp,即所有的 dependencies
因此,本节开头的 Makefile 片段可以改为:
hello: main.o function1.o function2.o
$(CC) $(LFLAGS) $^ -o $@
而第四版 Makefile 就是这样的:
CC = g++
CFLAGS = -c -Wall
LFLAGS = -Wall
all: hello
hello: main.o function1.o function2.o
$(CC) $(LFLAGS) $^ -o $@
main.o: main.cpp
$(CC) $(CFLAGS) $<
function1.o: function1.cpp
$(CC) $(CFLAGS) $<
function2.o: function2.cpp
$(CC) $(CFLAGS) $<
clean:
rm -rf *.o hello
但是手动敲文件名还是有点麻烦,能不能自动检测目录下所有的 cpp 文件呢?此外 main.cpp 和 main.o 只差一个后缀,能不能自动生成对象文件的名字,将其设置为源文件名字后缀换成 .o 的形式?
第五版 Makefile
想要实现自动检测 cpp 文件,并且自动替换文件名后缀,需要引入两个新的命令:patsubst 和 wildcard。
5.1 wildcard
wildcard 用于获取符合特定规则的文件名,例如下面的代码:
SOURCE_DIR = . # 如果是当前目录,也可以不指定
SOURCE_FILE = $(wildcard $(SOURCE_DIR)/*.cpp)
target:
@echo $(SOURCE_FILE)
make 后发现,输出的为当前目录下所有的 .cpp 文件:
./function1.cpp ./function2.cpp ./main.cpp
其中 @echo 前加 @是为了避免命令回显,上文中 make clean 调用了 rm -rf 会在 terminal 中输出这行命令,如果在 rm 前加了 @ 则不会输出了。
5.2 patsubst
patsubst 应该是 pattern substitution 的缩写。用它可以方便地将 .cpp 文件的后缀换成 .o。它的基本语法是:$(patsubst 原模式,目标模式,文件列表)。运行下面的示例:
SOURCES = main.cpp function1.cpp function2.cpp
OBJS = $(patsubst %.cpp, %.o, $(SOURCES))
target:
@echo $(SOURCES)
@echo $(OBJS)
输出的结果为:
main.cpp function1.cpp function2.cpp
main.o function1.o function2.o
5.3 综合两个命令
综合上述两个命令,我们可以升级到第五版 Makefile:
OBJS = $(patsubst %.cpp, %.o, $(wildcard *.cpp))
CC = g++
CFLAGS = -c -Wall
LFLAGS = -Wall
all: hello
hello: $(OBJS)
$(CC) $(LFLAGS) $^ -o $@
main.o: main.cpp
$(CC) $(CFLAGS) $< -o $@
function1.o: function1.cpp
$(CC) $(CFLAGS) $< -o $@
function2.o: function2.cpp
$(CC) $(CFLAGS) $< -o $@
clean:
rm -rf *.o hello
然而这一版的 Makefile 还有提升空间,它的 main.o,function1.o,function2.o 使用的都是同一套模板,不过换了个名字而已。第六版的 Makefile 将处理这个问题。
第六版 Makefile
这里要用到 Static Pattern Rule,其语法为:
targets: target-pattern: prereq-patterns
其中 targets 不再是一个目标文件了,而是一组目标文件。而 target-pattern 则表示目标文件的特征。例如目标文件都是 .o 结尾的,那么就将其表示为 %.o,prereq-patterns (prerequisites) 表示依赖文件的特征,例如依赖文件都是 .cpp 结尾的,那么就将其表示为 %.cpp。
通过上面的方式,可以对 targets 列表中任何一个元素,找到它对应的依赖文件,例如通过 targets 中的 main.o,可以锁定到 main.cpp。
下面是第六版的 Makefile
OBJS = $(patsubst %.cpp, %.o, $(wildcard *.cpp))
CC = g++
CFLAGS = -c -Wall
LFLAGS = -Wall
all: hello
hello: $(OBJS)
$(CC) $(LFLAGS) $^ -o $@
$(OBJS):%.o:%.cpp
$(CC) $(CFLAGS) $< -o $@
clean:
rm -rf *.o hello
杂
看到有的 Makefile 设置了 -lm 的 flag,查阅资料发现表示连街 math 库,因为代码中可能 #include<math.h> 。
例如 g++ -o out fun.cpp -lm
CC = g++
LIBS = -lm
out: fun.cpp
$(CC) -o $@ $^ $(LIBS)
总结
本文介绍了如何写 Makefile,主要的知识点有:
- 在 Makefile 中定义变量并引用
- @,$< 的含义
- wildcard,patsubst 的用法
- static pattern rule:targets: target-pattern: prereq-patterns
感谢阅读,希望各位朋友能有收获。