makefile从入门到放弃——博主吐血整理的笔记

2022-09-05 19:31:13 浏览数 (1)

大家好,又见面了,我是你们的朋友全栈君。

想要成为专业程序员,mekefile必须懂 !尤其是在Linux下进行软件编译,makefile就不得不自己写。因为,makefile关系到了整个工程的编译规则。一个工程中的源文件不计其数,并且按类型、功能、模块分别放在若干个目录中,makefile定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作,因为makefile就像一个 Shell脚本一样,其中也可以执行操作系统的命令。 makefile带来的好处就是——“自动化编译”,一旦写好,只需要一个make命令,整个工程完全自动编译,极大的提高了软件开发的效率。 make是一个命令工具,是一个解释makefile中指令的命令工具,一般来说,大多数的IDE都有这个命令,比如:Visual C 的nmake,Linux下GNU的make。可见,makefile都成为了一种在工程方面的编译方法。 这篇文章需要C/C 编译、Linux基础、GUN工具使用的相关知识,可以借鉴博主往期文章有详细讲解!

文章目录
  • make简介
  • makefile基本结构
  • makefile的基本使用实例
  • makefile变量
  • make的使用
  • makefile的隐含规则
  • makefile的VPATH
  • makefile的嵌套

make简介

  • 工程管理器,顾名思义,指管理较多的文件。
  • Make工程管理器就是个“自动化编译管理器”,这里的“自动”是指它能够根据文件时间戳自动发现更新过的文件而减少编译的工作量,同时,它通过读入Makefile文件的内容来执行大量的编译工作。
  • Make将只编译改动的代码文件,而不用完全编译。

makefile基本结构

makefile是make读入的唯一配置文件

  • 由make工具创建的目标体(target),通常是目标文件或可执行文件
  • 要创建的目标体所依赖的文件(dependency_file)
  • 创建每个目标体时需要运行的命令(command)
  • 注意:命令前必须是一个“TAB键”,否则编译错误为:*** missing separator. Stop.

makefile格式:

代码语言:javascript复制
target : dependency_files ##dependency_files是依赖的文件
	command #注意是一个TAB

例子:

代码语言:javascript复制
hello.o:hello.c hello.h
	gcc -c hello.c -o hello.o

makefile的基本使用实例

新建f1.c、f2.c、main.c、head.h四个文件,并输入一下内容:

  • f1.c内容如下:
代码语言:javascript复制
#include <stdio.h>

void print1(){ 
   
    printf("Message:f1.cn");
}
  • f2.c内容如下:
代码语言:javascript复制
#include <stdio.h>

void print2()
{ 
   
    printf("Message:f2.cn");
}
  • head.h内容如下:
代码语言:javascript复制
void print1();
void print2();
  • main.c内容如下:
代码语言:javascript复制
#include <stdio.h>
#include "head.h"#调用自己写的头文件用引号

int main()
{ 
   
        print1();
        print2();

        printf("end mainn");
        return 0;
}

在没有编写makefile之前可以使用命令gcc *.c -Wall查看当前代码是否有语法错误。检查没有语法错误以后可以编写makefile文件。

  • 新建makefile文件并输入以下内容:
代码语言:javascript复制
test:f1.o f2.o main.o
    gcc f1.o f2.o main.o -o test
f2.o:f2.c
    gcc -c -Wall f2.c -o f2.o# -Wall允许发出gcc所有有用的报警信息
f1.o:f1.c
    gcc -c -Wall f1.c -o f1.o#-c表示只编译不链接,生成目标文件“.o”
main.o:main.c
    gcc -c -Wall main.c -o main.o#-o file表示把输出文件输入到file里
clean:
	rm *.o test#删除.o和执行文件

执行makefile文件:

代码语言:javascript复制
$ make #默认生成第一个文件
$ make '目标名'#选择性的编译

当工程中的文件名和makefile中的目标重名时,就会有伪目标。执行make命令时会发现提示目标文件已经是最新的了,将不被不执行!如果我想让makefile中某个命令永远被执行。可以在makefile目标前加上.PHONY:'目标名'

makefile变量

在makefile中的定义的变量,就像是C/C 语言中的宏一样,他代表了一个文本字串,在Makefile中执行的时候其会自动原模原样地展开在所使用的地方。其与C/C 所不同的是,你可以在Makefile中改变其值。在makefile中,变量可以使用在“目标”,“依赖目标”, “命令”或是Makefile的其它部分中。 变量的命名字可以包含字符、数字,下划线(可以是数字开头),但不应该含有“:”、“#”、“=”或是空字符(空格、回车等)。变量是区分英文字母大小写的。

创建和使用变量:

  • 变量的类型

预定义变量:

变量名

变量含义

