Makefile 简易教程

本文是一篇关于Makefile的简易教程,介绍了Makefile的基本概念、优点与缺点,以及主要版本。文章通过一个简单的C语言项目示例,讲解了如何编写Makefile以自动化编译过程,包括依赖关系的设定、编译命令的优化,以及如何处理多目录下的文件。还提到了gcc的编译选项和Makefile中的一些常用函数,如foreach、wildcard、filter和patsubst。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

刚开始一直没有认真看过Makefile,后来查到了下面这篇博客,讲的清楚明了,从简单到复杂一步一步介绍,很明白,所以就转载过来。具体直接点开下面的连接吧。

同时在本文后面我也会再填加一部分自己在看的时候查的资料。

https://www.cnblogs.com/owlman/p/5514724.html

再次对 凌杰 表示感谢。

 

 

本文部分内容引用:
中文维基百科
一个简单的Makefile教程

Makefile简介

在软件开发中,make通常被视为一种软件构建工具。该工具主要经由读取一种名为“makefile”或“Makefile”的文件来实现软件的自动化建构。它会通过一种被称之为“target”概念来检查相关文件之间的依赖关系,这种依赖关系的检查系统非常简单,主要通过对比文件的修改时间来实现。在大多数情况下,我们主要用它来编译源代码,生成结果代码,然后把结果代码连接起来生成可执行文件或者库文件。

优点与缺点

与大多数古老的Unix工具一样,make也分别有着人数众多的拥护者和反对者。它在适应现代大型软件项目方面有着许许多多的问题。但是,依然有很多人坚定地认为(包括我)它能应付绝大多数常见的情况,而且使用非常的简单,功能强大,表达清楚。无论如何,make如今仍然被用来编译很多完整的操作系统,而且它的那些“更为现代”的替代品们在基本操作上与它没有太大差别。

当然,随着现代的集成开发环境(IDE)的诞生,特别是非Unix的平台上,很多程序员不再手动管理依靠关系检查,甚至不用去管哪些文件是这个项目的一部分,而是把这些任务交给了他们的开发环境去做。类似的,很多现代的编程语言有自己专属的、能高效配置依赖关系的方法(譬如Ant)。

主要版本

make程序经历过各方多次的改写与重写,各方都依据自己的需要做了一些特定的改良。目前市面上主要流行有以下几种版本:

  • GNU make:
    GNU make对make的标准功能(通过clean-room工程)进行了重新改写,并加入作者自认为值得加入的新功能,常和GNU编译系统一起被使用,是大多数GNU Linux默认安装的工具。

  • BSD make:
    该版本是从Adam de Boor制作的版本上发展起来的。它在编译目标的时有并发计算的能力。主要应用于FreeBSD,NetBSD和OpenBSD这些系统。

  • Microsoft nmake:
    该版本主要用于微软的Windows系统中,需要注意的是,微软的nmake与Unix项目中的nmake是两种不同的东西,千万不要混淆。

从一个简单的例子开始

我们可以用K&R C中4.5那个例子来做个说明。在这个例子中,我们会看到一份主程序代码(main.c)、三份函数代码(getop.c、stack.c、getch.c)以及一个头文件(calc.h)。通常情况下,我们需要这样编译它:

gcc -o calc main.c getch.c getop.c stack.c 

如果没有makefile,在开发+调试程序的过程中,我们就需要不断地重复输入上面这条编译命令,要不就是通过终端的历史功能不停地按上下键来寻找最近执行过的命令。这样做两个缺陷:

  1. 一旦终端历史记录被丢失,我们就不得不从头开始;

  2. 任何时候只要我们修改了其中一个文件,上述编译命令就会重新编译所有的文件,当文件足够多时这样的编译会非常耗时。

那么Makefile又能做什么呢?我们先来看一个最简单的makefile文件:

calc: main.c getch.c getop.c stack.c
    gcc -o calc main.c getch.c getop.c stack.c 

现在你看到的就是一个最基本的Makefile语句,它主要分成了三个部分,第一行冒号之前的calc,我们称之为目标(target),被认为是这条语句所要处理的对象,具体到这里就是我们所要编译的这个程序calc。冒号后面的部分(main.c getch.c getop.c stack.c),我们称之为依赖关系表,也就是编译calc所需要的文件,这些文件只要有一个发生了变化,就会触发该语句的第三部分,我们称其为命令部分,相信你也看得出这就是一条编译命令。现在我们只要将上面这两行语句写入一个名为Makefile或者makefile的文件,然后在终端中输入make命令,就会看到它按照我们的设定去编译程序了。

请注意,在第二行的“gcc”命令之前必须要有一个tab缩进。语法规定Makefile中的任何命令之前都必须要有一个tab缩进,否则make就会报错。

