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

时间:2014-09-24 20:56:40

标签: c++ makefile

使用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 个答案:

答案 0 :(得分:12)

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

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

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

shell脚本的过程范例对于类似编译的作业

是错误的

编写Makefile类似于编写一个稍微改变透视的shell脚本。在shell脚本中,我们描述了一个问题的过程解决方案:我们可以使用未定义的函数以非常抽象的术语开始描述整个过程,并且我们会对这个描述进行优化,直到我们达到最基本的描述级别,其中一个过程就是一个普通的shell命令。在Makefile中,我们不介绍任何抽象,但我们关注的是我们想要生成的文件以及如何生成它们。这很好用,因为在UNIX中,一切都是文件,因此每个处理都是由程序完成的,它从输入文件中读取输入数据,做一些计算并写出结果在一些输出文件中

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

无害的观察 描述“计划执行者所在的位置”的数据正是已经准备好的中间文件集,这正是在我们写Makefiles。

这种无害的观察实际上是shell脚本和Makefile之间的概念差异,它解释了Makefile在编译作业和类似作业中相对于shell脚本的所有优点。当然,要充分理解这些优点,我们必须编写正确的 Makefile,这对初学者来说可能很难。

Make可以轻松地继续执行

处的中断任务

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

Make可以轻松使用项目的多个版本

您发现Makefile会使源文件与对象文件混乱。但Makefiles实际上可以进行参数化以将这些目标文件存储在专用目录中,而高级Makefile允许我们同时拥有包含具有不同选项的项目的多个构建的多个目录。 (例如,启用了不同的功能,或调试版本等)这也是无害观察的结果,Makefiles实际上围绕着一组中间文件进行了清晰。

Make可以轻松实现构建的并行化

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

Makefile易于扩展

由于特殊视角 - 也就是无害观察的另一个结果 - 用于编写Makefile,我们可以轻松扩展它们。例如,如果我们决定所有数据库I / O样板代码都应该由自动工具编写,我们只需要在Makefile中写入自动工具用作编写样板代码的输入的文件。没有更少,仅此而已。我们可以在我们喜欢的地方添加这个描述,make无论如何都会得到它。在shell脚本构建中执行此类扩展将比必要更难。

这种可扩展性易用性是Makefile代码重用的一个很好的动力。

答案 1 :(得分:5)

在我列出众多原因的最重要的两个主要原因是:

  • 您可以在减少构建时间的同时编译多个源文件
  • 如果更改了一个文件,则只重新编译该文件而不是重新编译所有文件

答案 2 :(得分:4)

嗯,这里可以看到“每次编译所有内容”的论点:

http://xkcd.com/303/

但是开玩笑说,与每次重新编译所有内容相比,在进行少量更改后编译一个文件的速度要快得多。我的Pascal编译器项目不是很大,但编译它仍然需要大约35秒左右。

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

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

我知道我宁愿等待16或35秒......

答案 3 :(得分:2)

我将解释为什么制作目标文件会导致更快的编译速度。

考虑以下类比:

您正在学习从头开始建造汽车(即,车轮有很多金属和橡胶)。您拥有制造汽车所有主要零件(车架,车轮,引擎等)所需的所有机器。假设每台机器都制造汽车的一个特定部分,例如说您有一台框架制造机。假设准备一台机器需要花费大量时间,因为您必须阅读一本非常复杂的手册:(

场景1 您花了半天时间尝试准备好所有机器,然后一个小时来制造零件并将所有零件焊接在一起以完成汽车的制造。然后,关闭所有使用过的计算机(这将删除所有自定义设置)。但是,后来您意识到自己为汽车制造了错误的发动机。由于您已将汽车的所有零件焊接在一起,因此您无法更换以前的发动机,因此必须重新制造所有零件。您需要花半天的时间来准备好机器(重新阅读手册),再花一个小时的时间来制作所有零件并加入它们。痛苦的!

方案2 您可以在半天之内准备好机器,然后记下在笔记本中准备机器时所做的一切。您可以在一个小时内完成汽车的所有零件,然后将所有零件焊接在一起以完成汽车的制造并关闭机器。但是,后来您意识到自己为汽车安装了错误的发动机。您必须重新制作所有零件。 因为您一直跟踪在笔记本上所做的所有事情,所以现在只需10分钟即可完成机器的准备工作。您再次花费一个小时来制作零件并将它们连接在一起。这为您节省了大量时间(将近半天)。

  • 您阅读的复杂手册是源文件
  • 带有笔记的笔记本是目标文件
  • 最终的汽车是 binary 文件。

目标文件是中间结果,通过跟踪在制作二进制文件之前以适当的形式完成的大部分艰苦工作,可以帮助您避免再次进行所有编译(使机器准备就绪)。目标文件有更多用途!如果这使您兴奋,则应阅读有关它们的信息:D。