单片机技术网|技术阅读
登录|注册

您现在的位置是:单片机技术网 > 技术阅读 > Makefile 入门

Makefile 入门

点击上方蓝字关注我哦~

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

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

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

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

  • 为了解决上述第一个问题,我们可以把所有编译需要的命令保存到文件中,编译时一键执行。针对第二个问题,我们希望有一个软件,自动检测哪些源文件被修改过,然后自动把它们挑出来选择性地编译。而 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>。即,目标:目标依赖的文件。

    顺着代码捋一下逻辑:

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

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

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

  • 遇到 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,主要的知识点有:

  • 在 Makefile 中定义变量并引用

  • $^,$@,$< 的含义

  • wildcard,patsubst 的用法

  • static pattern rule:targets: target-pattern: prereq-patterns

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


    推荐阅读:




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

    扫码关注我们

    看更多嵌入式案例

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