接下来,让我们来解决一下效率方面的问题,先初步修改一下上面的代码:

cc = gcc
prom = calc
source = main.c getch.c getop.c stack.c
 
$(prom): $(source)
    $(cc) -o $(prom) $(source)

如你所见,我们在上述代码中定义了三个常量cc、prom以及source。它们分别告诉了make我们要使用的编译器、要编译的目标以及源文件。这样一来,今后我们要修改这三者中的任何一项,只需要修改常量的定义即可,而不用再去管后面的代码部分了。

请注意,很多教程将这里的cc、prom和source称之为变量,个人认为这是不妥当的,因为它们在整个文件的执行过程中并不是可更改的,作用也仅仅是字符串替换而已,非常类似于C语言中的宏定义。或者说,事实上它就是一个宏。

但我们现在依然还是没能解决当我们只修改一个文件时就要全部重新编译的问题。而且如果我们修改的是calc.h文件,make就无法察觉到变化了(所以有必要为头文件专门设置一个常量,并将其加入到依赖关系表中)。下面,我们来想一想如何解决这个问题。考虑到在标准的编译过程中,源文件往往是先被编译成目标文件,然后再由目标文件连接成可执行文件的。我们可以利用这一点来调整一下这些文件之间的依赖关系:

cc = gcc
prom = calc
deps = calc.h
obj = main.o getch.o getop.o stack.o
 
$(prom): $(obj)
    $(cc) -o $(prom) $(obj)

main.o: main.c $(deps)
    $(cc) -c main.c

getch.o: getch.c $(deps)
    $(cc) -c getch.c

getop.o: getop.c $(deps)
    $(cc) -c getop.c

stack.o: stack.c $(deps)
    $(cc) -c stack.c                

这样一来,上面的问题显然是解决了,但同时我们又让代码变得非常啰嗦,啰嗦往往伴随着低效率,是不祥之兆。经过再度观察,我们发现所有.c都会被编译成相同名称的.o文件。我们可以根据该特点再对其做进一步的简化:

cc = gcc
prom = calc
deps = calc.h
obj = main.o getch.o getop.o stack.o

$(prom): $(obj)
    $(cc) -o $(prom) $(obj)

%.o: %.c $(deps)
    $(cc) -c $< -o $@

在这里,我们用到了几个特殊的宏。首先是%.o:%.c,这是一个模式规则,表示所有的.o目标都依赖于与它同名的.c文件(当然还有deps中列出的头文件)。再来就是命令部分的$<和$@,其中$<代表的是依赖关系表中的第一项(如果我们想引用的是整个关系表,那么就应该使用$^),具体到我们这里就是%.c。而$@代表的是当前语句的目标,即%.o。这样一来,make命令就会自动将所有的.c源文件编译成同名的.o文件。不用我们一项一项去指定了。整个代码自然简洁了许多。

到目前为止,我们已经有了一个不错的makefile,至少用来维护这个小型工程是没有什么问题了。当然,如果要进一步增加上面这个项目的可扩展性,我们就会需要用到一些Makefile中的伪目标和函数规则了。例如,如果我们想增加自动清理编译结果的功能就可以为其定义一个带伪目标的规则;

cc = gcc
prom = calc
deps = calc.h
obj = main.o getch.o getop.o stack.o

$(prom): $(obj)
    $(cc) -o $(prom) $(obj)

%.o: %.c $(deps)
    $(cc) -c $< -o $@

clean:
    rm -rf $(obj) $(prom)

有了上面最后两行代码,当我们在终端中执行make clean命令时,它就会去删除该工程生成的所有编译文件。

另外,如果我们需要往工程中添加一个.c或.h,可能同时就要再手动为obj常量再添加第一个.o文件,如果这列表很长,代码会非常难看,为此,我们需要用到Makefile中的函数,这里我们演示两个:

cc = gcc
prom = calc
deps = $(shell find ./ -name "*.h")
src = $(shell find ./ -name "*.c")
obj = $(src:%.c=%.o) 

$(prom): $(obj)
    $(cc) -o $(prom) $(obj)

%.o: %.c $(deps)
    $(cc) -c $< -o $@

clean:
    rm -rf $(obj) $(prom)

其中,shell函数主要用于执行shell命令,具体到这里就是找出当前目录下所有的.c和.h文件。而$(src:%.c=%.o)则是一个字符替换函数,它会将src所有的.c字串替换成.o,实际上就等于列出了所有.c文件要编译的结果。有了这两个设定,无论我们今后在该工程加入多少.c和.h文件,Makefile都能自动将其纳入到工程中来。

