一、Makefile命令规则
1
2
Makefile的命令规则如下:
目标:依赖

假设有一个Test.cpp文件,代码如下:

1
2
3
4
5
6
7
8
#include <iostream>

using namespace std;

int main() {
count << "===Test===" << endl;
return 0;
}

我们可以使用gcc或g++来 预处理汇编编译链接,也可以利用Makefile来执行这些命令。

在Makefile中填入配置:

1
2
3
# 生成预处理文件
Test.i:Test.cpp
g++ -E Test.cpp -o Test.i

Test.i 是目标,Test.cpp 是依赖,整体的意思是:Test.i是依赖于Test.cpp生成的。
左右两边用冒号隔开,也就是Makefile命令规范:

1
目标:依赖

下一行是 g++ 命令,Test.i的生成是根据这条 g++ 命令生成的。g++前面必须含有分割符,而且必须是tab分割,不能是空格,否则在执行Makefile时不被识别。

执行该Makefile文件:

image-20230717161647472

Makefile中可能由多个目标组成,make命令会找到Makefile中第一个目标执行。也可以指定一个目标执行:

1
make 目标

现在Makefile中只有一个目标,所以可以执行:

1
make Test.i
二、Makefile实现 预处理汇编编译链接
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 生成预处理文件
Test.i:Test.cpp
g++ -E Test.cpp -o Test.i

# 生成汇编文件
Test.s:Test.i
g++ -S Test.i -o Test.s

# 生成目标文件
Test.o:Test.s
g++ -c Test.s -o Test.o

# 生成链接文件(可执行文件)
Test:Test.o
g++ Test.o -o Test。

定义四个目标,分别是:Test.iTest.sTest.oTest

分别执行这四个目标:

1
2
3
4
make Test.i
make Test.s
make Test.o
make Test

可以完成 预处理汇编编译链接
但是,完成这写指令,还可以一步完成,我们的目标是执行 make 命令完成所有的步骤。

只需要改变一下Makefile中的目标顺序即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 生成链接文件(可执行文件)
Test:Test.o
g++ Test.o -o Test

# 生成目标文件
Test.o:Test.s
g++ -c Test.s -o Test.o

# 生成汇编文件
Test.s:Test.i
g++ -S Test.i -o Test.s

# 生成预处理文件
Test.i:Test.cpp
g++ -E Test.cpp -o Test.i

输入make命令一次性执行完所有的目标:

image-20230717161803969

注意:顺序要正确,必须形成逐步依赖关系。

三、makefile编译多个文件

Student.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#ifndef _STUDENT_H_
#define _STUDENT_H_

#include <string>

using namespace std;

class Student {

private:
string name;
int age;

public:
void setName(string name);
string getName();
void setAge(int age);
int getAge();
};

#endif

Student.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include "Student.h"

void Student::setName(string name) {
this->name = name;
}
string Student::getName() {
return name;
}
void Student::setAge(int age) {
this->age = age;
}
int Student::getAge() {
return age;
}

Test.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
#include "Student.h"

using namespace std;

int main() {

Student* stu = new Student();
stu->setName("zhangsan");
stu->setAge(13);
cout << "姓名:" << stu->getName() << ", 年龄:" << stu->getAge() << endl;
return 0;
}

Makefile:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 生成链接文件(可执行文件)
Test:Test.o Student.o
g++ Test.o Student.o -o Test

# 生成目标文件Test.o
Test.o:Test.s
g++ -c Test.s -o Test.o

# 生成目标文件Student.o
Student.o:Student.s
g++ -c Student.s -o Student.o

# 生成汇编文件Test.s
Test.s:Test.i
g++ -S Test.i -o Test.s

# 生成汇编文件Test.s
Student.s:Student.i
g++ -S Student.i -o Student.s

# 生成预处理文件Test.i
Test.i:Test.cpp
g++ -E Test.cpp -o Test.i

# 生成预处理文件Student.i
Student.i:Student.cpp
g++ -E Student.cpp -o Student.i

执行make命令之后,生成 预处理文件汇编文件目标文件可执行文件

