当前位置:首页 > 公众号精选 > 嵌入式案例Show
[导读]点击上方蓝字关注我哦~ 01 前言 今天抽空研究了下 Makefile,在这里整理一下各处搜到的资料,以备将来复习时快速上手,同时也帮助和我一样的初学者们节约时间。 02 准备工作 首先,假设我们有如下几个代码文件:main.cpp functions.h function1.cpp function

点击上方蓝字关注我哦~

01

前言


今天抽空研究了下 Makefile,在这里整理一下各处搜到的资料,以备将来复习时快速上手,同时也帮助和我一样的初学者们节约时间。

02

准备工作


首先,假设我们有如下几个代码文件:main.cpp functions.h function1.cpp function2.cpp

--- functions.h ---// functions.hvoid print_hello();int factorial(int n);--- function1.cpp ---// function1.cpp#include "functions.h"int factorial(int n){ if (n!=1) return n*factorial(n-1); else return 1;}--- function2.cpp ---//function2.cpp#include<iostream>#include "functions.h"void print_hello(){ std::cout << "hello world" << std::endl;}--- main.cpp ---//main.cpp# include<iostream># include "functions.h"
int main(){ print_hello(); std::cout << "this is main" << std::endl; std::cout << "The factorial of 5 is " << factorial(5) << std::endl; return 0;}

03

不用makefile如何编译

如果不用 makefile,则需要按照下面的方式编译上述代码:

g++ -c function1.cppg++ -c function2.cppg++ -c main.cppg++ -o hello main.o function1.o function2.o

其中,g++ -c function1.cpp 会将源码编译成名为 function1.o 对象文件。如果不想采用默认的命名,也可以自定义文件名,例如:

g++ -c function1.cpp -o fun1.o

也可以用一行命令整合编译、链接的步骤:

g++ -o hello main.cpp function1.cpp function2.cpp

这种方式有很多弊端,例如:

  1. 每次编译、链接都需要手动敲的很多命令。

  2. 当工程量很大时,编译整个工程需要花很久。而我们往往并不是每次都修改了所有源文件,因此希望程序自动编译那些被修改的源码,而没被修改的部分不要浪费时间重新编译。

为了解决上述第一个问题,我们可以把所有编译需要的命令保存到文件中,编译时一键执行。针对第二个问题,我们希望有一个软件,自动检测哪些源文件被修改过,然后自动把它们挑出来选择性地编译。而 make 命令通过检测代码文件的时间戳,决定是否编译它。

04

使用Makefile编译

第一版Makefile

首先需要确定 Makefile 的名字,需要设置成 Makefile 或者 makefile,而不能是其它版本(MakeFile, Make_file, makeFile,... )。其次,需要注意的是 Makefile 是缩进敏感的,在行首一定不能随便打空格。下面我们看一下第一版 Makefile。

