Linux项目编程之Makefile

在一个大型工程,其源文件可是不计其数的,而且各个源文件还存在复杂的依赖关系,使得手动编译成为难题。同时,为了提高编译效率(未修改的源文件不再重新编译),一个自动化编译脚本的必要性就更加显现出来。在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.osub.o文件
然后系统就寻找main.osub.o文件,发现没有找到
于是系统就找如何生成main.osub.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连接的对象连接的对象
CCgcc
cc(默认)
gcc-arm-none-eabi等
目标C语言编译器
CFLAGS默认空
-std=c11
目标C语言编译器选项
CXXg++(默认)
g++-arm-none-eabi等
目标C++编译器
CXXFLAGS默认空
-std-c++11 -O2
目标C++编译器选项
CPP$(CC) -E(默认)
gcc -E
C预处理器
CPPFLAGS默认空C预处理器选项
LDld(默认)目标链接器
LDFLAGS默认空目标链接器选项
ASas(默认)目标汇编器
ARar(默认)静态库打包
ARFLAGS默认空静态库打包选项
TARGET_ARCH默认无用于交叉编译的目标架构
RMrm -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
(比较详细,分类清晰)

发表评论