img

四、makefile多文件管理

在编译期间,在同一个目录下生成多个文件,文件多了有点乱,这时,就需要分类管理,将不同格式的文件放入不同的文件夹。

修改Makefile配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# 定义变量INFO_DIR,指定预处理文件和汇编文件的存放目录
INFO_DIR = ./info

# 定义变量INC_DIR,指定头文件的存放目录
INC_DIR = ./include

# 定义变量SRC_DIR,指定c/c++文件的存放目录
SRC_DIR = ./src

# 定义变量BIN_DIR,指定可执行文件的存放目录
BIN_DIR = ./bin

# 定义变量OBJ_DIR,指定可执行文件的存放目录
OBJ_DIR = ./obj

# 生成链接文件(可执行文件)
Test:${OBJ_DIR}/Test.o ${OBJ_DIR}/Student.o
g++ ${OBJ_DIR}/Test.o ${OBJ_DIR}/Student.o -o ${BIN_DIR}/Test

# 生成目标文件Test.o
${OBJ_DIR}/Test.o:${INFO_DIR}/Test.s
g++ -c ${INFO_DIR}/Test.s -o ${OBJ_DIR}/Test.o

# 生成目标文件Student.o
${OBJ_DIR}/Student.o:${INFO_DIR}/Student.s
g++ -c ${INFO_DIR}/Student.s -o ${OBJ_DIR}/Student.o

# 生成汇编文件Test.s
${INFO_DIR}/Test.s:${INFO_DIR}/Test.i
g++ -S ${INFO_DIR}/Test.i -o ${INFO_DIR}/Test.s

# 生成汇编文件Test.s
${INFO_DIR}/Student.s:${INFO_DIR}/Student.i
g++ -S ${INFO_DIR}/Student.i -o ${INFO_DIR}/Student.s

# 生成预处理文件Test.i
${INFO_DIR}/Test.i:${SRC_DIR}/Test.cpp
g++ -E ${SRC_DIR}/Test.cpp -o ${INFO_DIR}/Test.i

# 生成预处理文件Student.i
${INFO_DIR}/Student.i:${SRC_DIR}/Student.cpp
g++ -E ${SRC_DIR}/Student.cpp -o ${INFO_DIR}/Student.i

目录结构如下:

img

1
2
3
4
5
6
bin、include、info、obj、src文件夹需要手动创建;
bin文件夹存放可执行文件;
include文件夹存放头文件;
info文件夹存放预处理文件和汇编文件;
obj文件夹存放目标文件;
src文件夹存放c或cpp文件;
五、clean命令

在Makefile中添加:

1
2
clean:
del bin obj info

执行clean命令,可以删除 bin、obj、info文件夹中所有的文件。

1
make clean
六、Makefile通配符
1
2
3
4
5
6
7
%.o:表示一个xx.o文件
$@:表示目标文件
$<:表示第一个依赖文件
$^:所有不重复的依赖文件,以空格分开
$*:不包含扩展名的target文件名称
$+:所有的依赖文件,以空格分开,并以出现的先后为序,可能包含重复的依赖文件
$?:所有时间戳比target文件晚的依赖文件,并以空格分开

由于 -o 后面输出的必然是目标文件,所以 -o 后面的输出文件可以替换为 $@:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# 定义变量INFO_DIR,指定预处理文件和汇编文件的存放目录
INFO_DIR = ./info

# 定义变量INC_DIR,指定头文件的存放目录
INC_DIR = ./include

# 定义变量SRC_DIR,指定c/c++文件的存放目录
SRC_DIR = ./src

# 定义变量BIN_DIR,指定可执行文件的存放目录
BIN_DIR = ./bin

# 定义变量OBJ_DIR,指定可执行文件的存放目录
OBJ_DIR = ./obj

# 生成链接文件(可执行文件)
${BIN_DIR}/Test:${OBJ_DIR}/Test.o ${OBJ_DIR}/Student.o
g++ ${OBJ_DIR}/Test.o ${OBJ_DIR}/Student.o -o $@