– AR

库文件维护程序名称,默认为ar.AS汇编程序名称,默认值为as。

– CC

C编译器的名称,默认为cc。CPP C预编译器的名称,默认值为$(CC) -E

– CXX

C 编译器的名称,默认为g

– FC

FORTRAN编译器的缩写,默认值为f77

– RM

文件删除程序名称,默认为rm -f

自动变量:

变量名

变量含义

– $*

不包含扩展名的目标文件名称

– $

所有的依赖文件,以空格分开,并以出现后的先后为序,可能包含重复依赖文件

– $<

第一个依赖文件的名称

– $?

所有时间戳比目标文件晚的依赖文件,并以空格分开目标文件的完整名称

– $@

目标文件的完整名称

– $^

所有不重复的目标依赖文件,以空格分开

– $%

如果目标是归档成员,则该变量表示目标的归档成员的目标名称

  • 变量定义的两种方式
代码语言:javascript复制
- 递归展开方式 VAR=var
- 简单方式 VAR: =var

一般使用递归展开方式进行变量定义

  • 变量的使用

示例1:

代码语言:javascript复制
dir :=/foo/bar
FOO?=bar

以上代码的含义是:如果FOO没有定义过,那么变量FOO的值就是‘bar’,如果FOO之前被定义过,那么什么也不做。 示例2:

代码语言:javascript复制
#为变量添加值:可以通过 =为己定义的变量添加新的值
Main=hello.o hello-1.o
Main =hello-2.o

make的使用

  • 直接运行make
  • 选项

参数

参数的作用

-C

dir读入指定目录下的makefile

-f

file读入当前目录下的file文件作为makefile

-i

忽略所有命令执行错误

-I

dir制定被包含的makefile所在目录

-n

只打印要执行的命令,但是不执行这些命令

-p

显示make变量数据库的隐含规则

-s

在执行命令时不显示命令

-w

如果执行make在执行过程中改变目录,打印当前目录名

当然makefile也可以像C语言一样调用其他的makefile的文件。

config.mk文件内容如下:

代码语言:javascript复制
OBJS=f1.o f2.o
OBJS =main.o
CFLAGS=-c -Wall -I include

makefile文件内容如下:

代码语言:javascript复制
include config.mk#调用config.mk文件的内容
test:$(OBJS)
	gcc $(OBJS) -o test
f2.o:$<
	gcc $(CFLAGS) f2.c -o $@
f1.o:f1.c
	gcc $(CFLAGS) f1.c -o $@
main.o:main.c
	gcc $(CFLAGS) main.c -o mian.o
.PHONY:clean
clean:
	rm *.o test

makefile的隐含规则

  • 隐含规则1:编译C语言的隐含规则 .o的目标的依赖目标会自动推导为.c,并且其生成命令是(CC) -c (CPPFLAGS)
代码语言:javascript复制
include config.mk
test:$(OBJS)
	gcc $(OBJS) -o test

.PHONY:clean
clean:
	rm *.o test
  • 隐含规则2:链接Object文件的隐含规则 n目标依赖于n.o,通过运行C语言编译器来运行链接程序生成(一般是“ld”),其命令是:(CC) (LDFLAGS) n.o
代码语言:javascript复制
x : x.o y.o#并且x.c、y.c都存在时,隐含命令如下
cc -c x.c -o x.o
cc -c y.c -o y.o
cc x.o y.o -o x

注意:如果没有一个源文件(如上例中的x.c)和目标名字(如上例中的x)相关联,那么最好写出自己的生成规则,不然,隐含规则会报错。

那么根据此隐含规则,我们又可以将makefile文件进行优化为以下内容:

代码语言:javascript复制
include config.mk
f1:f1.o f2.o main.o

.PHONY:clean
clean:
	rm *.o test

第二行代码中就是应用了规则二,目标文件可以改成f2、main都可以,但是必须是和所依赖的文件相关联,如果不关联(如:test)那么makefile就会报错。

makefile的VPATH

VPATH:虚路径

  • 在一些大的工程中,有大量的源文件,我们通常的做法是把许多的源文件分类,并且存放在不同的目录下。所以,当make需要找文件依赖关系时,可以在文件前加上路径,最好的办法就是把一个路径告诉make,让make在自动的去找。
  • makefile文件中的特殊变量VPATH就是完成这么一个功能,如果没有指明这个变量,make只会在当前目录中去寻找依赖文件和目标文件。如果定义了这个变量,那么,make就会在当前目录找不到的情况下,到指定的目录中去找寻文件了。
  • VPATH = src:../headers
  • 上面的定义指定两个目录,‘src’和‘…/headers’,make会按照这个顺序进行搜索。目录由‘冒号’分割。当然,当前目录永远是最高优先级搜索的地方。

