一个简单的通用Makefile实现

Makefile是Linux下程序开发的自动化编译工具,一个好的Makefile应该准确的识别编译目标与源文件的依赖关系,并且有着高效的编译效率,即每次重新make时只需要处理那些修改过的文件即可。Makefile拥有很多复杂的功能,这里不可能也没必要一一介绍,为了简化问题的复杂性,本文仅和大家讨论针对单目录下的C/C++项目开发,如何写一个通用的Makefile。

首先,我们假设当前工程目录为prj/,该目录下有6个文件,分别是:main.c、abc.c、xyz.c、abc.h、xyz.h和Makefile。其中main.c包含头文件abc.h和xyz.h,abc.c包含头文件abc.h,xyz.c包含头文件xyz.h,而abc.h又包含了xyz.h。它们的依赖关系如图1。

 

 

图1 文件依赖关系

第一次使用Makefile应该写成这个样子(假设生成目标main):

main:main.o abc.o xyz.o

    gcc main.o abc.o xyz.o -o main

main.o:main.c abc.h xyz.h

    gcc -c main.c –o main.o -g

abc.o:abc.c abc.h xyz.h

    gcc -c abc.c –o abc.o -g

xyz.o:xyz.c xyz.h

    gcc -c xyz.c -o xyz.o -g

clean:

    rm main main.o abc.o xyz.o -f

虽然这样Makefile完全符合Makefile的书写规则,但是当代码文件再增加几倍后,再管理这些命令将会是一个噩梦!!!因此Makefile提供了默认规则和自动推导帮我们完成一些常用功能。然后,我们将Makefile修改如下:

EXE=main

CC=gcc

OBJ=main.o abc.o xyz.o

CFLAGS=-g

(EXE):(OBJ)

    (CC)^ -o $@

clean:

    rm (EXE)(OBJ) -f

变量EXE,CC,OBJ分别代指目标程序名,编译器名,目标文件名。CFLAGS是Makefile的预定义变量,它会附加在每条编译命令(gcc -c)之后。

$(EXE)是对变量的引用,$^代指所有的依赖项——即$(OBJ),$@代指目标项——即$(EXE)。该命令等价于:(CC)(OBJ) -o $(EXE)。

这个Makefile只有目标文件链接的命令,源文件的编译命令都被忽略了!这正是Makefile的自动推导功能——它可以将目标文件自动依赖于同名的源文件,即:

main.o:main.c

    gcc -c main.c -o main.o

abc.o:abc.c

    gcc -c abc.c -o abc.o

xyz.o:xyz.c

    gcc -c xyz.c -o xyz.o

 

按照上述方式,只要工程下增加了源文件后,只需要在OBJ初始化处增加一个*.o即可。但是这种方式是有问题的,Makefile的自动推导功能只会推导出目标文件对源文件的依赖关系,而不会增加头文件的依赖关系!!!这导致的直接问题就是修改项目的头文件,不会导致make的自动更新!除非修改头文件后运行一次make clean,再运行make…… :-)

为了能让make自动包含头文件的依赖关系,我们需要做一点额外的工作。幸运的是gcc为我们提供了一个编译选项(gcc -M,对于g++是-MM),能输出目标文件的依赖关系!比如:

$gcc -M main.c

main.o:main.c abc.h xyz.h

如果将每个源文件的依赖关系包含到Makefile里,就可以使得目标文件自动依赖于头文件了!再次修改原先的Makefile:

EXE=main

CC=gcc

SRC=$(wildcard *.c)

OBJ=$(SRC:.c=.o)

CFLAGS=-g

all:depend $(EXE)

depend:

@(CC)−MM(SRC) > .depend

-include .depend

(EXE):(OBJ)

(CC)(OBJ) -o $(EXE)

clean:

@rm (EXE)(OBJ) .depend -f

我们虚设了一个目标all,它依赖于depend和实际的目标EXE。而depend正式将所有的源文件对应的目标文件的依赖关系输入到.depend文件,并包含在Makefile内!这里有几个细节需要说明:

1..depend文件是隐藏文件,避免和工程的文件混淆。

2.include命令之前增加符号‘-’,避免第一次make时由于.depend文件不存在报告错误信息。

3.SRC初始化为wildcard *.c表示当前目录下的所有.c源文件,这就省去了我们手动输入新增的源文件。

4.OBJ初始化为SRC:.c=.o,表示将SRC中所有.c结尾的文件名替换为.o结尾的,这样就自动生成了源文件的目标文件序列。