Makefile (#号为注释)all: g++ -o hello main.cpp function1.cpp function2.cppclean: rm -rf *.o hello


(注意上面代码片段的缩进,是一个<tab>而不是4个或者8个空格。)

其中 all 、clean的术语为 target,我也可以随意指定一个名字,例如 abc,真正执行编译的是它下面缩进行的命令。我们可以看到,这个命令和我们在命令行中手动敲的没有任何区别。因此,通过这个简单的 Makefile,就可以省去了每次手动敲命令的痛苦:只需要在命令行敲下 make 回车,即可完成编译。

clean 表示清除编译结果,它下方就是普通的命令行删除文件命令。命令行输入 make 将默认执行第一个 target (即 all)下方的命令;如要执行清理操作,则需要输入 make clean,指定执行 clean 这个 target 下方的命令。

这个 Makefile 虽然可以省去敲命令的痛苦,却无法选择性编译源码。因为我们把所有源文件都一股脑塞进了一条命令,每次都要编译整个工程,很浪费时间。第二版 Makefile 将解决这个问题。

第二版Makefile

既然我们希望能够选择性地编译源文件,就不能像上一节那样把所有源文件放在一条命令里编译了,而是要分开写:

all: hellohello: main.o function1.o function2.o g++ main.o function1.o function2.o -o hellomain.o: main.cpp g++ -c main.cppfunction1.o: function1.cpp g++ -c function1.cppfunction2.o: function2.cpp g++ -c function2.cpp
clean: rm -rf *.o hello

上面的 Makefile 包含了一条重要的语法:<target>:<dependencies>。即,目标:目标依赖的文件。

顺着代码捋一下逻辑:

  1. 命令行输入 make ,将默认执行 all 这个 target;

  2. 而 all 这个 target 依赖于 hello,hello 在当前目录下并不存在,于是程序开始往下读取命令..……终于找到了 hello 这个 target;

  3. 正待执行 hello 这个 target 的时候,却发现它依赖于 main.o,function1.o,function2.o 这三个文件,而它们在当前目录下都不存在,于是程序继续向下执行;

  4. 遇到 main.o target,它依赖于 main.cpp。而 main.cpp 是当前目录下存在的文件,终于可以编译了,生成 main.o 对象文件。后面两个函数以此类推,都编译好之后,再回到 hello target,连接各种二进制文件,生成 hello 文件。

第一次编译的时候,命令行会输出:

g++ -c main.cppg++ -c function1.cppg++ -c function2.cppg++ main.o function1.o function2.o -o hello

证明所有的源码都被编译了一遍。假如我们对 main.cpp 做一点修改,再重新 make(重新 make 前不要 make clean),则命令行只会显示:

g++ -c main.cppg++ main.o function1.o function2.o -o hello

这样,我们就发挥出 Makefile 选择性编译的功能了。下面,将介绍如何在 Makefile 中声明变量(declare variable)

第三版Makefile

我们希望将需要反复输入的命令整合成变量,用到它们时直接用对应的变量替代,这样如果将来需要修改这些命令,则在定义它的位置改一行代码即可。

CC = g++CFLAGS = -c -WallLFLAGS = -Wall
all: hellohello: main.o function1.o function2.o $(CC) $(LFLAGS) main.o function1.o function2.o -o hellomain.o: main.cpp $(CC) $(CFLAGS) main.cppfunction1.o: function1.cpp $(CC) $(CFLAGS) function1.cppfunction2.o: function2.cpp $(CC) $(CFLAGS) function2.cpp
clean: rm -rf *.o hello

上面的 Makefile 中,开头定义了三个变量:CC,CFLAGS,和 LFLAGS。其中 CC 表示选择的编译器(也可以改成 gcc);CFLAGS 表示编译选项,-c 即 g++ 中的 -c,-Wall 表示显示编译过程中遇到的所有 warning;LFLAGS 表示链接选项,它就不加 -c 了。这些名字都是自定义的,真正起作用的是它们保存的内容,因此只要后面的代码正确引用,将它们定义成阿猫阿狗都没问题。容易看出,引用变量名时需要用 $() 将其括起来,表示这是一个变量名。

第四版Makefile

第三版的 Makefile 还是不够简洁,例如我们的 dependencies 中的内容,往往和 g++ 命令中的内容重复:

hello: main.o function1.o function2.o $(CC) $(LFLAGS) main.o function1.o function2.o -o hello

我们不想敲那么多字,能不能善用 <target>:<dependencies> 中的内容呢?这就需要引入下面几个特殊符号了(也正是这些特殊符号,把 Makefile 搞得像是天书,吓退了很多初学者):

$@ ,$<$^

例如我们有 target: dependencies 对:all: library.cpp main.cpp

$@ 指代 all ,即 target

$< 指代 library.cpp, 即第一个 dependency

$^ 指代 library.cpp 和 main.cpp,即所有的 dependencies

因此,本节开头的 Makefile 片段可以改为:

hello: main.o function1.o function2.o $(CC) $(LFLAGS) $^ -o $@

而第四版 Makefile 就是这样的:

CC = g++CFLAGS = -c -WallLFLAGS = -Wall
all: hellohello: main.o function1.o function2.o $(CC) $(LFLAGS) $^ -o $@main.o: main.cpp $(CC) $(CFLAGS) $<function1.o: function1.cpp $(CC) $(CFLAGS) $<function2.o: function2.cpp $(CC) $(CFLAGS) $<
clean: rm -rf *.o hello

但是手动敲文件名还是有点麻烦,能不能自动检测目录下所有的 cpp 文件呢?此外 main.cpp 和 main.o 只差一个后缀,能不能自动生成对象文件的名字,将其设置为源文件名字后缀换成 .o 的形式?

第五版Makefile

想要实现自动检测 cpp 文件,并且自动替换文件名后缀,需要引入两个新的命令:patsubst 和 wildcard。

wildcard

wildcard 用于获取符合特定规则的文件名,例如下面的代码:

SOURCE_DIR = . # 如果是当前目录,也可以不指定SOURCE_FILE = $(wildcard $(SOURCE_DIR)/*.cpp)target: @echo $(SOURCE_FILE)

make 后发现,输出的为当前目录下所有的 .cpp 文件:

./function1.cpp ./function2.cpp ./main.cpp

其中 @echo 前加 @是为了避免命令回显,上文中 make clean 调用了 rm -rf 会在 terminal 中输出这行命令,如果在 rm 前加了 @ 则不会输出了。

patsubst

patsubst 应该是 pattern substitution 的缩写。用它可以方便地将 .cpp 文件的后缀换成 .o。它的基本语法是:$(patsubst 原模式,目标模式,文件列表)。运行下面的示例:

SOURCES = main.cpp function1.cpp function2.cppOBJS = $(patsubst %.cpp, %.o, $(SOURCES))target: @echo $(SOURCES) @echo $(OBJS)

输出的结果为:

main.cpp function1.cpp function2.cppmain.o function1.o function2.o

综合上述两个命令,我们可以升级到第五版 Makefile:

OBJS = $(patsubst %.cpp, %.o, $(wildcard *.cpp))CC = g++CFLAGS = -c -WallLFLAGS = -Wall
all: hellohello: $(OBJS) $(CC) $(LFLAGS) $^ -o $@main.o: main.cpp $(CC) $(CFLAGS) $< -o $@function1.o: function1.cpp $(CC) $(CFLAGS) $< -o $@function2.o: function2.cpp $(CC) $(CFLAGS) $< -o $@
clean: rm -rf *.o hello

然而这一版的 Makefile 还有提升空间,它的 main.o,function1.o,function2.o 使用的都是同一套模板,不过换了个名字而已。第六版的 Makefile 将处理这个问题。

第六版Makefile

这里要用到 Static Pattern Rule,其语法为:

targets: target-pattern: prereq-patterns

其中 targets 不再是一个目标文件了,而是一组目标文件。而 target-pattern 则表示目标文件的特征。例如目标文件都是 .o 结尾的,那么就将其表示为 %.o,prereq-patterns (prerequisites) 表示依赖文件的特征,例如依赖文件都是 .cpp 结尾的,那么就将其表示为 %.cpp。


通过上面的方式,可以对 targets 列表中任何一个元素,找到它对应的依赖文件,例如通过 targets 中的 main.o,可以锁定到 main.cpp。

下面是第六版的 Makefile:

OBJS = $(patsubst %.cpp, %.o, $(wildcard *.cpp))CC = g++CFLAGS = -c -WallLFLAGS = -Wall
all: hellohello: $(OBJS) $(CC) $(LFLAGS) $^ -o $@$(OBJS):%.o:%.cpp $(CC) $(CFLAGS) $< -o $@
clean: rm -rf *.o hello


05

其它

看到有的 Makefile 设置了 -lm 的 flag,查阅资料发现表示连街 math 库,因为代码中可能

#include<math.h>

例如 

g++ -o out fun.cpp -lm
CC = g++LIBS = -lmout: fun.cpp $(CC) -o $@ $^ $(LIBS)

/ The End /

本文介绍了如何写 Makefile,主要的知识点有:

  1. 在 Makefile 中定义变量并引用

  2. $^,$@,$< 的含义

  3. wildcard,patsubst 的用法

  4. static pattern rule:targets: target-pattern: prereq-patterns

公众号回复:"Makefile"  获取本文源文件下载链接。


推荐阅读:

win10下使用VS Code编译、运行 和调试C
使用git管理嵌入式软件版本
基于VS2015的串口开发

免责声明:本文转载自知乎,版权归原作者所有。如涉及作品版权问题,请与我联系删除。

扫码关注我们

看更多嵌入式案例

喜欢本篇内容请给我们点个在看

免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!

本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除。
关闭
关闭