GNU make— —教你快速入门Makefile教程1

Makefile教程

1、定义

1.1、引出举例

四则运算实例

//add.h,加法
#ifndef _ADD_H
#define _ADD_H

int add(int a,int b);

#endif


//sub.h,减法
#ifndef _SUB_H
#define _SUB_H

int sub(int a,int b);

#endif

//mul.h、div.h同加法、减法,改一下函数名
//add.cpp
#include "add.h"

int add(int a, int b)
{
    return a + b;
}

//sub.cpp
#include "sub.h"

int sub(int a, int b)
{
    return a - b;
}

//mul.cpp,div.cpp同上,只需要改一下函数名,以及对应的函数体内的运算符号
//main.cpp
#include<iostream>
#include"add.h"
#include"sub.h"
#include"mul.h"
#include"div.h"

using namespace std;

int main()
{
    cout << "add_sum = " << add(1,1) << endl;
    cout << "sub_sum = " << sub(1,1) << endl;
	cout << " mul_sum = " << mul(2,3) << endl;
	cout << "div_sum = " << div(8,2) << endl;
	return 0;
}

普通编译过程:
在这里插入图片描述
在这里插入图片描述
makefile的编写实例:直接定义一个makefile文件名(没有多余后缀),然后make编译

//vi makefile  指令 生成  makefile文件
main:main.o add.o mul.o sub.o div.o
	g++ -o main add.o mul.o sub.o div.o main.o
main.o:main.cpp
	g++ -c main.cpp
add.o:add.cpp 
	g++ -c add.cpp
mul.o:mul.cpp
	g++ -c  mul.cpp
sub.o:sub.cpp 
	g++ -c sub.cpp
div.o:div.cpp 
	g++ -c div.cpp
.PHONY:clean
clean:
	rm -f *.o main
//make编译

在这里插入图片描述

1.2、定义

一个工程中的源文件不计其数,并且按类型、功能、模块分别放在若干个目录中,makefile定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作,因为makefile就像一个Shell脚本一样,其中也可以执行操作系统的命令。

makefile带来的好处就是——“自动化编译”,一旦写好,只需要一个make命令,整个工程完全自动编译,极大的提高了软件开发的效率。 make是一个命令工具,是一个解释makefile中指令的命令工具。

2、Makefile文件的格式

target ... : prerequisites ...
    command
    ...
    ...

第一行冒号前面的部分,叫做目标(target) ,冒号后面的部分叫做前置条件(prerequisites);第二行必须由一个tab键(空格)起首,后面跟着命令(commands)

  • 1、目标target:可以是一个object file(目标文件),也可以是一个执行文件,还可以是一个标签(label)。对于标签这种特性,在后续的“伪目标”章节中会有叙述。
    • "目标"是必需的,不可省略;"前置条件"和"命令"都是可选的,但是两者之中必须至少存在一个。
  • 2、前置条件prerequisites:生成该target所依赖的文件和/或target
  • 3、命令command:该target要执行的命令(任意的shell命令)
    • 每条规则就明确两件事:构建目标的前置条件是什么,以及如何构建。

这是一个文件的依赖关系,也就是说,target这一个或多个的目标文件依赖于prerequisites中的文件,其生成规则定义在command中。说白一点就是说:

prerequisites中如果有一个以上的文件比target文件要新的话,command所定义的命令就会被执行。

2.1、目标(target)

一个目标(target)就构成一条规则。目标通常是文件名,指明Make命令所要构建的对象 。目标可以是一个文件名,也可以是多个文件名,之间用空格分隔。

扫描二维码关注公众号,回复: 11470604 查看本文章
a.txt: b.txt c.txt
    cat b.txt c.txt > a.txt
//这里的a.txt就是目标,a.txt 依赖于 b.txt 和 c.txt

make a.txt 这条命令的背后,实际上分成两步:

  • 第一步,确认 b.txt 和 c.txt 必须已经存在;
  • 第二步使用 cat 命令 将这个两个文件合并,输出为新文件。

除了文件名,目标还可以是某个操作的名字,这称为伪目标(phony target)

clean:
      rm *.o
 //删除.o后缀的对象文件
make  clean