5.clean的rm命令钱@符号表示执行该命令时不输出任何信息。

这样,每次执行make时都会重新计算目标文件的依赖关系,并输出到.depend文件,然后包含到Makefile后进行编译工作,这样目标文件的依赖关系就不会出错了!而我们得到了一个能自动包含源文件和识别头文件依赖关系的Makefile,将该文件应用于任何单目录的C/C++工程(C++需要修改部分细节,不作赘述)都能正常工作。

但是,这种方式也有一定的不足,当头文件的依赖关系不发生变化时,每次make也会重新生成.depend文件。如果这样使得工程的编译变得不尽人意,那么我们可以尝试将依赖文件拆分,使得每个源文件独立拥有一个依赖文件,这样每次make时变化的只是一小部分文件的依赖关系。

EXE=main

CC=gcc

SRC=$(wildcard *.c)

OBJ=$(SRC:.c=.o)

DEP=(patsubst(SRC))

CFLAGS=-g

(EXE):(OBJ)

(CC)^ -o $@

$(DEP):.%.d:%.c

@set -e;\

rm -f $@;\

(CC)−M< > @.$$$;\

sed 's,$∗\.o[ :]*,\1.o @:,g′<@.

 
> $@;\

 

rm -f @.$$$

-include $(DEP)

clean:

@rm (EXE)(OBJ) $(DEP) -f

该Makefile增加了一个变量DEP,初始化为patsubst %.c,.%.d,$(SRC),表示将SRC中的以*.c结尾的源文件名替换为.*.d的形式,比如main.c对应着文件.main.d,这就是main.c的依赖关系文件,且是隐藏的。

为了生成每个源文件的依赖文件,建立了目标依赖关系$(DEP):.%.d:%.c,该关系表示,对于目标DEP,通过$@可以访问一个依赖文件,通过$>则访问对应的同名源文件。命令部分使用\连接,表示当前命令作为一个整体在一个进程内执行。该组命令的含义是:将gcc -M生成的信息输出到一个临时文件,然后在:之前加上当前的文件名输出到依赖文件。比如对于main.c生成的临时文件信息为:

main.o:main.c abc.h xyz.h

处理后依赖文件信息是:

main.o .main.d:main.c abc.h xyz.h

这样的依赖关系表示main.o和它的依赖关系文件的依赖项是一致的,只要相关的源文件或头文件发生了改变,才会重新生成目标文件和依赖关系文件,也就达到了依赖关系文件单独更新的目的了。

虽然如此,但是这样的Makefile也不是完美的。现假设工程目录内新增一个源文件lmn.c,按照Makefile的指令make后会产生.lmn.d依赖关系文件。而如果我们再删除lmn.c源文件后,重新make后.lmn.d依然存在!尤其是当重复增删很多源文件后,工程目录下可能会存在很多无用的依赖文件,当然这些问题可以通过make clean解决。

通过前边的讨论,我们得到一个能在单目录工程下工作的通用Makefile,至于是实现为单独一个依赖文件的形式,还是每个源文件产生一个独立的依赖文件,要根据程序作者自己的喜恶来选择。虽然每种方法都有一些细微的瑕疵,但是不影响这个通用的Makefile的实用性,试想一下在工程目录下拷贝一份当前的Makefile,稍加修改便可以正确的编译开发,一定会令人心情大好。希望本文对你学习Linux写的程序开发有所帮助!

时间: 2024-08-04 07:13:12

一个简单的通用Makefile实现的相关文章

多目标-一个简单的 Makefile 问题

问题描述 一个简单的 Makefile 问题 5C 12 13 #每个 obj 的依赖项,最好写上依赖的 .h,否则 .h 更改将不重新编译 .o 14 15 CRequestHandlerFactory.o:./CRequestHandlerFactory.cpp ./CRequestHandlerFactory.h ./CRequestHandler.h 16 $(CXX) -c $(CXXFLAGS) -o CRequestHandlerFactory.o ./CRequestHandle

一个简单的链表模版类的实现

这是翻阅<数据结构.算法与应用--C++语言描述> 以及在网上得到的一些资料后写出来的.起因是在项目中要用到一个链表,但我做一个简单的链表在C++中用的时候跟C差别很多,比如赋值运算(编译器说要做操作符重载,或者考贝构造函数,C++中把结构当成一个类来看了,详见相关介绍的文档或书籍).后来一想干脆做个template顺便学习一下,一举两得. 几个问题: CListData和CNode的函数均为内联函数(inline),因为目前的编译器仍不支持分离编译.按<Thinking in C++&

