在linux环境下编译文件的时候,我们经常用 gcc xxx.c -o xxx 类似这样去生成可执行文件,当我们遇到要同时编译很多个文件的时候该怎么办呢,总不能一个一个罗列出来吧,这就引出了我们今天的主角 makefile ,makefile就是可以把很多个.c文件放一起编译,只需要将源文件放入一个新编辑的文件中,以后只对那一个文件进行编译即可,每次只要执行一个make命令就能完成所有的编译,能够提高编译的效率。下面就让我们来介绍一下makefile的一些简单规则吧。
目标:依赖
(tab)命令 (注意:此处的tab指的是按一次tab键,不能用空格代替)
目标:就是要生成的文件 (比如gcc main.c -o main 这里的main就是目标文件)
依赖:目标文件由哪些文件生成(上面的 mian 就是通过mian.c生成的)
命令:就是生成目标文件需要执行那些命令。
介绍完makefile的规则就可以来看makefile的第一个版本了。在当前文件夹下编写main.c fun1.c fun2.c sum.c 这几个文件,用来测试的我们就写简单一点:
main.c:
#include
int main()
{
int i=0;
for(i=0;i<5;i++)
{
printf("%d ",i);
}
printf("\n");
fun1();
fun2();
int d=sum(5);
printf("sum=%d\n",d);
return 0;
}
fun1.c:
#include
void fun1()
{
printf("makefile第一个版本\n");
}
fun2.c
#include
void fun2()
{
printf("Hello world\n");
}
sum.c:
#include
int sum(int n)
{
int sum=1;
int i=1;
for(i=1;i<=n;i++)
{
sum*=i;
}
return sum;
}
以上就是所有的测试代码文件,后面会一直用这几个文件,读者也可以自己指定自己的文件。下面我们就在当前文件下创建一个makefile文件(注意:文件名就叫makefile)
vim makefile
然后根据我们上面说到的规则就可以开始写正式的代码了,接下来就是第一个版本的makefile文件的编写:
main: main.c fun1.c fun2.c sum.c
gcc main.c fun1.c fun2.c sum.c -o main
main就是我们的目标文件,main.c fun1.c fun2.c sum.c 就是依赖,注意下面的那个 tab 键,后面就是生成目标文件需要执行的命令 gcc main.c fun1.c fun2.c sum.c -o main .要生成目标文件,我们要执行的命令就是:
make
执行完make命令后我们我们查看当前目录会发现生成了可执行文件文件 main ,我么就可以通过直接执行main来同时执行四个文件,如果有文件被修改了,我们就需要把这个可执行文件先删除了,然后再重新执行make命令重新进行编译(删除用的是 rm -f main)。
以上就是第一个版本,但是当我们只对其中一个文件进行更改时,再次使用make编译的时候会将所有的文件都再编译一遍,那么如何避免这种重复编译呢?这时候就需要引入我们的第二个版本。
在了解第二个版本之前先了解一下makefile的工作原理:
如果想要生成目标,会先检查所有的依赖是否存在,如果依赖不存在,会先向下搜索相应的规则看生成该依赖的命令是否存在,如果存在就会执行相应的命令,如果不存在,就会报错。如果全部依赖都存在,那么就会检查目标和依赖的更新时间,哪个时间大,哪个是最新的,
如果目标的时间 > 依赖的时间 那么就不要更新
如果目标的时间 < 依赖的时间 那么就需要更新
在了解完makefile的原理之后就来看看第二个版本的makefile吧。
对于第一个版本只是对.c文件的编译,我们可以从.c文件之前考虑一下,直接从.o文件开始处理,并将其分开处理。
main: main.o fun1.o fun2.o sum.o
gcc main.o fun1.o fun2.o sum.o -o main
main.o: main.c
gcc -c main.c
fun1.o: fun1.c
gcc -c fun1.c
fun2.o: fun2.c
gcc -c fun2.c
sum.o: sum.c
gcc -c sum.c
生成的目标文件还是main ,刚开始会检查是否有main.o文件,没有就通过下面的.c文件来生成,其他.o文件也是这种操作,集齐所有的.o文件后就会执行生成目标文件main的那条命令。
执行make命令会发现几个.c文件是一个一个生成的,最后生成目标文件,当我们更改其中 一个文件时,看看会执行那几条命令。
当我们更新了fun1.c文件时,再次编译会发现只对fun1.c文件进行了编译并且执行了最终的那条命令,这样就就很好的解决第一个版本的分开编译问题,但是当一个目录有很多文件要编译时,要加入很多条
fun1.o:fun1.c
gcc -c fun1.c
这种命令,会显得很复杂。逐步往下看,接下来看看第三个版本的makefile。
这里我们就要引入makefile里的变量:自定义变量,自带变量,自动变量
自定义变量:就是用户自己定义的变量,用$()引用 (例如:var=hello)
自带变量:CC 指定编译器是gcc, CPPFLAGS 预处理时的参数 -I./ CFLAGS C编译器的选项 -Wall-g-c LDFLAGS 链接器选项 -L -l
自动变量:$@ 表示目标文件(就是要生成的目标文件) $< 表示规则中的第一个条件 $^ 表示规则中的所有条件 (这三个自动变量只能在命令中使用)
解释完这些我们就可以自己来实操了,自己定义变量来简化代码。我们先把目标和依赖定义出来
target=main
object=main.o fun1.o fun2.o sum.o
定义完之后就可以进行替换了:
这都是用户自定义的变量的替换
成功编译,下面进行自带变量和自动变量的替换
替换完成,并且可以编译执行,但是看到这个makefile文件多次重复了$(CC) -c $<这个命令,显得很冗余,那么接下来对其进行修改,这里就要用到我们的模式规则了。
模式规则:%.o = %.c %表示一个或多个,简单的说就是:xxx.o依赖xxx.c
target=main
object=main.o fun1.o fun2.o sum.o
$(target): $(object)
$(CC) $^ -o $@
%.o: %.c
$(CC) -c $<
全部替换完成,看上去和之前比简洁了很多,但是他们的效果都是一样的。记住上面的写法,完全可以当成一个模板来记忆,只要替换target 和 object就可以完成自己文件的编译。(注意自动变量只能用在命令中)
在这里就要引入makefile的几个常用的函数,wildcard 和patsubst :
wildcard:查找只指定类型的文件 src=$(wildcard *.c) 就是查找当前目录下的所有的.c文件并赋给src。patsubst:匹配替换,object=$(patsubst *.c *.o,$(src)) 把src里的.c文件全部替换成.o。
注意:makefile里的函数都是有返回值的
target=main
src=$(wildcard *.c)
object=$(patsubst %.c,%.o,$(src))
$(target): $(object)
$(CC) $^ -o $@
%.o: %.c
$(CC) -c $<
带入函数的化就快接近最终的版本了,这样下次替换别的文件时,只需要将taget更改一下就行了,其他的会通过函数自动获取,非常方便快捷。
因为每次编译完文件都会生成.o文件,其实这些.o文件都是可以删除的,因为你的目标文件已经生成了,只要保存好.c文件就行,这样的话我们就需要增加一个清理操作了,不然就会显得很麻烦,每次都去清理文件,有时候还容易删错。下面看如何更改。
这里涉及到终极目标问题,第一个出现的目标就是终极目标。最后执行这个clean,可能会有人认为直接就把前面生成的文件都给删了,其实是不是这样的,这个编译执行,一定生成终极目标,这个清理命令是需要自己单独使用别的命令调用的,就是指定目标。 根据前面的makefile的工作原理,我们直到,每次都会根据依赖去检查目标是否是最新的,这里由于清理命令是没有依赖的,因此我们就要将其声明成伪目标,这样就不用去检查依赖也不用去更新。
.PHONY:clean #(生成一个伪目标)这样就不会检查依赖也不会检查更新
这样我们的清理就大功告成了,所有的也就都完成了,当需要调用清理的时候,直接使用:
make clean
就可以完成调用。
target=main
src=$(wildcard *.c)
object=$(patsubst %.c,%.o,$(src))
$(target): $(object)
$(CC) $^ -o $@
%.o: %.c
$(CC) -c $<
.PHONY:clean
clean:
rm -f $(target) $(object)
以上就是makefile的最终版本 ,在实际使用中直接进行替换即可。
当前目录如果有多个makefile文件时候,我们需要加上 -f 参数,因为make命令默认的是去找makefile文件的。
make -f makefile 文件
在这里对makefile文件进行改名,加上-f参数后依旧可以执行。