示例: 新建src1/f1.c、src2/f2.c、main/main.c、include/head.h、include/myinclude.h四个文件,并输入一下内容:

  • src1/f1.c内容如下:
代码语言:javascript复制
#include <stdio.h>

void print1(){ 
   
    printf("Message:f1.cn");
}
  • src2/f2.c内容如下:
代码语言:javascript复制
#include <stdio.h>

void print2()
{ 
   
    printf("Message:f2.cn");
}
  • include/head.h内容如下:
代码语言:javascript复制
void print1();
void print2();
  • include/myinclude.h内容如下:
代码语言:javascript复制
#include <stdio.h>
  • main/main.c内容如下:
代码语言:javascript复制
#include <stdio.h>
#include "head.h"#调用自己写的头文件用引号

int main()
{ 
   
        print1();
        print2();

        printf("end mainn");
        return 0;
}

没有使用VPATH的的makefile内容如下:

代码语言:javascript复制
CFLAGS=-c -Wall -I include
test:src1/f1.o src2/f2.o main/main.o
	gcc src1/f1.o src2/f2.o main/main.o -o test
src1/f1.o:src1/f1.c#可以省略
	gcc $(CFLAGS) $^ -o $@
src2/f2.o:src2/f2.c
	gcc $(CFLAGS) $^ -o $@
main/main.o:main/main.c
	gcc $(CFLAGS) $^ -o $@#可以省略 
.PHONY:clean
clean:
	find ./ -name "*.o" -exec rm { 
   } ;;test#删除当前目录下所有的.o文件

以上代码中根据隐含规则可以简化——第4行~第9行可以省略

利用VPATH进行优化makefile

代码语言:javascript复制
CFLAGS=-c -Wall -I include
VPATH=src1 src2 main
f1:src1/f1.o src2/f2.o main/main.o

.PHONY:clean
clean:
	find ./ -name "*.o" -exec rm { 
   } ;;test

makefile的嵌套

当工程文件比较多时,如果只使用一个makefile会使得这个makefile显得比较繁琐复杂,那么我们可以通过使用makefile嵌套,内层使用子makefile,外层来调用这些子makefile。 如何来使用呢?举例说明如下:

代码语言:javascript复制
subsystem:
    cd subdir && $(MAKE)

这个例子可以这样来理解,在当前目录下有一个目录文件 subdir 和一个 makefile 文件,子目录 subdir 文件下还有一个 makefile 文件,这个文件是用来描述这个子目录文件的编译规则。使用时只需要在最外层的目录中执行 make 命令,当命令执行到上述的规则时,程序会进入到子目录中执行 make。这就是嵌套执行 make,我们把最外层的 Makefile 称为是总控 makefile。

上述的规则也可以换成另外一种写法:

代码语言:javascript复制
subsystem
    $(MAKE) -C subdir

在 make 的嵌套执行中,我们需要了解一个变量 “CURDIR”,此变量代表 make 的工作目录。当使用 make 的选项 “-C” 的时候,命令就会进入指定的目录中,然后此变量就会被重新赋值。总之,如果在 Makefile 中没有对此变量进行显式的赋值操作,那么它就表示 make 的工作目录。我们也可以在 Makefile 中为这个变量赋一个新的值,当然重新赋值后这个变量将不再代表 make 的工作目录。 export的使用: 使用 make 嵌套执行的时候,变量是否传递也是我们需要注意的。如果需要变量的传递,那么可以这样来使用:

代码语言:javascript复制
export <variable>

如果不需要那么可以这样来写:

代码语言:javascript复制
unexport <variable>

<variable>是变量的名字,不需要使用 “$” 这个字符。如果所有的变量都需要传递,那么只需要使用 “export” 就可以,不需要添加变量的名字。

Makefile 中还有两个变量不管是不是使用关键字 “export” 声明,它们总会传递到下层的 Makefile 中。这两个变量分别是 SHELL 和 MAKEFLAGS,特别是 MAKEFLAGS 变量,包含了 make 的参数信息。如果执行总控 Makefile 时,make 命令带有参数或者在上层的 Makefile 中定义了这个变量,那么 MAKEFLAGS 变量的值将会是 make 命令传递的参数,并且会传递到下层的 Makefile 中,这是一个系统级别的环境变量。

make 命令中有几个参数选项并不传递,它们是:”-C”、”-f”、”-o”、”-h” 和 “-W”。如果我们不想传递 MAKEFLAGS 变量的值,在 Makefile 中可以这样来写:

代码语言:javascript复制
subsystem:
    cd subdir && $(MAKE) MAKEFLAGS=
 

发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/135270.html原文链接:https://javaforall.cn

0 人点赞