C++程序设计:原理与实践(进阶篇)15.6 实例:一个简单的文本编辑器

15.6 实例:一个简单的文本编辑器 列表最重要的性质就是可以在不移动元素的情况下对其进行插入或删除操作.下面我们通过一个例子来说明这一点.考虑应该如何在文本编辑器中表示一个文本文件中的字符.所选用的表示方式应当能够使对文本文件进行的操作简单而高效. 那么具体会涉及哪些操作呢?假设文件能存储在计算机的内存中.也就是说,我们可以选择任何一种适合的表示方式,当需要保存到文件中时,只要把它转换成一个字节流就可以了.相应地,我们也可以把一个文件中的字符转成字节流,从而把它读入内存中.这说明我们只需要选择

Java核心技术卷I基础知识3.1 一个简单的Java应用程序

第3章 Java的基本程序设计结构 ▲  一个简单的Java应用程序     ▲  字符串 ▲  注释                      ▲  输入输出 ▲  数据类型               ▲  控制流 ▲  变量                      ▲  大数值 ▲  运算符                  ▲  数组   现在,假定已经成功地安装了JDK,并且能够运行第2章中给出的示例程序.我们从现在开始将介绍Java应用程序设计.本章主要介绍程序设计的基本概念(如数

《精通 ASP.NET MVC 5》----2.4 创建一个简单的数据录入应用程序

2.4 创建一个简单的数据录入应用程序 本章的其余部分将通过建立一个简单的数据录入应用程序来考查MVC的更多基本特性.本小节打算分步进行,目的是演示MVC的运用,因此会跳过对幕后工作原理的一些解释.但不必担心,在后面的章节中会重新深入地讨论这些论题. 2.4.1 设置场景 假设一个朋友决定举行一个"新年除夕晚会",于是她请笔者为其创建一个Web应用程序,以便让受邀人进行电子回复(RSVP).她的要求有以下4个关键特性. 一个显示此晚会信息的首页. 一个可以用来进行电子回复(RSVP)的

从一个简单的约束看规范性的SQL脚本对数据库运维的影响

原文:从一个简单的约束看规范性的SQL脚本对数据库运维的影响   之前提到了约束的一些特点,看起来也没什么大不了的问题,http://www.cnblogs.com/wy123/p/7350265.html以下以实际生产运维中遇到的一个问题来说明规范的重要性. 如下是一个简单的建表脚本,表面上看起来并没有什么问题.其中创建了3个约束,一个主键约束,一个唯一约束,一个默认值约束,该脚本执行起来没有任何问题. USE Test GO if exists(select 1 from sys.table

makefile-在linux写的一个简单的程序 编译不能通过 求大神给看看

问题描述 在linux写的一个简单的程序 编译不能通过 求大神给看看 解决方案 总共有3个文件 test.h test.cpp main.cpp; 在main.cpp内部调用test类实现的方法,执行make命令后,可以生成test.o和main.o但是好像链接的时候有问题, 不能生成目标问题 解决方案二: 找出问题来了 是makefile文件写的有问题 编译应该使用g++ 代码有问题的地方 希望各位指正 解决方案三: 提示信息是函数没有定义,链接返回错误.

一个简单粗暴的前后端分离方案(转)

  项目背景 刚刚参加完一个项目,背景:后端是用java,后端服务已经开发的差不多了,现在要通过web的方式对外提供服务,也就是B/S架构.后端专注做业务逻辑,不想在后端做页面渲染的事情,只向前端提供数据接口.于是协商后打算将前后端完全分离,页面上的所有数据都通过ajax向后端取,页面渲染的事情完全由前端来做.另外还有一个紧急的情况,项目要紧急上线,整个web站点的开发时间只有两周,两周啊!于是在这样的背景下,决定开始一次前后端完全分离的尝试. 之前开发都是同步渲染和异步渲染混搭的,有些东西可以

函数计算-建立一个简单的数学计算函数场景

Serverless作为一种微服务是一种概念,Serverless允许你提供代码或可执行程序提交给云厂商的解析服务(如阿里云的FunctionComputer等),由解析服务来为你执行它们,而你无需自己管理服务器.配置环境.Web环境等.这就是所谓的执行即服务execution-as-a-service,这样的一种概念听起来非常吸引人,实际上也可以算是一场革命,引用一张经典图轨迹图,让大家看看整个IT运维运行的发展轨迹,如下图: 图解:犹如我们从爬行动物猩猩一样,最初靠两只手和两只脚慢慢的挪动,