makefile快速入门

Makefile干嘛的?

当我们编程做一个项目,文件众多复杂,不可能每次编译都去敲gcc指令编译链接工程,一则效率不高,随便修改某个文件就需要把工程内所有文件都编译一遍,二则包含项目太多稍不注意就会遗漏、缺失容易出错,Makefile其实就是一个项目工程管理工具,只需要执行make指令,就能做到像编程IDE中的build按钮那样轻轻一点自动把项目编译完成。生成目标程序,效率很高,但要写好Makefile,写出自己的风格,并不是短时间就能做到,笔者水平有限,但我相信这是一篇针对新手来说极有效率的Makefile入门文章


Makefile格式

目标:依赖项
(TAB)   所执行的命令

格式很简单,一个操作基本单元一般是两行,第一行是所想要生成的目标与生成该目标所需要的依赖项,两者用:隔开,第二行就是具体的实现方法,开头用TAB键缩进,这是语法规定,后面跟所要执行的命令即可。
举一个简单的例子,我们编译一个main.c文件生成run可执行程序,一般做法是:gcc main.c -o run,在Makefile里面可以这样写:

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

保存完成后,只需要执行make指令,编译就会自动完成,不用每次都去敲gcc命令,这样是不是提高效率了呢?可能一个文件的编译还是不能体现Makefile工程管理的优越性


Makefile粗略原理:

文件编辑都会保存最后一次修改的时间戳,Makefile执行时会对目标和所依赖项的时间进行比较,如果目标最后修改的时间要早于依赖项内目标时间,证明依赖项被改动过,那么目标需要被重新构建,那么所在的这两行操作单元就会被执行,当然一个Makefile会有很多这样的操作单元,Makefile会依次比较、构建直到最终目标生成。原理大概如此,反正这样理解不会有太大的偏差。


实战

通过一个小例程一步步实现Makefile优化,看一下我们的工程结构

root@DESKTOP-FR31BP0:/tmp# ls
src
root@DESKTOP-FR31BP0:/tmp# tree src/
src/
├── func1.c
├── func2.c
└── main.c
root@DESKTOP-FR31BP0:/tmp#

工程目录src里面有三个.c文件,我们需要将它们编译成名称为run的目标文件
普通做法:

root@DESKTOP-FR31BP0:/tmp/src# gcc *.c -o run
root@DESKTOP-FR31BP0:/tmp/src# ls
func1.c  func2.c  main.c  run

第一版本Makefile

run:main.c func1.c func2.c
        gcc main.c func1.c func2.c -o run

执行

root@DESKTOP-FR31BP0:/tmp/src# make
gcc main.c func1.c func2.c -o run
root@DESKTOP-FR31BP0:/tmp/src# ls
func1.c  func2.c  main.c  Makefile  run

执行成功没问题,生成目标文件,证明编写无误
存在的问题:执行效率太低,当我们编译一个工程文件,编译的时候就会把整个工程里面连同没改动的文件都会重新编译,这里的例子是三个文件体现不出弊端,如果工程量很大、文件数量多,编译会浪费很长的时间,针对改进执行效率,看我们推出的第二版Makefile

第二版本Makefile

run:main.o func1.o func2.o
        gcc main.o func1.o func2.o -o run
main.o:main.c
        gcc -c main.c
func1.o:func1.c
        gcc -c func1.c
func2.o:func2.c
        gcc -c func2.c

执行一下:

root@DESKTOP-FR31BP0:/tmp/src# ls
func1.c  func2.c  main.c  Makefile
root@DESKTOP-FR31BP0:/tmp/src# make
gcc -c main.c
gcc -c func1.c
gcc -c func2.c
gcc main.o func1.o func2.o -o run
root@DESKTOP-FR31BP0:/tmp/src#

执行成功没问题,生成目标文件,证明编写无误
执行效率上根据Makefile原理,make执行时只会对改动过的文件重新编译,未改动过的内容不会重新编译,我们将过程细化,做到了可针对某个文件的编译,执行效率问题已经解决。然而它还是不完美
第二版存在问题:很多重复操作:将.c文件变为.o文件,这里只是三个文件,问题不算突出,假设工程内有成百上千的文件,这一步就要重复写成百上千次,对我们写Makfile来说真是个灾难,这个问题同样存在第一个基本单元中,在执行命令中,把所有的依赖文件全部写出来,很烦!!!我们能不能用简洁的方法替换掉这些书写不高效的内容呢?那就要祭出变量这个工具了,第三版Makefile就着重改进这一项吧:提高书写效率。


第三版Makefile

material=main.o func1.o func2.o
target=run
$(target):$(material)
        gcc $^ -o $@
%.o:%.c
        gcc $< -c

执行效果:

root@DESKTOP-FR31BP0:/tmp/src# ls
func1.c  func2.c  main.c  Makefile
root@DESKTOP-FR31BP0:/tmp/src# make
gcc main.c -c
gcc func1.c -c
gcc func2.c -c
gcc main.o func1.o func2.o -o run
root@DESKTOP-FR31BP0:/tmp/src# ls
func1.c  func1.o  func2.c  func2.o  main.c  main.o  Makefile  run

