为什么有些C程序写在一个巨大的源文件中?


88

例如,过去的SysInternals工具“ FileMon”具有内核模式驱动程序,其源代码完全位于一个4,000行文件中。有史以来第一个ping程序也是如此(〜2,000 LOC)。

Answers:


143

使用多个文件总是需要额外的管理开销。必须设置具有单独的编译和链接阶段的构建脚本和/或makefile,确保正确管理不同文件之间的依赖关系,编写“ zip”脚本以便通过电子邮件或下载更容易地分发源代码,等等。上。今天的现代IDE通常会承担很多负担,但是我可以肯定,在编写第一个ping程序时,尚无此类IDE。而对于文件为〜4000 LOC,没有它,管理多个文件,你也这样的IDE,权衡之间的开销提及并使用多个文件的好处可能让人们做出的单个文件的方式作出决定。


9
“并且对于大约4000 LOC的文件...”我现在正在作为JS开发人员。当我的文件只有400行代码时,我会担心它的大小!(但我们的项目中有数十个文件。)
凯文(Kevin

36
@Kevin:我头上的一根头发太少了,汤里的一根头发也太多了;-) JS多个文件中的AFAIK不会像“没有现代IDE的C”中那样带来太多的管理开销。
布朗

4
@Kevin JS是完全不同的野兽。每次用户加载网站并且浏览器尚未缓存JS时,JS就会将其传输给最终用户。C只需将代码传输一次,然后另一端的人对其进行编译并保持编译状态(显然,这里有例外,但这是一般的预期用例)。C语言的东西也往往是遗留代码,就像人们在注释中描述的“ 4000行是正常的”项目一样。
法拉普

5
@Kevin现在,看看如何编写underscore.js(1700 loc,一个文件)和许多其他分布的库。实际上,就模块化和部署而言,JavaScript几乎与C一样糟糕。
Voo

2
@Pharap我认为他的意思是在部署代码之前使用Webpack之类的东西。使用Webpack,您可以处理多个文件,然后将它们编译为一个捆绑包。
Brian McCutchon

81

