在Makefile中单独创建目标文件然后将它们链接在一起的目的是什么?

9
使用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

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

4
你的“简单” makefile 不太好,因为如果你改变其中一个源文件,它不会重新构建。 - juanchopanza
2
@juanchopanza 更不用提头文件依赖了... :P - πάντα ῥεῖ
2
在一个体面的代码库中,您可能有一堆需要不同对象文件集的可执行文件,它们之间共享一些文件。然后,您可能希望将一些对象文件放入库中,并直接链接其他文件。通过将对象文件分为单独的逻辑单元,可以高效灵活地构建可执行文件。 - Galik
2
如果你有十几个源文件,每个都有几千行代码,并且只更改了一个文件,你是否仍然想要每次重新编译所有文件? http://xkcd.com/303/ - Mats Petersson
2
@MSalters我完全不同意!GNU make工具仍然是我手边最强大的武器之一,可以用来处理构建过程中的任何微小模糊细节位和依赖关系。相比于make的能力,CMake、Ant以及其他许多工具都显得非常逊色(其中大部分只是在其基础上添加了一些功能)。 - πάντα ῥεῖ
显示剩余5条评论
4个回答

12
为什么要写Makefile而不是写简单的shell脚本?在您认为简单的示例中,您没有使用任何make的功能,甚至可以编写一个理解关键字“build”和“clean”的简单shell脚本,就可以完成任务!
实际上,您正在质疑编写Makefile而非shell脚本的意义,我将在我的回答中解决这个问题。
此外,请注意,在我们编译并链接三个中等大小的文件的简单情况下,任何方法都可能令人满意。因此,我将考虑一般情况,但使用Makefiles的许多好处仅在较大的项目中才很重要。一旦我们学会了可以掌握复杂情况的最佳工具,我们也想在简单情况下使用它。
shell脚本的过程式范式对类似编译的作业是错误的
编写Makefile类似于以稍微改变的视角编写shell脚本。在shell脚本中,我们描述了解决问题的过程式解决方案:我们可以开始使用未定义的函数以非常抽象的术语描述整个过程,并将此描述细化,直到达到最基本的描述级别,其中过程只是普通的shell命令。在Makefile中,我们不引入任何抽象概念,而是关注我们要生成的文件以及如何生成它们。这很有效,因为在UNIX中,一切都是文件,因此每个处理都由一个从输入文件读取其输入数据的程序完成,进行一些计算,并将结果写入一些输出文件。如果我们想计算一些复杂的东西,就必须使用许多输入文件,这些文件由程序处理,其输出作为其他程序的输入等等,直到生成包含结果的最终文件。如果我们将准备最终文件的计划转换为一堆 shell 脚本中的过程,则当前处理状态变得隐含:计划执行器知道“它在哪里”,因为它正在执行给定的过程,这个过程隐含保证了已经完成了某些计算,即已经准备好某些中间文件。那么,描述“计划执行器在哪里”的数据是什么呢? <弱化观察> 描述“计划执行器在哪里”的数据正好是已经准备好的中间文件集,这正是在编写 Makefile 时使数据显式的数据。 这个弱化观察实际上是 shell 脚本和 Makefile 之间的概念差异,解释了 Makefile 在编译作业和类似作业中的所有优点。当然,要充分体会这些优点,我们必须编写 正确的 Makefile,这可能对初学者来说很难。

Make使中断的任务易于恢复

当我们使用Makefile描述编译作业时,可以轻松地中断它并稍后恢复。这是<弱化观察>的结果。在 shell 脚本中,只有付出相当大的努力才能达到类似的效果。

Make使处理多个项目构建变得容易

您注意到 Makefiles 会用对象文件混乱源树。但实际上可以对 Makefile 进行参数设置,将这些对象文件存储在专用目录中,并且高级 Makefile 允许我们同时拥有几个包含不同选项的项目构建的目录。(例如启用不同的功能或调试版本等)。这也是基于<弱化观察>,Makefile 实际上是围绕中间文件集组织的。

