在一个大型工程,其源文件可是不计其数的,而且各个源文件还存在复杂的依赖关系,使得手动编译成为难题。同时,为了提高编译效率(未修改的源文件不再重新编译),一个自动化编译脚本的必要性就更加显现出来。在Windows系统下的Visual Studio,在编译时也需要产生一个编译脚本(VS称为nmake),而在Linux开发中,如果不使用集成开发环境开发的话,我们就需要自己编写make文件。
多文件编译概述
对于C/C++语言初学者来说,还没有接触过多文件编程。多文件编程即将不同代码放到不同文件里,通过包含头文件来建立彼此的联系。比如现在我们创立一个文件夹,名为“MakeTest”,文件夹内包含以下内容:

在这里,mian.cpp是主程序,sub.cpp是子程序。其实它们名字都可以随便取(即使是main.cpp)。我们在这三个文件里分别写入如下代码:
// main.cpp 源代码
#include <iostream>
#include "sub.hpp"
using namespace std;
int main() {
subFunction();
cout << "Finished!" << endl;
return 0;
}
// sub.hpp 源代码
#ifndef _sub_H
#define _sub_H
void subFunction( void );
#endif
// sub.cpp 源代码
#include <iostream>
#include "sub.hpp"
using namespace std;
void subFunction() {
cout << "Hello, world!" << endl;
return;
}
C/C++多文件编写
关于头文件:说在前面:
1)头文件不是各个C源文件硬性连接的桥梁,它只是一个列表,里面包含了一系列函数,对新手来说,最大的作用就是让IDE知道所有函数的列表,方便自动填充。编译器不会根据头文件的包含情况寻找要连接的文件。
2)头文件的另一个作用是头文件里的函数列表写成函数声明的形式,符合C语言先声明后使用的规范,编译器也会根据头文件执行一些操作,但这就不细讲了。你可以试试,学习完整个C/C++多文件编辑和编译后,把头文件去掉,将函数声明分别写入主文件和子文件,编译,也是可以达到指定效果的。
头文件写成#ifndef ... #define ... #endif的形式可以避免在复杂的依赖环境下,同一个头文件被包含多次的情况。(此时会报错:重复定义函数)
关于C源文件:
可以这么理解:
编译:每一个C源文件都将编译成一个独立的二进制代码。这些二进制代码内部包含一系列标签,称为函数入口。标签后面就是该函数的二进制代码。编译出来的代码一般还不可运行,还需要进一步处理。比如上面的sub.cpp就会被编译成sub.o,其中包含标签subFunction。
连接:连接就相当于在引用标签的地方,将标签所在程序的地址放进去。结果就是若干个.o文件的整合。比如上面的主程序,需要调用subFunction,就将subFunction的地址放到该处,系统执行的时候,就会跳转到subFunction的地址,执行子函数。
生成:通常连接和生成是在一起完成的,知道有这样一道工序就可以了。
C/C++多文件编译
如果此时我们像单文件编译那样执行:g++ main.cpp -o out,则会报错。因为gcc/g++如果没有接收到任何选项参数的话,默认将执行一整套编译操作(编译、连接、生成)。而单个的main.cpp或者sub.cpp是不完整的,这样编译肯定会出错。
比如现在main.cpp就用到了一个位于sub.cpp的函数,编译器找不到这个函数在哪里,就无法连接,因此编译错误。
我们应该这样编译:(-c表示仅编译,不执行连接等步骤)
g++ -c main.cpp # 仅编译main.cpp,会在当前目录生成main.o文件
g++ -c sub.cpp # 仅编译sub.cpp,会在当前目录生成sub.o文件
g++ main.o sub.o -o out # 连接两个文件,生成目标代码,即可执行文件out
运行可执行代码,就可以达到想要的效果了:

