Makefile入门
Posted faf4r
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Makefile入门相关的知识,希望对你有一定的参考价值。
【Makefile 20分钟入门,简简单单,展示如何使用Makefile管理和编译C++代码】的学习笔记
C/C++多文件编译
以C++为例,假设有main.cpp
, A.cpp
, B.cpp
这三个源文件和head.h
头文件,
要编译出可执行文件main
(这个命名自定)
演示代码在附录或 直接下载
直接编译
$ g++ main.cpp A.cpp B.cpp -o main
# 或者
$ g++ -o main main.cpp A.cpp B.cpp
-o
后面接输出文件名(目标
),其它的都是依赖
,就是靠这些依赖,也就是源代码生成可执行文件。
所以上面的步骤可以简化为
gcc或g++ -o 目标 依赖
或者
gcc或g++ 依赖 -o 目标
单独编译源文件,然后链接
$ g++ A.cpp -c # 生成A.o文件
$ g++ -c B.cpp # 生成B.o文件
$ g++ -c main.cpp # 生成main.o文件
.o
(Windows下是.obj)就是object,目标代码
,已经是机器码
了,链接后就是可执行文件
(注意这里的目标不是最终的目标,只是这个文件叫这个而已,DOS系统叫对象文件)
另外这里的-c放哪里都行,没有顺序要求。
其次,也可以使用通配符一次性生成.o文件
$ g++ -c *.cpp
# 或
$ g++ *.cpp -o
进行链接:
$ g++ *.o -o main
同理,链接时可以一个一个写出.o
文件,也可用通配符来写
这只是用到的编译中间过程,具体详细的中间过程
参考:C语言的编译过程详解
Makefile的工作原理(个人理解)
Makefile本质就是帮你简化了编译命令的编写,当你在命令行输入make
的时候,就跟运行shell脚本一样,它会按照规则运行相应的命令。
下面以实际的例子来学习。
提示:命令make会知道去pwd找Makefile文件去运行,如果你的makefile不是默认名,那就要用
$ make -f 文件名
(PS: makefile 小写也是默认的,一样)
Makefile Level 1 -- 命令和依赖
# Level 1
# 注释语法和Python、shell一样,用的#号
main: main.cpp A.cpp B.cpp head.h
g++ -o main main.cpp A.cpp B.cpp
这就是最基本的Makefile语法,最后两行才是起作用的。
其中,顶格的main
就是我们要生成的目标
,而冒号后面的就是依赖
再下一行,开头是一个\\t
而不是空格,就是一个tab
,开头是一个\\t
而不是空格,就是一个tab
,开头是一个\\t
而不是空格,就是一个tab
。在markdowm里显示是4个空格,但一定记住这是这是\\t
tab后面就是需要运行的真正的shell命令。
关于依赖
冒号后面的是依赖(冒号后可空格可不空),这些依赖你不写也是可以的,比如
main:
g++ -o main main.cpp A.cpp B.cpp
这代表这个目标没有依赖,直接运行下面的命令了。
所以,依赖有什么用呢?
简单来说就是,Makefile会帮你检测依赖和目标是否更新。
比如,运行make后再运行make,它的结果是这样的:
make: “main”已是最新。
可见,当目标(也就是main)已经存在时,它就不一定帮你重新编译了(毕竟大型项目你重新编译一下需要的时间太久了)。
那么它怎样才会重新编译呢?就是看依赖
了。
如果你有指定依赖,那么,
Makefile会检测目标和依赖之间的修改时间,从而确定目标是否需要更新
。
比如你修改了A.cpp
,如果依赖有A.cpp
,那么Makefile就会检测到更新从而重新编译main
,
如果依赖没有A.cpp
,那么无论你怎么修改A.cpp
它都不会编译。
同理,即使你在依赖加一个无关的文件比如test.txt
,那么只要你修改了test.txt
,运行make它就会重新编译。
Makefile Level 2 -- 引入变量
# level 2
CXX = g++ # 指定编译器
TARGET = main # 指定目标
SRC = main.cpp A.cpp B.cpp
$(TARGET): $(SRC) head.h
$(CXX) -o $(TARGET) $(SRC)
可以看到,这里引入了变量来简化编写,而且语法规则应该和shell命令是一样的。
变量名可以自定,实际命令把变量值代入即可。
注意这里SRC
是指代.cpp
文件,没有头文件,因此依赖那手动加一下,或者把头文件也做成变量:
# level 2
CXX = g++ # 指定编译器
TARGET = main # 指定目标
SRC = main.cpp A.cpp B.cpp
HEAD = head.h
$(TARGET): $(SRC) $(HEAD)
$(CXX) -o $(TARGET) $(SRC)
这样,引入变量后,项目变更时只需修改变量即可。
Makefile Level 3 -- 分开编译
# level 3
CXX = g++
TARGET = main
OBJ = main.o A.o B.o
HEAD = head.h
$(TARGET): $(OBJ) $(HEAD)
$(CXX) -o $(TARGET) $(OBJ)
main.o: main.cpp
$(CXX) -c main.cpp
A.o: A.cpp
$(CXX) -c A.cpp
B.o: B.cpp
$(CXX) -c B.cpp
可以看到,这里我们不是使用所有源文件直接编译,而是单独编译每个源文件,然后链接所有目标代码。
因此-o
那一行只是链接的命令,那么目标代码哪里来呢?就是下面的其它目标了。
首先,目标只有一个:即第一个目标$(TARGET)
,我叫它主目标。
Makefile会检查目标和依赖,发现都没有,下面又有依赖的生成方式,那么它就会先完成下面的子目标,使得依赖满足,然后进行编译。
如果依赖发生了更新,但因为你更新的是源代码,那么下面的子目标就会检测到需要更新,于是它会更新相应的依赖,但不会全部更新。然后再链接所有的依赖。
这样做,就避免了大型项目改动之后全部重新编译的弊端。
但是要注意子目标和主目标的依赖要同时编辑。
同时要明确,只有第一个目标是主目标,下面的都是子目标。
# level 3
CXX = g++
TARGET = main
OBJ = main.o A.o B.o
HEAD = head.h
A.o: A.cpp
$(CXX) -c A.cpp
$(TARGET): $(OBJ) $(HEAD)
$(CXX) -o $(TARGET) $(OBJ)
main.o: main.cpp
$(CXX) -c main.cpp
B.o: B.cpp
$(CXX) -c B.cpp
比如我调换一下顺序,删除生成的main
,再运行make
make: “A.o”已是最新。
可见,它检查的是第一个目标,而不是后面的子目标。
因为主目标的A.o
和A.cpp
都是最新的,所以就有上面的输出。
简单理解子目标:
主目标的依赖也利用Makefile来生成,子目标是对依赖的补充,也就是对主目标的补充。其实就是把主目标给拆分了
简单实验一下,如果把A.cpp删了,清除所有输出,会发生什么呢?
g++ -c main.cpp
make: *** 没有规则可制作目标“A.o”,由“main” 需求。 停止。
可以看到,它说目标A.o是main所需要的,也就是依赖,“没有规则”说明当依赖没有时,它会自己寻找下面有没有子目标是生成依赖的,有的话就生成,没有就出现上面的输出。
同时我们在Makefile里加一个新的无关的子目标,无论是编译还是修改了这个无关目标的文件,发现对整个正常过程没有任何影响。说明它只看主目标,子目标是根据需要才看的。
Makefile Level 4 -- 分开编译+通配符(高级变量)
# level 4
CXX = g++
TARGET = main
OBJ = main.o A.o B.o
HEAD = head.h
$(TARGET): $(OBJ) $(HEAD)
$(CXX) -o $@ $^
%.o: %.cpp
$(CXX) -c $< -o $@
这里先声明一下,视频里没有考虑头文件,但个人还是考虑了。
因为链接时加入头文件并无影响,所以我加入了头文件作为依赖。
为什么说这个呢,下面一一解释代码的含义。
$@
这是Makefile的内置变量,代表的就是目标(它所在的那一个目标)
因此每一个目标的-o
后面都接了$@
$^
这个箭头向上,代表所在目标的所有依赖
因此主目标在链接时,会把头文件也带上。
$<
这个箭头向左,代表所在目标的第一个依赖
在子目标里,实际执行的命令诸如:
$ g++ -c A.cpp -o A.o
其实和前面一样,对单个源文件编译,所以只有一个依赖。
这当然不是说%.cpp
代表很多个依赖,其实也是一个,所以在这个子目标里,也可以用$^
:
%.o: %.cpp
$(CXX) -c $^ -o $@
关于这个-o
其实这里的-o
(指定输出文件名)也可以省略:
%.o: %.cpp
$(CXX) -c $^
它自己会生成%.o
形式的目标,加一个-o
只是明确指定它的输出文件名和目标一致。
这个建议加上,比如在别的系统g++ -c file.cpp
默认生成的可能是file.obj
而不是file.o
,虽然本质都一样,但是因为名称不对就出现bug了。所以加上这个能减少出bug的几率。
%通配符
%.o: %.cpp
$(CXX) -c $< -o $@
这个%
其实和命令行里的*
是一样的,就是个通配符。
那么通配符的内容来自哪呢?这要再了解一下Makefile的机制。
运行make,所有文件都有了,此时只删除A.o
(这是主目标的依赖之一)
再运行make,Makefile检查主目标和依赖,发现依赖更新了(没了就是和原来不一样了就是更新了),于是需要重新编译。
还是分开编译的原理,因为其它依赖B.o
,main.o
都是最新的,不需编译。
只有A.o
需要编译,那么它就会找子目标看有没有编译它的规则。
%.o
、A.o
都是和A.o
匹配的,所以这个带通配符的目标与需要的依赖匹配,它就会自动代入进去,也就是成了:
A.o: A.cpp
$(CXX) -c $< -o $@
因此看Makefile给出的输出就只有两条编译命令:
g++ -c A.cpp -o A.o
g++ -o main main.o A.o B.o head.h
另外注意它只是匹配%.o
,而%.cpp
也是跟着它的规则来的,而如果写的是%
而不是%.cpp
则会有点问题。
Makefile Level 5 -- clean && -Wall
# level 5
CXX = g++
TARGET = main
OBJ = main.o A.o B.o
HEAD = head.h
CXXARGS = -c -Wall
$(TARGET): $(OBJ) $(HEAD)
$(CXX) -o $@ $^
%.o: %.cpp
$(CXX) $(CXXARGS) $< -o $@
.PHONY: clean
clean:
rm -f *.o $(TARGET)
同样的,利用变量,把g++的参数也变量化了,但这次多了一个参数-Wall
,这是g++编译的命令。
-Wall
中的W
表示warning
警告,all
就是全部。这个命令表示在编译时输出所有的警告。
.PHONY
这是Makefile的内置变量,含义就是不检测这个目标是否存在或更新。
若先注释掉.PHONY: clean
,对整个Makefile没有影响,但是这个clean目标怎么用呢?
clean是子目标,子目标只有在主目标依赖需要时才运行,明显这个clean是永远不可能运行的。
所以我们需要手动运行它:
$ make clean
通过make加子目标将主目标转为我们指定的目标。
比如还可make A.o
来指定编译A.cpp
甚至不存在的文件,比如make FIle.o
,反正只要符合规则即可
而这个目标没有依赖,也就是说,只要生成这个目标,就会运行目标下面的编译代码,只是现在把编译代码换成了删除命令而已。
正如起名,这行代码就是删除所有输出的.o
文件和目标main
。
那么.PHONY
有什么用呢?
如果我们touch clean
创建一个叫clean
的文件,然后再make clean
试试?
发现因为这个clean
已经存在了,所以它不生成了,所以它就不会运行下面的命令。
而.PHONY
的作用就是忽略目标是否更新,不管怎样都要再生成目标,即保证我们运行下面的命令。
最后再说这个clean,一般Makefile会有这个,保证编译时清除了以前的残留。
Makefile Level 6 -- 更高级更简便
# level 6
CXX = g++
TARGET = main
SRC = $(wildcard *.cpp)
OBJ = $(patsubst %.cpp, %.o, $(SRC))
HEAD = $(wildcard *.h)
CXXARGS = -c -Wall
$(TARGET): $(OBJ) $(HEAD)
$(CXX) -o $@ $^
%.o: %.cpp
$(CXX) $(CXXARGS) $< -o $@
.PHONY: clean
clean:
rm -f *.o $(TARGET)
这里用到了Makefile的内置函数吧。$(函数名 参数1, 参数2)
这样的格式
$(wildcard *.cpp)
这是取当前文件夹(不包括子文件夹)所有的匹配的文件名,自动添加赋值,方便了很多
$(patsubst %.cpp, %.o, $(SRC))
这是个路径替换,规则是把cpp替换为o,替换的文本为SRC。
至此,Makefile入门就此结束,收货很多,很有用。
附录
文件下载
main.cpp
#include "head.h"
int main()
use_function_A();
use_function_B();
return 0;
A.cpp
#include <iostream>
#include "head.h"
using namespace std;
void use_function_A()
cout << "used function A!" << endl;
B.cpp
#include <iostream>
#include "head.h"
using namespace std;
void use_function_B()
cout << "used function B!" << endl;
head.h
#ifndef HEAD
#define HEAD
void use_function_A();
void use_function_B();
#endif
以上是关于Makefile入门的主要内容,如果未能解决你的问题,请参考以下文章