
Makefile 多文件编程一、 基本概念make是一个命令可执行程序通常位于 /usr/bin/。它的核心功能是解析 Makefile 文件并根据文件中描述的规则自动执行编译、链接等命令。Makefile是一个文本文件通常命名为 makefile 或 Makefile其中描述了项目的编译规则和依赖关系。程序员需要自己编写此文件来定义如何构建build整个软件项目。二、 为什么要使用 Makefile简化操作将复杂的编译命令如 gcc -c main.c -o main.o -I./include写入 Makefile。之后只需输入一个 make 命令即可完成整个构建过程无需记忆和输入长串命令。提高效率智能编译这是 Makefile 最核心的优势。make 工具通过文件的时间戳来判断文件是否被修改。◦ 只重新编译必要的文件如果某个源文件如 module1.c被修改了make 只会重新编译这个源文件及其依赖它的所有目标然后重新链接最终程序。未被修改的源文件则不会重新编译从而极大地节省了编译时间尤其对于大型项目。◦ 补充知识make 通过比较目标文件如 main.o和其依赖文件如 main.c, main.h的最后修改时间来实现。如果依赖文件比目标文件“更新”即时间戳更晚则认为目标已“过时”需要重新生成。三、 Makefile 核心语法规则基本规则格式是 Makefile 的基石目标: 依赖文件列表命令列表• 目标通常是要生成的文件名如可执行文件 main 目标文件 main.o。也可以是一个“动作名”称为假想目标如 clean。• 依赖文件列表生成“目标”所需要的输入文件列表。可以是一个或多个文件用空格分隔。例如main.o 依赖于 main.c 和 main.h。• 命令列表为了从“依赖文件”生成“目标”所需要执行的一系列 shell 命令。每行命令前必须有一个真正的 Tab 字符不能是空格。示例main:main.o module.o gcc main.o module.o-o main main.o:main.c main.h gcc-c main.c-o main.o module.o:module.c module.h gcc-c module.c-o module.o四、 make 命令的执行流程在当前目录下查找名为 GNUmakefile, makefile 或 Makefile 的文件。找到 Makefile 后make 会首先处理第一个目标默认目标除非用户通过 make [target] 指定了其他目标。为了构建最终目标如 mainmake 会递归地分析其依赖关系树。对于每一个规则make 检查“目标”是否存在或其“依赖文件”的时间戳是否比“目标”更新。如果目标不存在或已过时则执行该规则下的命令列表来更新目标否则跳过该规则。这个过程是自底向上的先确保所有底层的依赖.o 文件都是最新的然后再用它们来生成上层的目标最终的可执行文件。五、 Makefile 中的变量使用变量可以让 Makefile 更通用、更易于维护。文档中提到了三类变量常用的是自定义变量和自动变量。自定义变量◦ 定义变量名 变量值 例如 CC gcc。◦ 引用$(变量名) 或 ${变量名} 例如 $(CC) main.c -o main。◦ 补充知识▪ 赋值方式除了 递归展开还有 :简单展开、?条件赋值和 追加赋值。: 可以避免无限递归和提升性能。▪ override 指令如果命令行传入同名变量如 make CCclang会覆盖 Makefile 内的定义。使用 override CC gcc 可以防止被覆盖。自动变量预定义变量这类变量在规则命令中自动赋值非常实用是编写简洁通用规则的关键。变量 含义$ 当前规则中的目标文件名$ 当前规则中的第一个依赖文件名$^ 当前规则中所有不重复的依赖文件列表$? 比目标文件更新的所有依赖文件列表$* 不包含扩展名的目标文件名示例使用自动变量重写规则main:main.o module.o gcc $^-o $ # 等价于 gcc main.o module.o-o main%.o:%.c gcc-c $-o $ # 通配符规则为每个.c文件生成.o系统预定义变量部分这些是 make 内建的一些常用工具和标志的默认值可以直接使用或覆盖。变量 默认值 含义CC cc C 编译器CXX g C 编译器CFLAGS 无 C 编译器选项CXXFLAGS 无 C 编译器选项RM rm -f 删除命令补充知识CFLAGS/CXXFLAGS 常用值包括 -O2优化等级、-Wall显示所有警告、-g生成调试信息、-I include_dir指定头文件路径等。例如CFLAGS -Wall -O2 -I./include。预定义变量自动变量六、 假想目标假想目标不代表一个实际要生成的文件而是一个要执行的动作标签。• 常用作 clean清理、all构建所有目标、install安装等。• 为了避免与同名文件冲突应使用 .PHONY 声明其为假想目标。示例.PHONY:clean all all:main clean:$(RM)*.o main执行清理时需显式指定目标make clean。七、 文档示例代码整理与扩展文档中有一个稍复杂的例子涉及 main.c、printf1.c 和头文件。其代码整理如下main.c#includestdio.h#includemain.h#includeprintf1.hintmain(void){printf(hello make world\n);printf(PI%lf\n,PI);printf1();return0;}printf1.c#includestdio.h#includemain.hvoidprintf1(void){printf(hello printf1 world PI%lf\n,PI);}头文件// main.h#definePI3.1415926externvoidprintf1();// printf1.h (可能只是声明了printf1函数内容略)对应的 Makefile 演进示例补充文档中的练习第一步不使用变量基础版main:main.o printf1.o gcc main.o printf1.o-o main main.o:main.c main.h printf1.h gcc-c main.c-o main.o printf1.o:printf1.c main.h gcc-c printf1.c-o printf1.o clean:rm-f*.o main第二步使用自定义变量更通用CCgcc TARGETmain OBJSmain.o printf1.o $(TARGET):$(OBJS)$(CC)$(OBJS)-o $(TARGET)main.o:main.c main.h printf1.h $(CC)-c main.c-o main.o printf1.o:printf1.c main.h $(CC)-c printf1.c-o printf1.o clean:rm-f $(OBJS)$(TARGET)第三步使用自动变量和预定义变量更简洁通用CCgcc CFLAGS-Wall-I.# 补充了常用编译选项 TARGETmain OBJSmain.o printf1.o.PHONY:all clean all:$(TARGET)$(TARGET):$(OBJS)$(CC)$^-o $ # 使用 $^和 $%.o:%.c $(CC)$(CFLAGS)-c $-o $ # 使用模式规则和 $,$ clean:$(RM)$(OBJS)$(TARGET)这个版本使用了模式规则 %.o: %.c可以为所有 .c 文件自动推导出构建 .o 文件的规则是最为简洁和通用的写法。八、make 解释执行 Makefile 的完整流程例main:main.o module1.o module2.o gcc main.o module1.o module2.o-o main main.o:main.c head1.h head2.h com_head.h gcc-c main.c-o main.o module1.o:module1.c head1.h gcc-c module1.c-o module1.o module2.o:module2.c head2.h gcc-c module2.c-o module2.o当在终端执行 make 或 make main 命令时会发生以下过程第一步定位 Makefilemake 首先在当前目录下按名称查找 GNUmakefile、makefile 或 Makefile 文件。找到后将其载入并开始解析。第二步确定最终目标并开始构建如果没有在命令行指定目标如只输入 make则 make 默认选择 Makefile 中定义的第一个目标 作为最终要构建的目标。在本例中第一个目标是 main。make 开始尝试构建 main。构建的核心逻辑是递归的依赖分析和时间戳比较。第三步递归依赖分析与时间戳驱动执行这是最核心的部分。make 会为每一个目标如 main main.o执行以下判断判断逻辑如果 目标文件不存在 或者 任何一个依赖文件的时间戳比目标文件“更新”即依赖文件的最后修改时间晚于目标文件则认为该目标“过时”需要执行其下方的命令来重新生成它。否则目标就是“最新的”其命令将被跳过。让我们跟随 make 的视角走一遍处理目标 main◦ 目标main最终的可执行文件◦ 依赖main.o, module1.o, module2.o◦ 判断make 检查 main 文件是否存在以及它是否比它的三个 .o 依赖文件旧。由于我们是第一次编译main 文件肯定不存在。◦ 动作因为 main 是“过时”的所以它需要被重新生成。但生成 main 的命令 gcc main.o module1.o module2.o -o main 需要这三个 .o 文件作为输入。因此make 会暂停处理 main 的规则转而先去确保它的所有依赖main.o, module1.o, module2.o都是最新的。 这就是递归的开始。处理目标 main.o◦ 目标main.o◦ 依赖main.c, head1.h, head2.h, com_head.h◦ 判断检查 main.o 是否存在以及它是否比 main.c 或任何一个头文件旧。第一次编译时main.o 不存在。◦ 动作main.o 过时需要执行其命令 gcc -c main.c -o main.o 来生成。但执行前make 也会检查 main.o 的依赖.c 和 .h 文件是否需要更新吗不会。 因为 .c 和 .h 文件是源文件make 认为它们是“终极”依赖不会再去为它们寻找规则除非你定义了生成 .c 文件的规则。所以make 直接执行命令生成或更新main.o 文件。处理目标 module1.o 和 module2.o◦ 同理make 会接着处理 module1.o 和 module2.o 这两个目标检查它们相对于 module1.c/head1.h 和 module2.c/head2.h 是否过时并在需要时执行相应的 gcc -c 命令来生成它们。回溯与最终链接◦ 当 main 的所有依赖main.o, module1.o, module2.o都确保是最新状态后make 的控制流程回溯到最初的目标 main。◦ 此时生成 main 所需的所有材料.o 文件都已就绪。make 便会执行 main 规则下的链接命令gcc main.o module1.o module2.o -o main最终生成可执行文件 main。“智能编译”示例假设我们之后只修改了 module1.c 文件然后再次运行 makemake 依然先看目标 main。main 文件存在但它的依赖项 module1.o 可能过时了因为其源文件 module1.c 被修改了。为了确认make 需要先检查 module1.o。make 检查 module1.o发现 module1.c 的时间戳比 module1.o 新因此判定 module1.o 过时。make 执行 module1.o 对应的命令重新编译 module1.c生成新的 module1.o。make 回溯到 main。现在module1.o 的时间戳变得比 main 新了因此 main 也被判定为过时。make 执行 main 的链接命令将新生成的 module1.o 和未变动的 main.o、module2.o 重新链接生成新的 main 可执行文件。关键在于main.o 和 module2.o 因为其依赖文件没有变化在整个过程中没有被重新编译节省了编译时间。这正是 Makefile 和 make 工具提升开发效率的核心所在。