什么是 make
make
是一个命令,他会在源文件的当前目录下寻找 makefile
或者 Makefile
文件执行这个文件中的代码。
makefile 文件的编写
我们先来见见猪跑,看看 make
怎么用的:
下面是 makefile
文件的内容:
这是 test.c
中的内容:
#include<stdio.h>
int main()
{
printf("hello make\n");
return 0;
}
之前我们想要使用 gcc
编译 test.c
生成 test
可执行文件,你是不是要这样写命令:
gcc -o test test.c
在我们写了上面的 makefile
文件之后,我们就能使用 make
命令来代替啦!
我们可以看到使用 make
命令之后顺利编译出来了可执行文件,并且能够顺利执行!
makefile 文件的编写
我们再来看 makefile
文件中的代码:
其中这个冒号前面的部分叫做依赖关系(绿色框框的那个),后面的部分叫做依赖方法(红色框框的那个)。听上去十分高大尚,翻译成白话文就是:依赖关系的形成需要依赖方法中的所有文件。
make
命令会自动扫描makefile
文件,查看当前目录下是否存在依赖方法中的所有文件,如果已经存在,那么就会执行下一行Tab
缩进的代码(只能是Tab
缩进)。那么如果不存在怎么办呢?
我们在讲 C 语言编译链接的时候知道:从 C 语言的源文件到生成功可执行文件是分成很多步骤的:
gcc -E
gcc -S
gcc -C
根据这个原理我们就在 makefile
文件中将那一行编译生成可执行文件的代码分成一步一步来执行。
test:test.o
gcc test.o -o test
test.o:test.s
gcc -c test.s -o test.o
test.s:test.i
gcc -S test.i -o test.s
test.i:test.c
gcc -E test.c -o test.i
make
命令扫描 makefile
文件时:
- 发现依赖关系
test
的依赖文件test.o
在源文件的当前目录不存在,继续向下扫描。 - 发现依赖关系
test.o
的依赖文件test.s
在源文件的当前目录不存在,继续向下扫描。 - 发现依赖关系
test.s
的依赖文件test.i
在源文件的当前目录不存在,继续向下扫描。 - 发现依赖关系
test.i
的依赖文件test.c
在源文件的当前目录已经存在,就会执行Tab
缩进的代码:gcc -E test.c -o test.i
生成test.i
。 test.i
依赖文件已经存在啦,就会执行:gcc -S test.i -o test.s
生成test.s
文件。test.s
依赖文件已经存在啦,就会执行:gcc -c test.s -o test.o
生成test.o
文件。test.o
依赖文件已经存在啦,就会执行:gcc test.o -o test
生成test
可执行文件。
在上述过程执行完成之后(使用 make
命令之后),源文件的当前目录下就会生成:test.i test.s test.o test
文件。
我们可以看到显示出来命令的执行顺序与我们推导的顺序是一样的哈!
综上所述:扫描 makefile
文件的时候,如果源文件的当前目录不存在依赖文件,就会递归似的向下执行,这种行为叫做 make
的自动化推导。
清理可执行文件
我们在更改了源文件的代码之后,需要清除可执行文件后重新编译。那么清除可执行文件能否使用 make
命令呢?那肯定是可以的撒!
clean:
rm -f test
其中,clean
是依赖关系,冒号右侧为空说明表明没有依赖的文件。那么我们应该如何使用这个依赖关系呢?
执行命令:make clean
即可。
make clean
我们看到顺利运行了呢!
将 clean
放在 makefile
文件的最开头
如果我们像这样写 makefile
文件会发生什么呢?
clean:
rm -f test
test:test.c
gcc -o test test.c
可以看到我们想要编译文件就需要使用命令:make test
,而 make
命令变成了执行:rm -f test
。
由此可见:make
命令会从上到下扫描 makefile
文件,将扫描到的第一个依赖关系作为 make
命令的默认行为。
不推荐将依赖关系 clean
放在 makefile
文件的开头。
make
命令编译多个文件
多个源文件生成一个可执行程序
我们写一个代码:在 function.h
中声明一个 Add
函数,在 function.c
中实现 Add
函数,然后在 test.c
中调用 Add
函数。
function.h
:
#pragma once
int Add(int a, int b);
function.c
:
int Add(int a, int b)
{
return a + b;
}
test.c
:
#include<stdio.h>
#include "function.h"
int main()
{
int a, b;
scanf("%d %d", &a, &b);
printf("a + b 的结果:%d\n", Add(a, b));
return 0;
}
我们想要编译 function.h function.c test.c
应该怎么做呢?其实很简单哈!
test:function.c test.c
gcc -o test test.c function.c
clean:
rm -f test
如果是多个源文件生成一个可执行程序,只需要在依赖文件中以空格隔开多个源文件即可。如果 .h
文件在源文件的当前目录,依赖文件中是不需要写 .h
文件的!
多个源文件生成多个可执行程序
如果在 makefile
文件的目录下有多个源文件,并且想要将这些个源文件分别编译成可执行文件应该怎么做呢?你可以先想一想🤔,你应该是有能力写出来的。
我们来写这样两个源文件:test1.c
和 test2.c
test1.c
:
#include<stdio.h>
int main()
{
printf("i am test1.c\n");
return 0;
}
test2.c
:
#include<stdio.h>
int main()
{
printf("i am test2.c\n");
return 0;
}
我们要使用 make
命令讲他们分别编译成:test1
和 test2
两个可执行文件。makefile
文件可以这样写:
All:test1 test2
test1:test1.c
gcc -o test1 test1.c
test2:test2.c
gcc -o test2 test2.c
clean:
rm -f test1 test2
依赖关系:All
依赖于 test1 和 test2
,make
命令扫描 makefile
文件,发现源文件当前目录不存在 test1 和 test2
那么就会继续向下扫描。当扫描到 test1 和 test2
这两个依赖关系,他们的依赖文件都在源文件的当前目录。可以直接执行他们 Tab
缩进的代码,生成 test1 和 test2
,最后完成两个源文件的编译生成两个可执行文件。
我们可以看到执行 make
命令之后也是顺利生成了 test1
和 test2
两个可执行文件了呢!
make
可以重复编译吗?为什么?
我们还是回到最开始的那个代码:
test.c
:
#include<stdio.h>
int main()
{
printf("hello make!\n");
return 0;
}
makefile
:
test:test.c
gcc -o test test.c
clean:
rm -f test
我们发现在不修改代码的情况下,是不允许二次编译的:
这是为什么呢?
显然是因为没有这个必要哈,既然你的源文件没有被修改为什么要为你重新编译呢?
那这个是怎么做到的呢?
- 一般来说,我们都是先有源文件,再有可执行程序。这就意味着源文件的最近修改时间比可执行程序的最近修改时间要早。
- 因此,我们只需要比较可执行程序的最近修改时间和源文件的最近修改时间,就可以判断源文件是否需要重新被编译啦!
🤔思考:源文件的最近修改时间会和可执行程序的最近修改时间想等吗?这个一般是不会的!😊
那么,这个用来比较的时间哪里来呢?
我们先来学习一个命令吧:这个命令可以查看一个文件的时间状态。
stat 文件
Access Modify Change
这三个时间称为文件的 ACM
时间。
- Access:最近访问时间,几乎文件的任何操作之后,这个时间都会发生改变。
- Modify:当对文件的内容做出修改之后,这个时间就会更新。
- Change:当对文件的属性做出修改之后,这个时间就会更新。
这就意味着,一旦对文件的内容做出修改,Access Modify Change
时间都会被更新。
因为
Access
时间要被频繁被修改,在实际的实现中Access
时间的更新是有一定的更新策略(例如:当Modiify
或者Change
时间到达一定的次数之后再更新Access
时间),而不是根据Access
时间的定义那样,操作一次文件都要更新这个时间。
原因:文件是被存放在磁盘中的,将数据刷新到磁盘的速度是比较慢的,频繁地修改Access
时间势必会影响操作系统的效率的。
在判断源文件是否需要重新编译,就是根据源文件和可执行程序 Modify
时间的比较结果来判定的!
如何验证呢?
再来学习一个命令吧:
touch 文件名
这个 touch
命令除了能够创建一个普通文件,还有一个功能就是:当这个文件已经存在时,能更新一个文件的 ACM
时间到当前的最新时间。
因此,我们可以更新源文件的 ACM
时间到最新,使得 make
命令可以反复编译一个相同的源文件。
我们看到,第一次可以顺利编译,这很正常。第二次使用 make
编译的时候就不能了!我们在更新源文件的 ACM
时间之后又能使用 make
编译了!由此可以验证就是通过比较源文件与可执行文件的时间来判断是否能使用 make
再次编译的!
如何让一个依赖关系一直被执行
我们上面讲了通过 touch
命令可以使用 make
一直编译。但是,还是不建议这么做,没有修改源文件就不要重复编译,这很好,不是吗!
但是清理可执行文件的依赖关系,我们就有这个需求,让他总是被执行。那么 makefile
文件应该怎么写呢?
test:test.c
gcc -o test test.c
.PHONY:clean
clean:
rm -f test
makefile
文件中被 .PHONY
修饰的依赖关系就可以被一直执行啦!
你若不信,就可以给可执行文件 test
这个依赖关系加上 .PHONY
修饰,看能不能 make
重复编译(不建议这么做!!!)。
特殊符号
- $@:表示:
依赖关系:依赖方法
中冒号前面的一坨! - @^:表示:
依赖关系:依赖方法
中冒号后面的一坨!
那么,我们写 makefile
文件就可以这么写啦:
test:test.c
gcc -o $@ $^
.PHONY:clean
clean:
rm -f test
在这个 makefile
文件中:$@
就是 test
,$^
就是 test.c
。
==这才是我们在平时用的最多的 makefile
文件的编写方法啦!==😊
取消回显
我们在使用 make
的时候是不是能看到 make
推导出来的要执行的指令的内容!像这样:
如果你不想回显命令,只需要在指令前面加上 @
符号就可以啦!
test:test.c
@gcc -o $@ $^
.PHONY:clean
clean:
@rm -f test