# 生成目标文件Test.o
${OBJ_DIR}/Test.o:${INFO_DIR}/Test.s
g++ -c ${INFO_DIR}/Test.s -o $@

# 生成目标文件Student.o
${OBJ_DIR}/Student.o:${INFO_DIR}/Student.s
g++ -c ${INFO_DIR}/Student.s -o $@

# 生成汇编文件Test.s
${INFO_DIR}/Test.s:${INFO_DIR}/Test.i
g++ -S ${INFO_DIR}/Test.i -o $@

# 生成汇编文件Test.s
${INFO_DIR}/Student.s:${INFO_DIR}/Student.i
g++ -S ${INFO_DIR}/Student.i -o $@

# 生成预处理文件Test.i
${INFO_DIR}/Test.i:${SRC_DIR}/Test.cpp
g++ -E ${SRC_DIR}/Test.cpp -o $@

# 生成预处理文件Student.i
${INFO_DIR}/Student.i:${SRC_DIR}/Student.cpp
g++ -E ${SRC_DIR}/Student.cpp -o $@

clean:
del bin obj info
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#$<、$^ 和 % 结合使用,可以最大程度上简化配置:
# 定义变量INFO_DIR,指定预处理文件和汇编文件的存放目录
INFO_DIR = ./info

# 定义变量INC_DIR,指定头文件的存放目录
INC_DIR = ./include

# 定义变量SRC_DIR,指定c/c++文件的存放目录
SRC_DIR = ./src

# 定义变量BIN_DIR,指定可执行文件的存放目录
BIN_DIR = ./bin

# 定义变量OBJ_DIR,指定可执行文件的存放目录
OBJ_DIR = ./obj

# 生成链接文件(可执行文件)
${BIN_DIR}/Test:${OBJ_DIR}/Test.o ${OBJ_DIR}/Student.o
g++ $^ -o $@

# 生成所有目标文件 %:任意字符通配符 $<:表示第一个依赖文件
${OBJ_DIR}/%.o:${INFO_DIR}/%.s
g++ -c $< -o $@

# 生成所有汇编文件 %:任意字符通配符 $<:表示第一个依赖文件
${INFO_DIR}/%.s:${INFO_DIR}/%.i
g++ -S $< -o $@

# 生成所有预处理文件 %:任意字符通配符 $<:表示第一个依赖文件
${INFO_DIR}/%.i:${SRC_DIR}/%.cpp
g++ -E $< -o $@

clean:
del bin obj info

以上配置显然已经简单了很多。

另外,头文件已经放在include目录,cpp文件已经放在了src目录,所以需要指定头文件位置,由两种方法可以指定:

【方法一】在cpp中修改头文件位置

1
#include "Student.h"

改成

1
#include "../include/Student.h"

【方法二】g++命令后面指定头文件路径

加入 -I $(INC_DIR) 导入头文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# 定义变量INFO_DIR,指定预处理文件和汇编文件的存放目录
INFO_DIR = ./info

# 定义变量INC_DIR,指定头文件的存放目录
INC_DIR = ./include

# 定义变量SRC_DIR,指定c/c++文件的存放目录
SRC_DIR = ./src

# 定义变量BIN_DIR,指定可执行文件的存放目录
BIN_DIR = ./bin

# 定义变量OBJ_DIR,指定可执行文件的存放目录
OBJ_DIR = ./obj

# -I 指定头文件
CXXFLAGS=-I $(INC_DIR)


# 生成链接文件(可执行文件)
${BIN_DIR}/Test:${OBJ_DIR}/Test.o ${OBJ_DIR}/Student.o
g++ $(CXXFLAGS) $^ -o $@

# 生成所有目标文件 %:任意字符通配符 $<:表示第一个依赖文件
${OBJ_DIR}/%.o:${INFO_DIR}/%.s
g++ $(CXXFLAGS) -c $< -o $@

# 生成所有汇编文件 %:任意字符通配符 $<:表示第一个依赖文件
${INFO_DIR}/%.s:${INFO_DIR}/%.i
g++ $(CXXFLAGS) -S $< -o $@