执行成功书写没有问题,书写上是不是比第二版简洁很多,但多出来很多奇怪的字符,这些都是什么呢?
这些是Makefile变量,在Makefile里变量分类很多

  • 自定义变量
    • 变量名(小写)=变量
    • 取变量值 $(变量名)
  • makefile自带的变量:要大写
    • cppFLAGS
    • CC
  • 自动变量
    • $@:规则中的目标
    • $<:规则中的第一个依赖
    • $^:规则中所有的依赖
    • 自动变量只能在规则的命令中使用,不能用在目标与依赖里面
  • %:通配符,根据需要用来填充相关内容

虽然我们只写了一次%.o:%.c,但这个操作会一直进行,直到没有新的依赖要转换,%根据实际情况会被替换,例如main.o:main.c,执行时%会被相应替换。只要替换未完成就会一直执行
第三版写到这儿对于一个工程来说已经算可以了,书写效率、执行效率也都还好,但是如果我们想让它更加完美,下一个突破点在哪呢?那得看我们还有哪些需求,通常公司对工程目录结构都有规定,这样每个员工写出的代码,工程目录结构都差不多,便于管理,这就意味着每个工程的Makefile书写具有很大的相似性,我们能不能搞一个通用的Makefile,让它适应任何相似工程结构的工程呢?答案是肯定的,我们现在存在通用性的阻碍主要是存在特定声明,Makefile变量里的.o文件只存在这个工程,另外一个工程并没有,进行移植时,我们需要逐一修改变量里的.o文件,如果文件数量多,那么书写工作也不轻松,那我们能不能让Makefile自己去寻找我们所需要的东西,自动填充,而不需要我们自己去书写呢?这样对与工程目录相似的工程的通用性问题便解决了,只要工程目录相似,Makefile都可以直接拿来使用。这个就是第四版Makefile所解决的问题了,思路就是利用Makefile函数


第四版Makefile

写之前先介绍两个Makefile函数

  • Makefile函数都是有返回值的,参数表不用括号括起来
  • 查找指定目录下指定类型的文件
    • 返回值 wildcard 参数
      • 参数目录+文件类型注意是一个参数
      • 返回值:查找出来的指定的文件列表
  • 匹配替换函数
    • 返回值 patsubst 参数1,参数2,参数3
      • 参数1:原来的类型
      • 参数2:替换的类型
      • 参数3:指定替换文件
      • 返回值:替换后的文件列表

具体用法见下面:

material=$(patsubst %.c,%.o,$(wildcard ./*.c))
target=run
$(target):$(material)
        gcc $^ -o $@
%.o:%.c
        gcc $< -c

运行情况:

root@DESKTOP-FR31BP0:/tmp/src# ls
func1.c  func2.c  main.c  Makefile
root@DESKTOP-FR31BP0:/tmp/src# make
gcc func1.c -c
gcc main.c -c
gcc func2.c -c
gcc func1.o main.o func2.o -o run
root@DESKTOP-FR31BP0:/tmp/src# ls
func1.c  func1.o  func2.c  func2.o  main.c  main.o  Makefile  run

可以正常运行

注意patsubst函数的参数一定要用%号。不要用*否则替换会失败
通用性问题解决了,书写效率问题解决了,运行效率问题解决了,想一想还有什么需要做的?那就是最后一步工程清理工作了,为了运行效率我们会生成很多中间文件.o等等。当我们不需要这些中间文件时可以使用一个伪命令去删除这些文件,为什么叫做伪命令,因为这些目标并不真实存在,流行做法如下:

clean:
    -rm *.o run -f

cleanMakefile语法中的目标,但它没有任何依赖,执行make它会被忽略,除非我们在终端指定它强制执行make clean,它才会被执行,这个命令一般放在Makefile尾部,执行Makefile时并不会妨碍我们正常编译工作。这不是标准写法,绝大多数情况下不会出问题,但是当工程里面确实存在clean这个文件时,执行make clean便不能正常执行,因为clean真实存在,执行时又不需要任何依赖,会被认定永远是最新的,会把clean的命令忽略掉不去执行,碰到这种情况怎么办呢?我们还是写标准一点吧。用Makefile关键字`.PHONY去声明伪指令

material=$(patsubst %.c,%.o,$(wildcard ./*.c))
target=run
$(target):$(material)
        gcc $^ -o $@
%.o:%.c
        gcc $< -c
.PHONY:clean
clean:
    -rm *.o run -f

解释:在删除命令前加-,表示如果执行这个命令遇阻会继续执行下面命令(我们的命令比较多的时候),不会影响其他命令的执行,加-f命令表示强制删除,不需要任何提示。
当然一个工程还需要链接头文件库文件等等,这里没有体现,可以自己去尝试,大同小异,写好Makefile就是需要不断练习,多看其他人的写法,不断丰富自己,本人能力有限,希望能给大家有所帮助

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值