分别创建目标文件,然后在Makefile中将它们链接在一起的目的是什么?< / h1 >



当使用gcc编译器时,它会一步链接并编译。然而,将源文件转换为目标文件,然后在最后链接它们似乎是习惯做法。对我来说,这似乎没有必要。这不仅会使您的目录与一堆目标文件混淆,而且当您可以简单地将所有源文件附加到编译器中时,它还会使Makefile复杂化。例如,以下是我认为很简单的:

.PHONY: all
SOURCES = $(wildcard *.cpp)
all: default
default:
g++ $(SOURCES) -o test

整齐地变成:

g++ main.cpp test.cpp -o test

然而,使用模式规则的更复杂的makefile会使每个文件的输出混乱。例如:

.PHONY: all
SOURCES = $(wildcard *.cpp)
OBJECTS = $(SOURCES:.cpp=.o)
%.o: %.cpp
g++ -c -o $@ $<
all: default
default: $(OBJECTS)
g++ -o test $^
clean:
rm -rf *.o
g++ -c -o main.o main.cpp
g++ -c -o test.o test.cpp
g++ -o test main.o test.o

对我来说,这似乎是不必要的复杂和容易出错。那么,这种做法的原因是什么呢?

为什么要编写一个Makefile而不是编写一个简单的shell脚本?在您认为简单的示例中,您没有使用make的任何特性,您甚至可以编写一个简单的shell脚本来理解关键字buildclean,就是这样!

你实际上是在质疑写makefile而不是shell脚本的意义,我将在我的回答中解决这个问题。

还要注意,在我们编译并链接三个中等大小文件的简单情况下,任何方法都可能令人满意。因此,我将考虑一般情况,但使用Makefiles的许多好处只对大型项目重要。一旦我们学会了最好的工具,它能让我们掌握复杂的情况,我们也想在简单的情况下使用它。

shell脚本的过程范式对于类似编译的作业是错误的

编写Makefile类似于编写shell脚本,只是视角略有不同。在shell脚本中,我们描述一个问题的过程解决方案:我们可以开始使用未定义的函数以非常抽象的术语描述整个过程,然后我们细化这个描述,直到达到最基本的描述级别,此时过程只是一个普通的shell命令。在Makefile中,我们不引入任何抽象,而是关注我们想要生成的文件以及如何生成它们。这工作得很好,因为在UNIX中,一切都是一个文件,因此每个处理都由程序完成,该程序从输入文件读取其输入数据,进行一些计算并将结果写入一些输出文件

如果我们想要计算一些复杂的东西,我们必须使用大量的输入文件,这些文件被程序处理,这些程序的输出作为其他程序的输入,以此类推,直到我们生成包含结果的最终文件。如果我们将准备最终文件的计划转换为shell脚本中的一堆过程,那么处理的当前状态就变成了隐式:计划执行器知道"它在哪里",因为它正在执行给定的过程,这隐式地保证了这样和那样的计算已经完成,也就是说,这样和那样的中间文件已经准备好了。现在,哪些数据描述了"计划执行者的位置"?

无害的观察 描述"计划执行器所在位置"的数据正是已经准备好的中间文件集,这正是我们在编写Makefiles时显式显示的数据。

这个无伤大亚的观察实际上是shell脚本和Makefiles之间概念上的区别,它解释了Makefiles在编译作业和类似作业中优于shell脚本的所有优点。当然,为了充分理解这些优点,我们必须正确地编写Makefiles,这对初学者来说可能很难。

Make可以很容易地在

处继续中断的任务。当我们用Makefile描述编译作业时,我们可以很容易地中断它并稍后继续它。这是无害观察的结果。只有在shell脚本中付出相当大的努力才能达到类似的效果。

Make可以很容易地处理一个项目的多个构建

您注意到Makefiles将使源树与目标文件混淆。但是Makefiles实际上可以被参数化,将这些目标文件存储在一个专用的目录中,高级的Makefiles允许我们同时拥有几个目录,其中包含一个项目的几个构建版本,并具有不同的选项。(例如,启用了不同的特性,或调试版本等)这也是无害观察的结果,即Makefiles实际上是围绕中间文件集进行连接的。

Make可以很容易地并行构建

我们可以很容易地构建并行程序,因为这是许多版本make的标准函数。这也是无害观察的结果:因为"计划执行者所在的位置"是Makefile中的显式数据,因此make有可能对此进行推理。在shell脚本中实现类似的效果需要付出很大的努力。

makefile很容易扩展

由于特殊的透视图——也就是说,作为无害观察的另一个结果——用于编写makefile,我们可以很容易地扩展它们。例如,如果我们决定所有数据库I/O样板代码都应该由一个自动工具编写,我们只需要在Makefile中写入自动工具应该使用哪些文件作为编写样板代码的输入。不多不少。我们可以在我们喜欢的地方添加这个描述,make会得到它。在shell脚本构建中做这样的扩展会比必要的困难。

这种可扩展性的便利性是Makefile代码重用的一个很大的激励。

对我来说,最重要的两个原因是:

  • 可以同时编译多个源文件,减少构建时间
  • 如果你改变了一个文件,你只重新编译那个文件,而不是重新编译所有文件

"compile everything every time"的参数如下:

http://xkcd.com/303/

但是开玩笑,当你做了一个小的改变时,编译一个文件要快得多,而不是每次都重新编译所有的文件。我的Pascal编译器项目不是很大,但它仍然需要大约35秒来编译它。

使用make -j3(即一次运行3个编译作业,我目前在我的备用计算机上只有一个双核处理器),编译文件的时间只需要十秒钟,但是如果你没有多个编译作业,你就不能做-j 3

只重新编译一个(较大的)模块需要16秒。

我知道我宁愿等16秒或35秒…

我将解释为什么创建目标文件会导致更快的编译。

考虑下面的类比:

你正在学习从零开始造一辆汽车(也就是说,你有很多金属和橡胶做轮子)。你拥有制造汽车所有主要部件(车架、车轮、发动机等)所需的所有机器。假设每台机器制造汽车的一个特定部件,例如,假设您有一台框架制造机器。假设需要大量时间来准备一台机器,因为您必须阅读非常复杂的手册:(

场景1你花了半天准备好所有的机器,然后一个小时制造零件并将所有零件焊接在一起以完成制造汽车。然后关闭所有使用过的机器(这会删除所有自定义设置)。然而,后来你意识到你为这辆车制造了错误的引擎。因为你已经把汽车的所有部件焊接在一起了,所以你不能更换以前的发动机,所以你必须重新制造所有的部件。你又花了半天时间把机器准备好(通过重新阅读说明书),又花了一个小时来制造所有的零件并把它们连接起来。痛苦!

场景2你在半天内准备好机器,然后你在笔记本上记下你在准备机器时所做的一切。。你在一个小时内制造了汽车的所有部件,然后把所有部件焊接在一起,完成了汽车的制造,然后关掉了机器。然而,后来你意识到你给这辆车装了错误的引擎。你必须重新制造所有零件。因为你在笔记本上记录了你所做的一切现在准备机器只需要10分钟。你再花一个小时制作零件并将它们连接在一起。这为你节省了很多时间(几乎半天)。

  • 您所阅读的复杂手册是源文件
  • 记录笔记的笔记本是对象文件
  • 最后一辆车是二进制文件。

目标文件是中间结果,通过在生成二进制文件之前以适当的形式跟踪已经完成的大部分艰苦工作,可以帮助您避免再次进行所有编译(使机器准备好)。目标文件有更多的用途!如果这让你感兴趣,你应该读一读。

最新更新