一、makefile入门
1、相关概念介绍
(1)一个工程中的源文件不计其数,其按类型、功能、模块分别放在若干个目录中,makefile定义了一系列的规则来指定哪些文件需要先编译、哪些文件需要后编译、哪些文件需要重新编译,甚至需要进行更复杂的功能操作。
(2)"make"是一个命令工具,它解释makefile中的指令,在Makefile文件中描述了整个工程所有文件的编译顺序、编译规则。

2、入门的第一个Makefile(makefile基础语法)
(1)makefile的基本语法:
<目标>:<依赖1> <依赖2>......
Tab @<命令1>
Tab @<命令2>
......
①目标:一般是指要编译的目标,也可以是一个动作,它由程序员自行命名。
②依赖:指执行当前目标所要依赖的先项(或者说前置条件),包括执行其它目标、某个具体文件或库等;一个目标可以有多个依赖,也可以没有依赖,如果没有则不写,如果有多个依赖则用空格分割。
③命令:该目标下需要执行的具体命令,可以没有,也可以有若干条,如果有多条命令,则每条命令写一行;命令前加"@"会抑制命令本身的打印(就是把命令的原文内容打印出来),一般不需要加。
(2)第一个Makefile示例:
bash
a:b c
echo "hello A_world"
ls ./ #列出当前目录下的文件
b:
echo "hello B_world"
c:
echo "hello C_world"
目标a依赖目标b和目标c,那么在执行目标a时,需要先执行目标b和目标c(目标b和目标c的执行顺序不固定),再执行目标a
3、make命令工具常用选项
(1)make命令工具的格式:
make -f filename other options target
(2)make默认在当前目录中按顺序依次寻找名为GUNmakefile、makefile、Makefile的文件作为输入文件(仅有一个输入文件,找到即进行编译,不是三个都读取一遍),如果不写参数(指定执行的目标),则make执行输入文件中的第一个目标,如果第一个目标有依赖,则会先执行其所依赖的目标,再执行它。
bash
make # 不写参数,自动读上面三个文件之一,然后执行第一个目标
make b # 手动指定目标b,只执行输入文件中的目标b
(3)常用选项(可叠加):
①"-f filename":make默认只寻找名为GNUmakefile、makefile、Makefile的文件,如果是自定义的脚本名字(如build.mk),则需要加上该参数。
bash
make -f build.mk
②"-n":空跑、预览指令,不会真实运行,多用于调试,检查Makefile中的指令是否写错。
bash
make -n
③"-s":只执行命令,但不显示具体命令,作用和"@"一样,只不过"@"是屏蔽单行指令打印。
bash
make -s
④"-C":指定Makefile所在的目录(如./src)。
bash
make -C ./src
⑤"-v":查看make工具的版本号。
bash
make -v
⑥"-w":多级工程跨目录编译时,打印执行前和执行后的所在路径。
bash
make -w
4、程序编译流程
(1)以gcc为例,gcc命令能够直接从源代码生成目标可执行文件,其格式如下所示。需要注意的是,gcc是C编译器,如果要编译C++源码,则需要加"-lstdc++"。
bash
gcc main.c
gcc -lstdc++ main.cpp
(2)一条gcc命令其实是分4步进行编译的:
①预处理:展开#include,替换#define宏,删除注释,如果是编译.c文件则输出.i文件,如果是编译.cpp文件则输出.ii文件。
bash
gcc -E main.c > main.i
gcc -E main.cpp > main.ii
②编译:经过语法分析、词法分析、语义分析和符号会中,把C/C++代码转换成汇编代码,结果保存在.s文件中。
bash
gcc -S main.i
gcc -S main.ii
③汇编:把汇编代码转换为二进制/机器指令,结果保存在.o文件中。
bash
gcc -c main.s
④链接:把多个.o文件合并(C++的链接还需要合并标准库libstdc++.so),以及符号表的重定位,生成可执行程序。
bash
gcc main.o
gcc -lstdc++ main.o
5、makefile中的变量
(1)自动系统变量:
①自动系统变量只在"<目标>:<依赖>"下的命令中生效,不用手动赋值,使用方法比如:
bash
app:main.c func.c test.c
$(CC) $^ -o $@
②自动系统变量的符号:
|-----|-------------------------|---------------------------------|
| 符号 | 释义 | 上例目标app中的自动系统变量含义 |
| @ | 当前目标的目标名 | @ = app |
| \< | 当前目标的第一个依赖 | < = main.c |
| \^ | 当前目标的所有依赖(空格分隔),重复的将去除 | ^ = main.c func.c test.c |
| + | 当前目标的所有依赖(空格分隔),重复的也不去除 | + = main.c func.c test.c |
| ? | 比目标新(时间戳比目标文件晚的依赖文件) | ?只列出最近修改过的源文件(更新时间比目标文件晚的依赖文件) |
| \* | 当前目标去掉后缀的名字 | 如果当前目标名为"main.o",则* = main |
(2)系统内置常量:
①make工具出厂自带全局变量,"make -p"可以查看全部系统内置常量及默认值。
②常用的系统内置常量:
|-----|-----------|-------|
| 常量名 | 释义 | 默认值 |
| AS | 汇编程序的名称 | as |
| CC | C编译器的名称 | cc |
| CPP | C预编译器的名称 | cc -E |
| CXX | C++编译器的名称 | g++ |
| RM | 文件删除程序名 | rm -f |
(3)自定义变量:
①自定义变量由程序员自行定义,可用于简化代码、方便统一修改编译参数(作用有点类似于C语言中的#define)。
②自定义变量定义格式(特别说明,尖括号不是语法的一部分):
<变量名> = <变量值>
③自定义变量调用格式(特别说明,尖括号不是语法的一部分):
$(<变量名>)
${<变量名>}
④自定义变量定义和使用举例:
bash
# 自定义变量
CC = gcc # 指定编译器
CFLAGS = -g -Wall # 编译参数:g调试、Wall告警
SRC = main.c func.c
OBJ = main.o func.o
TARGET = app
$(TARGET):$(OBJ)
$(CC) $(OBJ) -o $(TARGET) $(CFLAGS)
%.o:%.c
$(CC) -c $< -o $@ $(CFLAGS)
clean:
rm -rf $(OBJ) $(TARGET)
6、makefile的运行流程和伪目标
(1)make的时间戳原理(编译时间最小化):
比如在Linux中,每个文件都有一个"修改时间",即文件内容最后一次被改动的时间
make会将目标文件和其任意一个依赖文件的时间比对,如果依赖文件更新时间更晚,则需要重新编译;目标的依赖仍然是一个目标,它也要和自己所依赖的文件做更新时间比对,如果依赖文件更新时间更晚,则也需要重新编译,以此递归下去,直到目标没有依赖,则判断目标文件是否存在,若存在则不需要重新编译,否则需要生成目标文件

(2)伪目标:
①如果目录中有与makefile中目标同名的文件,比如"clean",而它正好没有任何依赖,那么"make clean"会判定clean文件已经是最新的文件,不会执行clean这个目标,为了避免这种情况,凡是只做动作、不生成与目标名同名文件的目标,都声明为伪目标,这样makefile就不会判断目标是否存在对应的文件以及是否需要更新。
②伪目标的声明方式:
.PHONY: <目标名>
③伪目标声明举例:
bash
.PHONY: clean
clean:
rm -rf *.o app
7、模式匹配
(1)模式匹配:
①"%"是通配符,可以代表任意一串文件名(作用类似于模糊搜索)。
②举例:
bash
.PHONY:clean show
OBJ= sub.o multi.o calc.o add.o
TARGET=calc
$(TARGET):$(OBJ)
$(CXX) $^ -o $@
%.o:%.cpp
$(CXX) -c $^ -o $@
clean:
$(RM) *.o $(TARGET)
遇到sub.o,自动匹配依赖sub.cpp;遇到add.o,自动匹配依赖add.cpp
再配合自动系统变量------\< = %.cpp、@ = %.o,可不用将目标和依赖关系逐个列出
(2)两个Make内置函数:
①wildcard函数:可以获取当前目录中所有指定后缀的文件。
bash
SRC := $(wildcard ./*.cpp)
# SRC = a.cpp b.cpp main.cpp 当前目录中有a.cpp、b.cpp、main.cpp
②patsubst函数:对被指定变量中的文件名做批量替换(被指定的变量不会改变,该函数是返回替换后的文件名),常用于替换后缀。
bash
SRC=$(wildcard ./*.cpp)
OBJ=$(patsubst %.cpp,%.o,$(SRC))
# SRC:a.cpp b.cpp → OBJ:a.o b.o 当前目录中有a.cpp、b.cpp
(4)使用以上两个内置函数,可以对模式匹配中的举例进行优化,定义变量OBJ时可以不用再一一列出当前目录中的文件名。
bash
.PHONY:clean show
OBJ=$(patsubst %.cpp,%.o,$(wildcard ./*.cpp))
TARGET=calc
$(TARGET):$(OBJ)
$(CXX) $^ -o $@
%.o:%.cpp
$(CXX) -c $^ -o $@
clean:
$(RM) *.o $(TARGET)