Utopi
a

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

这种方式有很多弊端,例如:

  1. 每次编译、链接都需要手动敲的很多命令。
  2. 当工程量很大时,编译整个工程需要花很久。而我们往往并不是每次都修改了所有源文件,因此希望程序自动编译那些被修改的源码,而没被修改的部分不要浪费时间重新编译。

为了解决上述第一个问题,我们可以把所有编译需要的命令保存到文件中,编译时一键执行。针对第二个问题,我们希望有一个软件,自动检测哪些源文件被修改过,然后自动把它们挑出来选择性地编译。而 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

(注意上面代码片段的缩进,是一个而不是4个或者8个空格。)

其中 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 包含了一条重要的语法::。即,目标:目标依赖的文件。

顺着代码捋一下逻辑:

  1. 命令行输入 make ,将默认执行 all 这个 target;
  2. 而 all 这个 target 依赖于 hello,hello 在当前目录下并不存在,于是程序开始往下读取命令..……终于找到了 hello 这个 target;
  3. 正待执行 hello 这个 target 的时候,却发现它依赖于 main.o,function1.o,function2.o 这三个文件,而它们在当前目录下都不存在,于是程序继续向下执行;
  4. 遇到 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

我们不想敲那么多字,能不能善用 : 中的内容呢?这就需要引入下面几个特殊符号了(也正是这些特殊符号,把 Makefile 搞得像是天书,吓退了很多初学者):@ ,<,$^。

例如我们有 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

感谢阅读,希望各位朋友能有收获。