Makefile的写法

Makefile的作用

假设在工程operator文件夹下面有add.c、sub.c、mul.c、div.c、common.h、test.c文件。

  • add.c
//add.c
#include "commom.h"
int add(int a, int b){
    return a + b;
}
  • sub.c
//sub.c
#include "commom.h"
int sub(int a, int b){
    return a - b;
}
  • mul.c
//mul.c
#include "commom.h"
int mul(int a, int b){
    return a * b;
}
  • div.c
//div.c
#include "commom.h"
int div(int a, int b){
    return a / b;
}
  • commom.h
//commom.h
#ifndef COMM_OM
#define COMM_OM

int add(int a, int b);
int sub(int a, int b);
int mul(int a, int b);
int div(int a, int b);

#endif
  • main.c
//main.c
#include "commom.h"
#include <stdio.h>
int main(void){
    int a = 10, b = 5;
    printf("a+b = %d\n", add(a, b));
    printf("a-b = %d\n", sub(a, b));
    printf("a*b = %d\n", mul(a, b));
    printf("a/b = %d\n", div(a, b));
    return 0;
}

如果在命令行中需要执行如下命令。

gcc main.c  -o main.o
gcc add.c -o add.o
gcc sub.c -o sub.o
gcc mul.c -o mul.o
gcc div.c -o div.o
gcc add.o sub.o mul.o div.o main.o -o app

如果工程中有一万个文件,那就直接放弃编程。所有有没有一个命令,直接输入就直接生成app可执行程序呢,那这个命令就是make。只要编写一个通用的makefile,那么直接在终端中输入make,就可以生成可执行程序,而不论这个工程中有多少个文件,那么接下来就开始介绍如何编写makefile。

Makefile的语法规则

想要使用make的话,则需要在文件中有makefile或者Makefile文件,或者其他文件名字的话,则需要使用make -f filename这种格式。

在makefile中的语法格式是这样的。

# 代表注释

目标 : 依赖(条件)

​ 命令

其中目标是可执行程序或者是中间的过程文件

命令是任意的shell命令。

案例1

如果我们要将test目录下的main.c文件改成test.c文件,则使用makefile则可以这样编写。

  • 目标 : test.c
  • 依赖: main.c
  • 命令 mv main.c test.c
test.c : main.c
    mv main.c test.c

如何写Makefile

阶段1-生成一个可执行程序

如果了解Makefile的语法规则,那么根据上述工程,可以编写一个最简单的生成app可执行程序的makefile

app : main.c add.c sub.c mul.c div.c  
    gcc  main.c add.c sub.c mul.c div.c -o app

此时的makefile除了好写之外并没有任何优点,假设在这里我们改了main.c,如果重新生成可执行程序的话,则还会重新编译add.c、sub.c、mul.c、div.c文件,这样会大大的增加编译时间。

而make是一个管理工具,应该具有检测哪个文件改动了,则重新编译那个文件,最后再将所有的.o文件链接生成可执行程序的这样一个功能。所以我们如何编写makefile使得在我们更改哪个文件则重新编译哪个文件而不是全部重新编译?

阶段2-根据改动编译

为了解决阶段1的问题,我们编写了下面这个makefile

app : main.o add.o sub.o mul.o div.o
    gcc main.o add.o sub.o mul.o div.o -o app

main.o : main.c
    gcc main.c -c -o main.o

add.o : add.c
    gcc add.c -c -o add.o

sub.o : sub.c
    gcc sub.c -c -o sub.o

mul.o : mul.c
    gcc mul.c -c -o mul.o

div.o : div.c
    gcc div.c -c -o div.o

.PHONY:clean  #主要是为了防止如果此工程目录下也有一个同名的文件clean,则不会执行删除命令。
clean:
    @rm -rf *.o #加 @ 目的:执行此行命令的时候,不显示命令本身
    -rm -rf app  #加 - 目的:如果此行命令出错,则可继续执行

在这里将main.c文件中添加一行空格, 再重新make。

image-20200221100711974

这里只编译了main.c并重新链接生成可执行程序,所以对于大型的工程,makefile不仅可以减去频繁的输入编译命令,还可以大大减少编译时间。其中的原理是如果依赖中有一个文件比目标还新的话,则重新执行这条命令,这是makefile最核心的内容

Linux中有3个时间atime(最后查看文件的时间), ctime(最后修改文件数据或属性的时间), mtime(最后修改文件数据的时间),make就是通过查看目标文件和依赖文件的mtime来判断是否需要执行相应的命令。