Make使并行生成更容易

我们可以轻松地并行构建程序,因为这是许多版本的“make”的标准功能。这也是“无害观察”的结果:因为“计划执行者所在位置”是Makefile中的显式数据,所以“make”可以考虑它。在shell脚本中实现类似的效果将需要很大的努力。
Makefile非常易于扩展,由于使用编写Makefile的特殊视角,即“无害观察”的另一个结果,我们可以轻而易举地对其进行扩展。例如,如果我们决定所有数据库I/O样板代码都应由自动工具编写,我们只需要在Makefile中编写哪些文件应该被自动工具用作输入来编写样板代码。没有多余的东西,也没有少的。我们可以在几乎任何地方添加此描述,make将无论如何获取它。在shell脚本中进行这样的扩展将比必要的困难。这种可扩展性的易用性是Makefile代码重用的巨大激励。

顺便提一下,我对问题“如何编写Makefile以在目标和源之间使用不同的目录”的回答可能会对OP有所帮助。 - Michaël Le Barbier

6

对于我来说,以下是众多原因中排名前两位的重要原因:

  • 您可以同时编译多个源文件,从而缩短构建时间
  • 如果您更改了一个文件,则只需重新编译该文件,而不必重新编译所有文件

4
好的,“每次都编译所有内容”的论点可以在这里看到:http://xkcd.com/303/。但开玩笑的是,与每次重新编译相比,当你做出小的更改时,只编译一个文件要快得多。我的Pascal编译器项目不是很大,但编译它还需要大约35秒左右。使用make -j3(即同时运行3个编译作业,我目前正在使用仅有双核处理器的备用计算机),编译文件所需的时间仅少了十秒钟,但如果没有多个编译作业,就不能执行-j 3。重新编译其中一个较大的模块只需要16秒。我知道我宁愿等待16秒还是35秒...

3
我将解释为什么制作对象文件可以使编译速度更快。
考虑以下类比:
您正在学习从头开始构建汽车(即您有许多金属和橡胶用于车轮)。 您拥有制作汽车所有主要部件所需的所有机器(框架,车轮,发动机等)。 假设每台机器都建造汽车的一个特定部分,例如假设您有一个“frame-building machine”。 假设准备一台机器需要很长时间,因为您必须阅读非常复杂的手册:(
情景1 你花了半天时间准备所有机器,然后花了一个小时来制造零件并将所有零件焊接在一起以完成制作汽车。 然后关闭您使用的所有机器(这将删除您的所有自定义设置)。 然而,之后您意识到您制造了错误的引擎。 由于您已经焊接了整个汽车的所有部件,因此无法替换以前的引擎,因此您必须重新制造所有零件。 您又花了半天时间准备机器(通过重新阅读手册),再花一个小时制造所有零件并将它们组合在一起。 痛苦!
情景2 您在半天时间内准备好机器,然后在笔记本电脑中记录下您准备机器时所做的一切。 您在一个小时内制造了所有汽车零部件,并将所有零部件焊接在一起以完成制作汽车并关闭机器。 然而,之后您意识到您为汽车制造了错误的引擎。 您必须重新制造所有零件。 因为您在笔记本电脑中记录了自己做过的所有工作,所以现在只需要花费10分钟即可准备好机器。 您再次花费一个小时来制造零件并将其组合在一起。 这节省了您大量时间(几乎半天)。
您阅读的复杂手册是“源文件”。 您带有笔记的笔记本电脑是“对象文件”。 最终的汽车是“二进制”文件。
对象文件是中间结果,它们通过以适当的形式跟踪您已经完成的大部分艰苦工作,帮助您避免重新进行所有编译(准备机器)以生成二进制文件。 对象文件还有更多用途! 如果您对此感到兴奋,请阅读有关它们的文章:D。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接