在一个大型工程,其源文件可是不计其数的,而且各个源文件还存在复杂的依赖关系,使得手动编译成为难题。同时,为了提高编译效率(未修改的源文件不再重新编译),一个自动化编译脚本的必要性就更加显现出来。在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
(比较详细,分类清晰)