但是这样的话,会生成大量的.o文件,如果每次都敲入rm *.o app,那就不符合懒惰才是第一生产力的宗旨了。解决办法是在makefile的最后加上一个伪目标,这样在终端中执行make clean相当于执行rm -rf *.orm -rf app

  • .PHONY的作用:

    如果没有.PHONY,并且在当前目录下有一个文件叫clean,则不会执行清除命令。所以为了避秒这种情况,加上.PHONY

    没有使用.PHONY的情况,并且存在clean文件,则不会执行rm命令。

    image
  • - 的作用

    在命令的前面加上-的作用:如果不加-,则执行到此条命令出错,则不继续向下执行;如果加上-,则执行完继续向下执行。

    image-20200221120149047
  • @ 的作用

    在命令的前面加上@的作用:如果不加@,则在终端上显示执行的命令;如果加上@,则执行命令的时候不现实执行的命令。

    image-20200221115810988

假如工程有一万个文件,那么像gcc main.c -o main.o这样的命令要敲入一万行,并且容易出错。并且这个工作很显然是一个流水线作业,有一个.c文件就根据命令生成一个对应的.o文件,所以可以引入变量去表示每一个.c和.o文件,这样的话就可以大大减少代码量了。

阶段3-使用变量

阶段3的目标是使用变量,使用的规则也很简单,常用的几个符号和变量列在下面。

  1. =:表示赋值。

  2. :=表示恒等于,类似const修饰符的意思。

  3. +=表示追加。

  4. $@ 表示目标。

  5. $^ 表示所有依赖。

  6. $< 表示所有依赖中的第一个。

  7. %.c 表示匹配任意一个以.c结尾的文件

  8. *.c 表示匹配所有以.c结尾的文件

  9. 使用变量的时候,在变量前前面加上$,并使用()扩起来变量名。

OBJ = main.o add.o sub.o mul.o div.o  # 定义中间文件

app : $(OBJ)  # 使用变量时,前面加$,并使用()扩起来
    gcc $(OBJ) -o app

%.o : %.c   #对于任意一个.o文件,都依赖于对应的.c文件
    gcc -c $< -o $@

clean:
    @rm -rf *.o #加 @ 目的:执行此行命令的时候,不显示命令本身
    -rm -rf app  #加 - 目的:如果此行命令出错,则可继续执行

在这个阶段中,已经使用了变量去替换中间过程文件,使整个makefile的代码行数大大减少。但是还有一点小缺陷是此makefile和具体的文件名相关,如果我们将此makefile移植到其他项目中,则还需要更改过程文件对应的名字。也即是这个makefile的通用性不强。

阶段4-使用函数

为了让makefile通用,在阶段4引入函数,函数的功能是列举工程下所有的.c文件,并将所有的.c文件根据规则生成对应的.o文件。在这里使用两个函数。

  • wildcard() 功能是列举目录下所有的.c文件
  • patsubst() 文件名替换

最后更改阶段3的makefile如下:

#OBJ = main.o add.o sub.o mul.o div.o

SRC = $(wildcard *.c) #找到所有以.c结尾的文件
OBJ = $(patsubst %.c,%.o,$(SRC))  # 将所有以.c结果的文件,去掉其中的.c作为OBJ
TAR = app

$(TAR) : $(OBJ)
    gcc $^ -o $@

%.o : %.c
    gcc -c $< -o $@

.PHONY:clean
clean:
    @rm -rf *.o #加 @ 目的:执行此行命令的时候,不显示命令本身
    -rm -rf app  #加 - 目的:如果此行命令出错,则可继续执行
test:
    @echo $(SRC)
    @echo $(OBJ)

阶段5-增加预处理、编译时选项

为了进一步完善,增加预处理、编译时候的选项,再次引入一下变量。

最后写成一个较为完整的makefile如下:

CPPFLAGS = -I include           #预处理时候的参数
CFLAGS = -g -Wall                           #编译的时候的参数
LDFLAGS = #-L../lib -lmylib     # 加载的库文件
CC = gcc                                                #用的编译器

SRC = $(wildcard *.c) #找到所有以.c结尾的文件
OBJ = $(patsubst %.c,%.o,$(SRC))
TAR = app

$(TAR) : $(OBJ)
    $(CC)  $^ $(LDFLAGS) -o $@

%.o : %.c
    $(CC)  $(CFLAGS) $(CPPFLAGS) -c $< -o $@

.PHONY:clean
# 彻底的删除生成的过程文件
clean:
    @rm -rf *.o  #加 @ 目的:执行此行命令的时候,不显示命令本身
    -rm -rf app  #加 - 目的:如果此行命令出错,则可继续执行
test:
    @echo $(SRC)
    @echo $(OBJ)
# 彻底的删除生成的过程文件和配置文件
distclean:

install:
    cp @(TAR) /usr/bin  #将生成的可执行程序放到/usr/bin目录下。
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容