上面代码的目标是clean,它不是文件名,而是一个操作的名字,属于"伪目标 "。但是,如果当前目录中,正好有一个文件叫做clean,那么这个命令不会执行。

  • 因为Make发现clean文件已经存在,就认为没有必要重新构建了,就不会执行指定的rm命令。

为了避免这种情况,可以明确声明clean是"伪目标",写法如下。

.PHONY: clean
clean:
        rm *.o temp
//.PHONY内置目标名

声明clean是"伪目标"之后,make就不会去检查是否存在一个叫做clean的文件,而是每次运行都执行对应的命令

2.2、前置条件(prerequisites)

前置条件通常是一组文件名,之间用空格分隔。它指定了"目标"是否重新构建的判断标准:只要有一个前置文件不存在,或者有过更新(前置文件的last-modification时间戳比目标的时间戳新),"目标"就需要重新构建。

  • make会比较targets文件和prerequisites文件的修改日期,如果prerequisites文件的日期要比targets文件的日期要新,或者target不存在的话,那么,make就会执行后续定义的命令。
result.txt: source.txt
    cp source.txt result.txt

构建 result.txt 的前置条件是 source.txt 。如果当前目录中,source.txt 已经存在,那么make result.txt可以正常运行,否则必须再写一条规则,来生成 source.txt 。

如果需要生成多个文件,往往采用下面的写法。

source: file1 file2 file3

上面代码中,source 是一个伪目标,只有三个前置文件,没有任何对应的命令。

make source

执行make source命令后,就会一次性生成 file1,file2,file3 三个文件。这比下面的写法要方便很多。

 make file1
 make file2
 make file3

2.3、命令(commands)

命令(commands)表示如何更新目标文件,由一行或多行的Shell命令组成。它是构建"目标"的具体指令,它的运行结果通常就是生成目标文件。

  • 每行命令之前必须有一个tab键。
  • 需要注意的是,每行命令在一个单独的shell中执行。这些Shell之间没有继承关系。
var-lost:
    export foo=bar
    echo "foo=[$$foo]"

上面代码执行后(make var-lost),取不到foo的值。因为两行命令在两个不同的进程执行。一个解决办法是将两行命令写在一行,中间用分号分隔。

var-kept:
    export foo=bar; echo "foo=[$$foo]"

另一个解决办法是在换行符前加反斜杠转义。

var-kept:
    export foo=bar; \
    echo "foo=[$$foo]"

最后一个方法是加上.ONESHELL:命令。

.ONESHELL:
var-kept:
    export foo=bar; 
    echo "foo=[$$foo]"

2.4、综合实例说明(以上面四则运算为例)

main:main.o add.o mul.o sub.o div.o
	g++ -o main add.o mul.o sub.o div.o main.o
main.o:main.cpp
	g++ -c main.cpp
add.o:add.cpp 
	g++ -c add.cpp
mul.o:mul.cpp
	g++ -c  mul.cpp
sub.o:sub.cpp 
	g++ -c sub.cpp
div.o:div.cpp 
	g++ -c div.cpp
.PHONY:clean
clean:
	rm -f *.o main

在这个makefile中,目标文件(target)包含:执行文件main和中间目标文件( *.o ),依赖文件(prerequisites)就是冒号后面的那些 .c 文件。每一个 .o 文件都有一组依赖文件,而这些 .o 文件又是执行文件 main 的依赖文件。依赖关系的实质就是说明了目标文件是由哪些文件生成的,换言之,目标文件是哪些文件更新的。

2.5、make是如何工作

在默认的方式下,也就是我们只输入 make 命令。那么,

  • 1、make会在当前目录下找名字叫“Makefile”“makefile”的文件。

  • 2、如果找到,它会找文件中的第一个目标文件(target),在上面的例子中,他会找到main这个文件,并把这个文件作为最终的目标文件。

  • 3、如果main文件不存在,或是main所依赖的后面的 .o 文件的文件修改时间要比main
    这个文件新,那么,他就会执行后面所定义的命令来生成 main这个文件。

  • 4、如果 main所依赖的 .o文件也不存在,那么make会在当前文件中找目标为 .o 文件的依赖性,如果找到则再根据那一个规则生成 .o文件。(这有点像一个堆栈的过程)

  • 5、当然,你的C文件和H文件是存在的啦,于是make会生成 .o 文件,然后再用 .o文件生成make的终极任务,也就是执行文件main了。

