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

C++ 单独创建对象文件,然后在Makefile中将它们链接在一起的目的是什么?,c++,makefile,C++,Makefile,当使用gcc编译器时,它将在一个步骤中链接和编译。然而,将源文件转换为目标文件,然后在最后链接它们似乎是惯用的做法。对我来说,这似乎没有必要。这不仅会用一堆对象文件把目录弄得乱七八糟,而且会使Makefile变得复杂,因为您可以简单地将所有源文件添加到编译器中。例如,以下是我认为简单的: .PHONY: all SOURCES = $(wildcard *.cpp) all: default default: g++ $(SOURCES) -o test 它巧妙地变成: g++ m

当使用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
。假冒:全部
源=$(通配符*.cpp)
对象=$(源:.cpp=.o)
%.o:%.cpp
g++-c-o$@$<
全部:默认值
默认值:$(对象)
g++-o测试$^
清洁:
rm-rf*.o
g++-c-o main.o main.cpp
g++-c-o test.o test.cpp
g++-o测试main.o测试.o

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

对我来说,在众多原因中,最重要的两个原因是:

  • 您可以同时编译多个源文件,以减少生成时间
  • 如果更改了一个文件,则只需重新编译该文件,而不是重新编译所有文件
那么,“每次编译所有内容”的论点可以在这里看到:

但开玩笑的是,与每次重新编译所有文件相比,在做了一点小改动后编译一个文件要快得多。我的Pascal编译器项目不是很大,但编译它仍然需要大约35秒

使用
make-j3
(即同时运行3个编译作业,我目前使用的是只有一个双核处理器的备用计算机),编译文件的时间只需少10秒,但如果没有多个编译作业,则无法执行
-j3

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


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

为什么要编写Makefile而不是编写简单的shell脚本?在您认为简单的示例中,您不使用任何特性,甚至可以编写一个简单的shell脚本,它可以理解关键字<代码> Bug 和<代码> Cube < /C> >,就是这样!p> 您实际上是在质疑编写Makefiles而不是shell脚本的意义,我将在回答中对此进行说明

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

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

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

无害观察描述“计划执行者在哪里”的数据正是已经准备好的中间文件集,这正是我们编写Makefiles时明确显示的数据

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

Make使中断的任务在其所在位置继续变得容易 当我们用Makefile描述编译作业时,我们可以很容易地中断它并在以后继续它。这是无害观察的结果。类似的效果只有在shell脚本中付出相当大的努力才能实现

Make使得使用项目的多个构建变得容易 您观察到Makefiles会将源树与对象文件混在一起。但是Makefiles实际上可以参数化,以将这些对象文件存储在专用目录中,高级Makefiles允许我们同时拥有多个目录,其中包含具有不同选项的项目的多个构建。(例如,启用了不同的功能或调试版本等)这也是一种无害的观察结果,即Makefiles实际上是围绕中间文件集进行表达的

使构建并行化变得容易 我们可以轻松地并行构建一个程序,因为这是