因为C不擅长模块化。它变得凌乱(头文件和#includes,外部函数,链接时错误等),并且引入的模块越多,它就越棘手。

更多现代语言具有更好的模块化功能,部分原因是他们从C的错误中学到了东西,并且可以更轻松地将您的代码库分解为更小,更简单的单元。但是对于C语言,避免或最小化所有麻烦可能是有益的,即使这意味着将原本会被认为过多的代码集中到一个文件中也是如此。


38
我认为将C方法描述为“错误”是不公平的。在做出决策时,它们是完全明智且合理的决定。
杰克·艾德利

14
这些模块化的东西都没有特别复杂。它可以坏的编码风格很复杂,但它不是很难理解或执行,没有它可以被归类为“错误”。根据Snowman的回答,真正的原因是过去对多个源文件进行的优化并不那么好,并且FileMon驱动程序需要高性能。另外,与OP的观点相反,这些文件不是特别大。
格雷厄姆

8
@Graham任何大于1000行代码的文件都应视为代码异味。
梅森•惠勒

11
@JackAidley一点也不不公平犯错误并不是说当时是一个合理的决定。鉴于信息不完善和时间有限,错误是不可避免的,应该从不羞耻地隐藏或重新分类以挽救面子中吸取教训。
贾里德·史密斯

8
任何声称C的方法不是错误的人都无法理解看似十行的C文件实际上如何是具有所有头文件#include:d的一万行文件。这意味着项目中的每个文件实际上至少要有一万行,而不管“ wc -l”给出的行数是多少。更好地支持模块化将很容易将解析和编译时间减少到很小的一部分。
juhist

37

除了历史原因外,在现代的性能敏感软件中也有使用它的原因。当所有代码都在一个编译单元中时,编译器可以执行整个程序的优化。使用单独的编译单元,编译器无法以某些方式(例如,内联某些代码)优化整个程序。

当然,链接程序除了可以执行编译器的功能外,还可以执行一些优化,但不是全部。例如:现代链接器非常擅长隐藏未引用的函数,即使跨多个目标文件也是如此。他们也许可以执行其他一些优化,但是没有什么比编译器可以在函数内完成的优化更重要。

SQLite是一个著名的单一源代码模块示例。您可以在“ SQLite合并”页面上阅读有关它的更多信息。

1。执行摘要

超过100个单独的源文件被合并为一个名为“ sqlite3.c”且称为“合并”的C代码大型文件。合并包含应用程序嵌入SQLite所需的所有内容。合并文件的长度超过180,000行,大小超过6兆字节。

将SQLite的所有代码组合到一个大文件中,可以使SQLite的部署更加容易-只有一个文件可以跟踪。由于所有代码都在一个翻译单元中,因此编译器可以更好地进行过程间优化,从而使机器代码的速度提高5%到10%。


15
但是请注意,现代的C编译器可以对多个源文件进行整个程序优化(尽管如果先将它们编译为单个目标文件则不能)。
戴维斯洛

10
@Davislor看一下典型的构建脚本:编译器实际上不会这样做。

4
将构建脚本更改为$(CC) $(CFLAGS) $(LDFLAGS) -o $(TARGET) $(CFILES)要比将所有内容移动到单个soudce文件要容易得多。您甚至可以将整个程序编译为传统构建脚本的替代目标,该传统构建脚本将跳过重新编译未更改的源文件,类似于人们可能会关闭生产目标的性能分析和调试。如果所有内容都放在一个大堆中,则没有这种选择。这不是人们习惯的,但是没有什么麻烦。
戴维斯洛

9
当您将代码“编译”为单独的目标文件时(取决于“编译”对您的含义),@ Davislor整个程序优化/链接时优化(LTO)也起作用。例如,GCC的LTO将在编译时将其已解析的代码表示形式添加到各个目标文件中,并且在链接时将使用该代码表示​​形式而不是(也存在)目标代码来重新编译并生成整个程序。因此,这可以与首先编译为单个目标文件的构建设置一起使用,尽管初始编译生成的机器代码将被忽略。
Dreamer

8
JsonCpp现在也这样做。关键是在开发过程中文件不是这种方式。
Lightness Races in Orbit

15

除了其他受访者提到的简单性因素外,许多C程序是由一个人编写的。

当您有一个团队时,最好将应用程序拆分为多个源文件,以避免代码更改中的不必要冲突。尤其是当有高级和非常初级的程序员从事该项目时。

当一个人独自工作时,这不是问题。

就个人而言,我基于功能使用多个文件。但这就是我。


4
@OskarSkog但是,您永远不会在将来与自己修改文件的同时进行修改。
罗伦·佩希特尔

2

因为C89没有inline功能。这意味着将文件分解为函数会导致将值压入堆栈并四处移动的开销。这在1个大switch语句(事件循环)中实现代码时增加了相当多的开销。但是,与更模块化的解决方案相比,事件循环始终要高效(甚至正确)实现起来要困难得多。因此,对于大型项目,人们仍然会选择模块化。但是,当他们预先考虑了设计并可以在1条switch语句中控制状态时,他们选择了这一点。

如今,即使在C语言中,也不必牺牲性能来模块化,因为即使C语言中的函数也可以内联。


2
在89年中,C函数的内联量可能会达到如今的水平,内联几乎不应该被使用-几乎在所有情况下,编译器都比您更懂。而且,这些4k LOC文件中的大多数都不是一个巨大的功能-这是一种可怕的编码风格,也不会带来任何明显的性能优势。
Voo

@Voo,我不知道您为什么提到编码风格。我不是在提倡它。实际上,我提到在大多数情况下由于实施不完善而导致解决方案效率较低。我还提到这是一个坏主意,因为它无法扩展(适用于较大的项目)。话虽这么说,在非常紧密的循环中(这是在接近于硬件的网络代码中发生的情况),不必要地将值压入/弹出堆栈(在调用函数时)会增加运行程序的成本。这不是一个很好的解决方案。但这是当时最好的。
德米特里·鲁巴诺维奇

2
强制性说明:inline关键字与内联优化几乎没有关系。这并不是编译器进行优化的特殊提示,而是与重复符号的链接有关。
海德

@Dmitry关键是声称由于inlineC89编译器中没有关键字无法内联,这就是为什么您必须在一个巨型函数中编写所有内容的原因。您几乎应该永远不要将其inline用作性能优化-不管怎样编译器通常都会比您更了解(并且也可以忽略该关键字)。
Voo

@Voo:一个程序员和一个编译器通常会彼此知道一些其他东西。该inline关键字连接器有关的语义这比与否进行在线优化的问题更重要,但一些实现具有其他指令在内衬控制和这样的事情有时是非常重要的。在某些情况下,一个函数看起来太大了,不值得内联,但是不断折叠可以将大小和执行时间减少到几乎没有。没有大力推动内联的编译器可能不会...
超级猫

1

这算是进化的一个例子,令我惊讶的是还没有提到。

在编程的黑暗日子里,单个文件的编译可能需要几分钟。如果程序是模块化的,那么包含必要的头文件(没有预编译的头选项)将是导致速度下降的另一个重要原因。另外,编译器可能选择/需要在磁盘本身上保留一些信息,这可能没有自动交换文件的好处。

这些环境因素导致的习惯被延续到正在进行的开发实践中,并且随着时间的推移逐渐适应。

那时,使用单个文件所获得的收益将类似于我们使用SSD而非HDD所获得的收益。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.