这就是整个make的依赖性,make会一层又一层地去找文件的依赖关系,直到最终编译出第一个目标文件。在找寻的过程中,如果出现错误,比如最后被依赖的文件找不到,那么make就会直接退出,并报错,而对于所定义的命令的错误,或是编译不成功,make根本不理。make只管文件的依赖性,即,如果在我找了依赖关系之后,冒号后面的文件还是不在,那么对不起,我就不工作啦。

通过上述分析,我们知道,像clean这种,没有被第一个目标文件直接或间接关联,那么它后面所定义的命令将不会被自动执行,不过,我们可以显示要make执行。即命令—— make clean ,以此来清除所有的目标文件,以便重编译。

于是在我们编程中,如果这个工程已被编译过了,当我们修改了其中一个源文件,比如add.cpp,那么根据我们的依赖性,我们的目标 add.o 会被重编译(也就是在这个依性关系后面所定义的命令),于是 add.o 的文件也是最新的啦,于是 add.o 的文件修改时间要比 main要新,所以 main也会被重新链接了。

3、Make使用变量和自动推导进行优化

3.1、Make使用变量

可以看到 .o 文件的字符串被重复了两次,如果我们的工程需要加入一个新的 .o 文件,那么我们需要在两个地方加(应该是三个地方,还有一个地方在clean中)。当然,我们的makefile并不复杂,所以在两个地方加也不累,但如果makefile变得复杂,那么我们就有可能会忘掉一个需要加入的地方,而导致编译失败。所以,为了makefile的易维护,在makefile中我们可以使用变量。

main:main.o add.o mul.o sub.o div.o
	g++ -o main add.o mul.o sub.o div.o main.o
main.o:main.cpp
	g++ -c main.cpp
add.o:add.cpp 
	g++ -c add.cpp
mul.o:mul.cpp
	g++ -c  mul.cpp
sub.o:sub.cpp 
	g++ -c sub.cpp
div.o:div.cpp 
	g++ -c div.cpp
.PHONY:clean
clean:
	rm -f *.o main

比如,我们声明一个变量,叫 objects , OBJECTS , objs , OBJS , obj 或是 OBJ ,反正不管什么啦,只要能够表示obj文件就行了。我们在makefile一开始就这样定义:

object = main.o add.o mul.o sub.o div.o

于是,我们就可以很方便地在我们的makefile中以 $(object) 的方式来使用这个变量了,于是我们的改良版makefile就变成下面这个样子:

object = main.o add.o mul.o sub.o div.o

main:$(object)
        g++ -o main $(object)
main.o:main.cpp
        g++ -c main.cpp
add.o:add.cpp
        g++ -c add.cpp
mul.o:mul.cpp
        g++ -c  mul.cpp
sub.o:sub.cpp
        g++ -c sub.cpp
div.o:div.cpp
        g++ -c div.cpp


.PHONY:clean
clean:
        rm -f $(object)  main

于是如果有新的 .o 文件加入,我们只需简单地修改一下 object变量就可以了。

3.2、Make自动推导

只要make看到一个 .o 文件,它就会自动的把 .cpp文件加在依赖关系中,如果make找到一个 whatever.o ,那么 whatever.cpp就会是 whatever.o 的依赖文件。并且 g++ -c whatever.cpp 也会被推导出来,于是,我们的makefile再也不用写得这么复杂。我们的新makefile又出炉了。

object = main.o add.o mul.o sub.o div.o

main:$(object)
        g++ -o main $(object)
main.o:
add.o:
mul.o:
sub.o:
div.o:

.PHONY:clean
clean:
        rm -f $(object)  main

那么多的重复的 .o ,能不能把其收拢起来,好吧,没有问题,这个对于make来说很容易,谁叫它提供了自动推导命令和文件的功能呢?来看看最新风格的makefile吧。


main:$(object)
        g++ -o main $(object)
$(object):


.PHONY:clean
clean:
        rm -f $(object)  main

进一步$(object):也可以删去,得到:

main:$(object)
        g++ -o main $(object)

.PHONY:clean
clean:
        rm -f $(object)  main

在这里插入图片描述

参考

1、https://seisman.github.io/how-to-write-makefile/overview.html
2、https://ruanyifeng.com/blog/2015/02/make.html

猜你喜欢

转载自blog.csdn.net/JMW1407/article/details/107360797