到这里,我们就基本上将日常会用到的Makefile写法介绍了一遍。如果你想了解更多关于makefile和make的知识,请参考GNU Make Manual

 

对于gcc 的-c -o参数的介绍

-c和-o都是gcc编译器的可选参数

-c表示只编译(compile)源文件但不链接,会把.c或.cc的c源程序编译成目标文件,一般是.o文件。
-o用于指定输出(out)文件名。不用-o的话,一般会在当前文件夹下生成默认的a.out文件作为可执行程序。

例如
gcc -c test.c将生成test.o的目标文件
gcc -o app test.c将生成可执行程序app

gcc -c a.c -o a.o表示把源文件a.c编译成指定文件名a.o的中间目标文件(其实在这里,你把-o a.o省掉,效果是一样的,因为中间文件默认与源文件同名,只是后缀变化)

为什么要生成.o文件?

.o文件是目标文件,先将每个.c文件都编译为.o文件,这样在你修改了工程中代码再次编译的时候,就会找到变化过的文件再次编译生成对应的.o文件,再与原来的.o文件进行链接生成可执行程序就可以了。会提高大量的效率。特别是很大的工程,文件很多,编译过程是很长的,想想如果只是修改了一行代码,编译却要十几分钟,那是不是要崩溃?而生成了中间的.o文件就不会出现这个问题了。

 

若.c 和.h在多个子目录中,使用一下命令来生成所有的c文件和o文件

VPATH = include : inculde/libpng16 : components/md5 : components/cjson :src
SOURCES = $(foreach dir, $(VPATH), $(wildcard $(dir)/*))
SRCS = $(filter %.c, $(SOURCES))
OBJS = $(patsubst %.c, %.o, $(SRCS))

  1.其中:foreach dir,foreach这个函数是用来做循环用的。

    $(foreach <var>,<list>,<text>)

这个函数的意思是,把参数<list>;中的单词逐一取出放到参数<var>;所指定的变量中,然后再执行< text>;所包含的表达式。每一次<text>;会返回一个字符串,循环过程中,<text>;的所返回的每个字符串会以空格分隔,最后当整个循环结束时,<text>;所返回的每个字符串所组成的整个字符串(以空格分隔)将会是foreach函数的返回值。

dir函数

$(dir <names...>)

    名称:取目录函数——dir。
    功能:从文件名序列<names>中取出目录部分。目录部分是指最后一个反斜杠(“/”)之前的部分。如果没有反斜杠,那么返回“./”。
    返回:返回文件名序列<names>的目录部分。
    示例: $(dir src/foo.c hacks)返回值是“src/ ./”。

转载: https://blog.youkuaiyun.com/ly890700/article/details/52774342

   2.对于wildcard

一般我们可以使用“$(wildcard *.c)”来获取工作目录下的所有的.c文件列表。复杂一些用法;可以使用“$(patsubst %.c,%.o,$(wildcard *.c))”,首先使用“wildcard”函数获取工作目录下的.c文件列表;之后将列表中所有文件名的后缀.c替换为.o。这样我们就可以得到在当前目录可生成的.o文件列表。

1、wildcard : 扩展通配符
2、notdir : 去除路径
3、patsubst :替换通配符

详情参考:https://blog.youkuaiyun.com/liangkaiming/article/details/6267357

所以第二条命令是对VPATH中的每个文件夹执行dir命令,获得该文件夹路径,然后再通过wildcard获得该路径后面的所有文件。并进行循环,直到将VPATH中所有的文件夹都执行了一次。

3.关于filter

$(filter PATTERN…,TEXT) 
函数名称:过滤函数—filter。 
函数功能:过滤掉字串“TEXT”中所有不符合模式“PATTERN”的单词,保留所有符合此模式的单词。可以使用多个模式。模式中一般需要包含模式字符“%”。存在多个模式时,模式表达式之间使用空格分割。 
返回值:空格分割的“TEXT”字串中所有符合模式“PATTERN”的字串。 
函数说明:“filter”函数可以用来去除一个变量中的某些字符串,我们下边的例子中就是用到了此函数

sources := foo.c bar.c baz.s ugh.h 

$(filter %.c %.s,$(sources))

函数返回
值为“foo.c bar.c baz.s”

4.关于patsubst

在$(patsubst %.c,%.o,$(dir) )中,patsubst把$(dir)中的变量符合后缀是.c的全部替换成.o。

可能上面有不准确的地方,后续会继续修改增加。

参考:

https://blog.youkuaiyun.com/u012554768/article/details/46043193/

https://blog.youkuaiyun.com/liangkaiming/article/details/6267357

https://blog.youkuaiyun.com/ly890700/article/details/52774342

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值