# 生成所有预处理文件 %:任意字符通配符 $<:表示第一个依赖文件
${INFO_DIR}/%.i:${SRC_DIR}/%.cpp
g++ $(CXXFLAGS) -E $< -o $@

clean:
del bin obj info

g++出现了多次,也可以使用变量代替,并且新增TAGERT和OBJS变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# 定义变量INFO_DIR,指定预处理文件和汇编文件的存放目录
INFO_DIR = ./info

# 定义变量INC_DIR,指定头文件的存放目录
INC_DIR = ./include

# 定义变量SRC_DIR,指定c/c++文件的存放目录
SRC_DIR = ./src

# 定义变量BIN_DIR,指定可执行文件的存放目录
BIN_DIR = ./bin

# 定义变量OBJ_DIR,指定可执行文件的存放目录
OBJ_DIR = ./obj

# -I 指定头文件
CXXFLAGS=-I $(INC_DIR)

# CC 指定编译器 gcc 、 g++
CC=g++

#最终生成的目标,
TAGERT=Test

# 目标文件
OBJS=${OBJ_DIR}/Test.o ${OBJ_DIR}/Student.o

# 生成链接文件(可执行文件)
${BIN_DIR}/${TAGERT}:${OBJS}
${CC} $(CXXFLAGS) $^ -o $@

# 生成所有目标文件 %:任意字符通配符 $<:表示第一个依赖文件
${OBJ_DIR}/%.o:${INFO_DIR}/%.s
${CC} $(CXXFLAGS) -c $< -o $@

# 生成所有汇编文件 %:任意字符通配符 $<:表示第一个依赖文件
${INFO_DIR}/%.s:${INFO_DIR}/%.i
${CC} $(CXXFLAGS) -S $< -o $@

# 生成所有预处理文件 %:任意字符通配符 $<:表示第一个依赖文件
${INFO_DIR}/%.i:${SRC_DIR}/%.cpp
${CC} $(CXXFLAGS) -E $< -o $@

clean:
del bin obj info

以上配置还存在一定的弊端,OBJS变量指定了目标文件,此时的目标文件是需要指定具体目标文件的。

七、wildcard函数查找

$(wildcard pattern):pattern定义了文件名的格式,wildcard取出其中存在的文件。

