(此OOP笔记系列并不会系统地记录知识点。只记录个人认为散乱,边角或者容易遗忘、需要注释的。)

本篇主要内容:一个Makefile模板;对目标、伪目标的理解

1 编译

有关编译链接的全过程参见:编译和链接的过程

为防止丢失粘上关键的流程图:

流程.png

从源代码生成目标文件:

g++ -std=c++14 -O2 main.cpp -c -o main.o

-std=c++14指定了C++版本,-O2开启O2优化,-c表示执行完汇编就停止不进行链接。最后生成了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)

3.2 一些备注

3.2.1 宏(macro)

shell中有和C++中的#define类似的宏替换。如上段代码中开头几行声明的,它们并不是“变量”,而是宏定义,这意味着机器会把它们的定义直接粘贴替换${var_name}。因此在shell中不妨试一试:

m = 1 + 1
echo $[$m * 4]

你会发现$m * 41 + 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的核心内容,即“目标”(或者“伪目标”)、“依赖”,以及目标下一行的那段代码它们之间的关系。

  1. 正如模板中的$(prom)(宏)、%.o(通配符)所代表的目标文件如mainmain.oclean也是“目标”。
  2. make会且只会完成一个终极目标。这一终极目标是从前往后的第一个。而为了完成这一个终极目标(如main),make会依次检查终极目标后面列出的依赖项(如main.o),如果依赖不存在则会一层一层向下找依赖的生成方法(main.o目标)并执行,最终完成终极目标。
  3. 对于简单的写法,如果我们想实现clean功能,我们直接在Makefile最后添加以下代码即可。

    clean:
    rm -rf $(prom) $(obj)
  4. 根据上述,目标clean 既不是第一个目标(它被写在最后),也不是终极目标的依赖项。因此仅仅执行makeclean直接就被忽略了。所以需要执行make clean,指定make的目标为clean才行。
  5. 之所以添加后执行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: 将其后面罗列的目标指定为“伪目标”。所谓伪目标,就是告诉makeclean就不是一个文件目标,而仅仅是个“假的目标”,它是它下面那段需要被执行的代码的一个“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放在第一个作为“终极目标”,其依赖为main1main2,这样在完成依赖项的构建时,main1main2就都会产生。在这里,all也不是一个“文件目标”,但是它也不是如clean一样的“伪目标”。首先,我们不能给它前面加上.PHONY,否则整个文件后面所有的目标都成了伪目标,但是main1这样的目标可是“文件目标”。其次,它有依赖项,但是没有第二行命令。当第一次生成了main1main2之后,如果你修改了相关源代码,但是没有删除main1main2就执行make,那显然什么都不会发生。因此如果改了生成main1main2的源文件,你需要先删除main1main2,再make才可以得到新的main1main2


其它参考资料:

Makefile编译选项CC与CXX/CPPFLAGS、CFLAGS与CXXFLAGS/LDFLAGS

makefile中的.PHONY和all的作用

Makefile中.PHONY的作用

Last modification:March 8th, 2020 at 05:35 pm
如果觉得我的文章对你有用,请随意赞赏