Makefile简单例子
简单地,使用上面简单的多文件程序,熟悉一下Makefile的基本用法。
在相同目录下编写Makefile文件:
Test4Makefile: main.o sub.o
g++ main.o sub.o -o Test4Makefile
main.o: main.cpp
g++ -c main.cpp
sub.o: sub.cpp
g++ -c sub.cpp
下面就来介绍一下Makefile的基本用法:
Makefile基本规则
目标: 依赖
<Tab> 系统命令
目标:生成的文件名
依赖:生成这个文件需要的文件
系统命令:能在终端运行的指令
注意:一定要用制表符Tab,不能使用空格或其他字符代替!
比如上面的Makefile流程:
首先发现:生成Test4Makefile需要main.o、sub.o文件
然后系统就寻找main.o、sub.o文件,发现没有找到
于是系统就找如何生成main.o、sub.o
系统找到:main.o需要main.cpp文件
于是系统又开始寻找main.cpp
系统找到main.cpp,于是就执行这下面的命令了:
g++ -c main.o
系统继续处理sub.o
需要sub.cpp,寻找sub.cpp
找到,执行:
g++ -c sub.cpp
返回
继续生成Test4Makefile,执行:g++ main.o sub.o -o Test4Makefile
Makefile完成
Makefile变量使用
使用abc=def定义变量,使用$(abc)使用变量:
target = Test4Makefile
obj = main.o sub.o
CC = gcc
$(target): $(obj)
$(CC) $(obj) -o $(target)
一般我们常常定义以下变量:
| 变量名称 | 值/一般值 | 说明 |
| target | 目标名称 | 目标名称 |
| obj | 连接的对象 | 连接的对象 |
| CC | gcc cc(默认) gcc-arm-none-eabi等 | 目标C语言编译器 |
| CFLAGS | 默认空 -std=c11 | 目标C语言编译器选项 |
| CXX | g++(默认) g++-arm-none-eabi等 | 目标C++编译器 |
| CXXFLAGS | 默认空 -std-c++11 -O2 | 目标C++编译器选项 |
| CPP | $(CC) -E(默认) gcc -E | C预处理器 |
| CPPFLAGS | 默认空 | C预处理器选项 |
| LD | ld(默认) | 目标链接器 |
| LDFLAGS | 默认空 | 目标链接器选项 |
| AS | as(默认) | 目标汇编器 |
| AR | ar(默认) | 静态库打包 |
| ARFLAGS | 默认空 | 静态库打包选项 |
| TARGET_ARCH | 默认无 | 用于交叉编译的目标架构 |
| RM | rm -f(默认) | 文件删除命令 |
还有其他的一些:
(摘自http://www.aiuxian.com/article/p-1299629.html)
LINK.o
把.o文件链接在一起的命令行,缺省值是$(CC) $(LDFLAGS) $(TARGET_ARCH)。
LINK.c
把.c文件链接在一起的命令行,缺省值是$(CC) $(CFLAGS) $(CPPFLAGS) $(LDFLAGS) $(TARGET_ARCH)。
LINK.cc
把.cc文件(C++源文件)链接在一起的命令行,缺省值是$(CXX) $(CXXFLAGS) $(CPPFLAGS) $(LDFLAGS) $(TARGET_ARCH)。
COMPILE.c
编译.c文件的命令行,缺省值是$(CC) $(CFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c。
COMPILE.cc
编译.cc文件的命令行,缺省值是$(CXX) $(CXXFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c。
以及一些交叉编译会用到的变量:HOSTCC、HOSTCXX等,表示在主机执行的编译动作。
Makefile通配符
% 模式自动匹配,相当于枚举遍历。
$<:规则中的第一个依赖$@:规则中的目标$^:规则中所有的依赖
比如可以这样写:
%.o: %.cpp
$(CXX) -c $< -o $@
这样就遍历了整个目录的所有.cpp文件,并进行编译。
还有其他的通配符,这些通配符和Shell的一致,这里就不多说啦。
Makefile伪目标
通常编译完项目的时候,我们通常会执行:make clean,将文件夹中的.o文件、编译好的可执行文件都被删除以节省空间。这是怎么实现的呢?参考以下代码:
.PHONY: clean
clean:
$(RM) $(obj) $(target)
这里clean写在了目标处,clean是一个伪目标。.PHONY声明了它是一个伪目标。
make clean的本质是:要求make目标为clean的部分。你也可以试试make main.o,这样只会对main.o目标进行操作。
对于上面的代码,make clean将清除所有obj文件和target文件。
还有,单独输入make命令时,默认自动构建Makefile里的第一个目标。所以应该把主要目标写在Makefile前面部分。
对于这个简单工程的Makefile示例
对于这个简单工程,这样就够了,以下是一个完整的Makefile文件:
target = Test4Makefile
obj = main.o sub.o
CXX = g++
$(target): $(obj)
$(CXX) $(obj) -o $(target)
%.o: %.c
$(CXX) -c $< -o $@
.PHONY: clean install
clean:
$(RM) $(obj) $(target)
# 常常还有安装目标:
install:
cp ./$(target) /usr/share/bin
进入这个源代码文件夹,执行make指令,然后就可以发现当前文件夹产生了目标文件-Test4Makefile。运行试试。然后执行make install(需要权限),Test4Makefile就安装到系统了。最后执行make clean,生成的所有文件将删除。你也可以多写一个uninstall目标,用于删除安装到系统的Test4Makefile文件。
看到这里,就可以捉去写基本的Makefile了。下面介绍一下Makefile的高级应用。更加具体的可以查看“Makefile手册整理”
Makefile高级应用
Makefile包含关系、嵌套执行
使用include来引用其他文件,如:
include ./compile-config.mk
在大型项目中,如果需要在Makefile中执行另外一个Makefile:
$(MAKE) -C /path/to/subproject
注意:当前Makefile的所有配置(如变量)将传递到下一级Makefile中。当然下一级Makefile可以覆盖这些配置。
关于传递配置,还有很大学问。大家可以到文章末尾看看另外几篇文章的详细描述。
命令错误忽略
make在执行过程中如果遇到错误,就会立即停止该处的执行,这样可能会令整个make终止。
为了避免一些不必要的错误发生,可以在命令前面加一个“-”。
-mkdir already/exists
-include file/not/exist
不过要注意这命令一定要用在正确的地方,如果把致命错误屏蔽了,那么这个构建肯定是失败的。
添加输出信息
在执行的时候,make将输出整条命令,方便开发人员调试:

我们可以使用以下命令输出自定义调试信息:
@echo 正在编译main.cpp
注意:“@”表示输出执行的命令的输出。如果不用“@”,将显示:
echo 正在编译main.cpp
“@”很有用,尤其对于一些会显示执行进度的命令,如wget。
条件结构
ifeq判断是否相等,ifneq判断是否不相等
$(a) = aaa
$(b) = bbb
ifeq($(a), $(b))
@echo a is equal to b
else
@echo a is not equan to b
endif
还有ifdef判断是否定义某个变量(变量值为空),ifndef判断是否没有定义某个变量(变量值非空),语法类似。
当然少不了的,直接使用if也可:
if($(a) == $(b))
# do something
else
# Leave the rest to us
循环结构
foreach类似于枚举循环。用法:
names := main sub
obj := $(foreach name_in_list, $(names), $(n).o)
这是将names列表里面的所有单词都加了一个.o,并返回添加给obj变量。
创建函数和函数调用(也叫命令包)
Makefile中,使用如下结构定义“函数”:
define delete_something
$(RM) /path/to/one/dir
$(RM) /path/to/another/dir
endef
接下来我们来调用这个函数。Makefile使用如下结构调用函数:
$(delete_something)
如果需要使用参数,可以定义含参数的函数:
define print_something
@echo $(1)
@echo $(2)
endef
使用call命令调用这个函数:
$(call print_something, foo, bar)


一些内置函数
# 取文件夹
$(dir /path/to/dir/file.cpp) # 返回 /path/to/dir/
# 取文件名
$(notdir /path/to/dir/file.cpp) # 返回 file.cpp
# 取文件后缀
$(suffix /path/to/dir/file.cpp) # 返回 .cpp
# 取文件前缀
$(basename /path/to/dir/file.cpp) # 返回 file
Makefile隐含规则
参见参考资料
make命令的参数
使用-f或--file指定具体的Makefile文件。默认为Makefile或者makefile。
make -f myMakefileName
make命令后面可跟随要make的目标,如:
make all # 编译所有的目标,包括伪目标
make clean # 清除
make main.cpp
make install
补充说明
Makefile变量关系
Makefile中的变量赋值,总为引用关系。
$(foo) = $(bar)
$(bar) = $(baz)
$(baz) = is that ok
# 此时 foo = bar = baz = is that ok
$(baz) = how about now
# 此时 foo = bar = baz = how about now
不过使用另外一种赋值运算符(:=)可以避免这种情况发生:
$(foo) = abc
$(bar) := $(foo) def
# 此时 $(bar) = abc def
$(foo) = def
# 此时 $(bar) = abc def 不变
总结
Makefile与Shell结合,功能强大。这里所列出的Makefile功能远远不是完整的,但也能帮助你完成一定程度的Makefile文件了吧。最后,附上一些参考链接。
参考链接
GNU make 官方手册:
https://www.gnu.org/software/make/manual/make.html
linux基础-makefile | 易学教程
https://www.e-learn.cn/content/linux/1221180
(介绍了较常用的指令)
Makefile教程(绝对经典,所有问题看这一篇足够了) – weixin_38391755的博客 – CSDN博客
https://blog.csdn.net/weixin_38391755/article/details/80380786
(比较详细)
Linux下编写 makefile 详细教程 – 知识天地 – 博客园
https://www.cnblogs.com/mfryf/p/3305778.html
(比较详细,分类清晰)