1
2
# wildcard查找当前目录下所有cpp文件
SRCS=$(wildcard $(SRC_DIR)/*.cpp)

此时 SRCS 就是 src目录下所有cpp格式的文件。

八、patsubst函数替换

$(patsubst pattern,replacement,$(var)):从var中将符合patern格式的内容,替换为replacement。

1
2
3
4
5
6
# wildcard查找当前目录下所有cpp文件
SRCS=$(wildcard $(SRC_DIR)/*.cpp)

# notdir 去除掉绝对路径,只保留名字
# patsubst 把字串 $(notdir $(SRCS)) 符合模式[%.cpp]的单词替换成[%.o]
OBJS=$(patsubst %.cpp,$(OBJ_DIR)/%.o,$(notdir $(SRCS)))

patsubst 可以解决指定明确目标文件的弊端,改进弊端之后的配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# 定义变量INFO_DIR,指定预处理文件和汇编文件的存放目录
INFO_DIR = ./info

# 定义变量INC_DIR,指定头文件的存放目录
INC_DIR = ./include

# 定义变量SRC_DIR,指定c/c++文件的存放目录
SRC_DIR = ./src

# 定义变量BIN_DIR,指定可执行文件的存放目录
BIN_DIR = ./bin

# 定义变量OBJ_DIR,指定可执行文件的存放目录
OBJ_DIR = ./obj

# -I 指定头文件
CXXFLAGS=-I $(INC_DIR)

# CC 指定编译器 gcc 、 g++
CC=g++

#最终生成的目标,
TAGERT=Test

# wildcard查找当前目录下所有cpp文件
SRCS=$(wildcard $(SRC_DIR)/*.cpp)

# notdir 去除掉绝对路径,只保留名字
# patsubst 把字串 $(notdir $(SRCS)) 符合模式[%.cpp]的单词替换成[%.o]
OBJS=$(patsubst %.cpp,$(OBJ_DIR)/%.o,$(notdir $(SRCS)))

# wildcard *.cpp 当前目录下所有c文件
SRCS=$(wildcard $(SRC_DIR)/*.cpp)

# 生成链接文件(可执行文件)
${BIN_DIR}/${TAGERT}:${OBJS}
${CC} $(CXXFLAGS) $^ -o $@

# 生成所有目标文件 %:任意字符通配符 $<:表示第一个依赖文件
${OBJ_DIR}/%.o:${INFO_DIR}/%.s
${CC} $(CXXFLAGS) -c $< -o $@

# 生成所有汇编文件 %:任意字符通配符 $<:表示第一个依赖文件
${INFO_DIR}/%.s:${INFO_DIR}/%.i
${CC} $(CXXFLAGS) -S $< -o $@

# 生成所有预处理文件 %:任意字符通配符 $<:表示第一个依赖文件
${INFO_DIR}/%.i:${SRC_DIR}/%.cpp
${CC} $(CXXFLAGS) -E $< -o $@

clean:
del bin obj info
九、Makefile的变量

Makefile的变量分为两种:及时变量延时变量

1
2
3
4
【1】 `:=`:即时变量,该变量的值即刻确定,在定义时就被确定了;
【2】`=`:延时变量,该变量的值,在使用时才确定;
【3】`?=`:延时变量,第一次定义才起效,如果前面被定义过了就忽略这句;
【4】`+=`:附加,它是即时变量还是延时变量取决于前面的定义;
十、函数遍历

$(foreach val,list,text):对于list(通常用空格隔开)里的每一个变量执行text操作

1
2
3
4
5
6
7
8
9
# 定义一个list
LIST=a b c

# 用f代表A中的各个变量,执行第三个参数的操作。
# foreach 遍历关键字,f:LIST中的每个变量,
RESULT=$(foreach f,$(LIST),$(f).result)

all:
@echo RESULT=$(RESULT)

输出结果:

1
RESULT=a.result b.result c.result
十一、filter函数过滤

$(filter pattern...,text):在text里面取出符合pattern格式的值

$(filter-out pattern...,text):在text里面取出不符合pattern格式的值

1
2
3
4
5
6
7
8
9
10
11
12
# 定义变量C 
C= a b c d/

# 在变量C中取出符合%/的值
D=$(filter %/,$(C))

# 在变量C中取出不符合%/的值
E=$(filter-out %/,$(C))

all:
@echo D=$(D)
@echo E=$(E)

输出结果:

1
2
D=d/
E=a b c
十二、CXXFLAGS补充

上面已经定义了 CXXFLAGS 变量:

1
CXXFLAGS=-I $(INC_DIR)

我们还可以对它进行补充,比如:

1
2
# -I 指定头文件 
CXXFLAGS=-I $(INC_DIR) -Wall -O2 -std=c++11 -frtti -fexceptions

可以指定的FLAG有:

1
2
3
4
5
6
7
8
9
【1】-Werror:会把所有警告当成错误
【2】-I: 该选项用于指定编译程序时依赖的头文件路径
【3】-On: 这是一个优化选项,如果在编译时指定该选项,则编译器会根据n的值(n取0到3之间)
对代码进行不同程度的优化,其中-O0 表示不优化,n的值越大,优化程度越高
【4】-L: 库文件依赖选项,该选项用于指定编译的源程序依赖的库文件路径,库文件可以是静态链接库,也可以是动态链接库
【5】-Wall: 允许发出gcc能提供的所有有用的警告,也可以用-W(warning)来标记指定的警告
【6】-std=c++11:C++11标准
【7】-frtti 和 -fexceptions:关闭 exceptions、rtti 可以减小程序的占用的空间和提升程序的运行效率,
关闭后可能带来一些不兼容问题,使用 typeid 运算符必须开启 RTTI

原文:NDK<第五篇>:Makefile(windows环境) - 简书 (jianshu.com)