(此OOP笔记系列并不会系统地记录知识点。只记录个人认为散乱,边角或者容易遗忘、需要注释的。)
本篇主要内容:一个Makefile
模板;对目标、伪目标的理解。
1 编译
有关编译链接的全过程参见:编译和链接的过程
为防止丢失粘上关键的流程图:
从源代码生成目标文件:
g++ -std=c++14 -O2 main.cpp -c -o main.o
-std=c++14
指定了C++版本,-O2
开启O2优化,-c
表示执行完汇编就停止不进行链接。-o
指定输出文件的名称。最后生成了main.o
。
2 链接
将编译好的目标文件(各个模块)连接为最终结果:
g++ -o main main.o
这样就生成了可执行文件main
3 Makefile
详解参见:C++之makefile写法
3.1 例子
这里给出一个满足简单需求的通用Makefile:
####################################
# Learnt from the Internet
# Edited by Colin
# 2020.02
####################################
cc = g++
CXXFLAGS = -std=c++14 -O2 # 编译选项
prom = main # 目标文件(名称)
deps = $(shell find . -name "*.h") # dependences
src = $(shell find . -name "*.cpp") # sources
obj = $(src:%.cpp=%.o) # objects
$(prom): $(obj) # prom: target file, obj: 依赖关系表,这里是.o文件
$(cc) -o $(prom) $(obj)
%.o: %.cpp $(deps)
$(cc) $(CPPFLAGS) -c $< -o $@ # %<: 表示依赖目标, $@: 表示目标集合
.PHONY: clean # 指定clean为伪目标
clean:
rm -rf $(prom) $(obj)
在实际使用中,上述代码中的$(shell find . -name "*.h")
会查找当前目录下所有的.h
文件,如果有子目录会递归地往下找。而有时候我们不希望它递归下去,或者递归到某一层就停止,因此可以指定查找深度。此外,除了CXXFLAGS这个编译选项,我们还可以添加一个FLAG参数,如果将它设置为-g
,就可以编译出用于调试的可执行文件。即在make时指定参数:make FLAG=-g
,这一操作可以用于配置VSCode的调试功能。
做出改动的代码如下:
####################################
# Learnt from Internet
# Edited by Colin
# 2020.02
####################################
cc = g++
FLAG =
CXXFLAGS = -O2 --std=c++17
prom = main
deps = $(shell find . -maxdepth 1 -name "*.h")
src = $(shell find . -maxdepth 1 -name "*.cpp")
obj = $(src:%.cpp=%.o)
$(prom): $(obj)
$(cc) -o $(prom) $(obj)
%.o: %.cpp $(deps)
$(cc) $(CXXFLAGS) $(FLAG) -c $< -o $@
.PHONY: clean
clean:
rm -rf $(prom) $(obj)
3.2 一些备注
3.2.1 宏(macro)
shell
中有和C++
中的#define
类似的宏替换。如上段代码中开头几行声明的,它们并不是“变量”,而是宏定义,这意味着机器会把它们的定义直接粘贴替换${var_name}
。因此在shell
中不妨试一试:
m = 1 + 1
echo $[$m * 4]
你会发现$m * 4
是1 + 1 * 4
,因此输出是5
,而不是8
。
但是,如果你echo $[m*4]
,输出就是8
。我还不清楚这是什么原因,欢迎留言。
用宏定义方便了编译器、编译参数的一次性修改。配合通配符方便了获取需要的文件。
3.2.2 .PHONY
让我们看看 Colin Collins 对phony的解释:
ADJ If you describe something as phony, you disapprove of it because it is false rather than genuine. 假的 [非正式]
字面意思,.PHONY:
将其后面的目标指定为“伪目标”。至于其作用,我们首先需要明确以下几点。这几点是初学者最迷惑的几点,也是Makefile的核心内容,即“目标”(或者“伪目标”)、“依赖”,以及目标下一行的那段代码它们之间的关系。
- 正如模板中的
$(prom)
(宏)、%.o
(通配符)所代表的目标文件如main
、main.o
,clean
也是“目标”。 make
会且只会完成一个终极目标。这一终极目标是从前往后的第一个。而为了完成这一个终极目标(如main
),make
会依次检查终极目标后面列出的依赖项(如main.o
),如果依赖不存在则会一层一层向下找依赖的生成方法(main.o
目标)并执行,最终完成终极目标。对于简单的写法,如果我们想实现clean功能,我们直接在
Makefile
最后添加以下代码即可。clean: rm -rf $(prom) $(obj)
- 根据上述,目标
clean
既不是第一个目标(它被写在最后),也不是终极目标的依赖项。因此仅仅执行make
,clean
直接就被忽略了。所以需要执行make clean
,指定make
的目标为clean
才行。 - 之所以添加后执行
make clean
第二行的命令会被执行,是因为当前文件夹下不存在名字为clean
的文件,因此make
“想通过执行这行命令来生成clean
文件”(这是为了方便理解臆想出来的目的)。而因为这行命令本身又没有创建clean
文件,因此每次clean
都不存在,因而每次第二行命令都会被执行。因此我们是利用了make
以为clean
是个需要生成的文件这一特性来“变相”达到了目的。然而其实clean
并不是文件。
然后我们再来解释.PHONY
的作用。
clean
后面的列表是空的,也就是没有依赖文件。回顾
main.o: main.cpp
g++ -c main.cpp -o main.o
中,如果依赖main.cpp
在现有的main.o
创建之后没有发生过变化,那么第二行的命令将不会被执行。考虑特殊情况:文件夹下正好有个文件名为clean
,而Makefile
中,目标clean
没有依赖(因此不存在依赖被更新后目标需要被更新的情况),而它又存在(因为前面说了正好有个文件名为clean
)(所以就是“最终版本”),那么make
显然就不会“试图执行第二行命令去生成它”。于是我们的make clean
就失效了。(这段话可能比较难理解,因为clean
时而是Makefile
中的目标,时而是文件。)
而为了解决这一问题,.PHONY:
将其后面罗列的目标指定为“伪目标”。所谓伪目标,就是告诉make
,clean
就不是一个文件目标,而仅仅是个“假的目标”,它是它下面那段需要被执行的代码的一个“label”,无论如何它后面的命令都需要被执行。于是,即使文件夹下正好有个文件名为clean
,当你执行make clean
时,由于make
已经知道了目标clean
的目的不是去生成一个名叫clean
的文件,而是要去执行它所代表的下面那行命令,因此make
就会无条件地执行第二行的命令。
3.2.3 all
上面提到,Makefile
中有且仅有一个终极目标,而如果我们想在make
中一次性完成多个“终极目标”怎么办呢?这就需要all
。
all: main1 main2
main1: main1.cpp
g++ main1.cpp -o main1
main2: main2.cpp
g++ main2.cpp -o main2
如上,all
放在第一个作为“终极目标”,其依赖为main1
和main2
,这样在完成依赖项的构建时,main1
和main2
就都会产生。在这里,all
也不是一个“文件目标”,但是它也不是如clean
一样的“伪目标”。首先,我们不能给它前面加上.PHONY
,否则整个文件后面所有的目标都成了伪目标,但是main1
这样的目标可是“文件目标”。其次,它有依赖项,但是没有第二行命令。当第一次生成了main1
和main2
之后,如果你修改了相关源代码,但是没有删除main1
和main2
就执行make
,那显然什么都不会发生。因此如果改了生成main1
和main2
的源文件,你需要先删除main1
和main2
,再make
才可以得到新的main1
和main2
。
其它参考资料: