【问题标题】:Why are forward declarations necessary? [duplicate]为什么需要前向声明? [复制]
【发布时间】:2010-04-13 19:32:33
【问题描述】:

可能重复:
Should C++ eliminate header files?

在 C# 和 Java 等语言中,无需在使用之前声明(例如)类。如果我理解正确,这是因为编译器对代码进行了两次传递。第一个它只是“收集可用的信息”,第二个它检查代码是否正确。

在 C 和 C++ 中,编译器只执行一次传递,因此此时所有内容都需要可用。

所以我的问题基本上是为什么在 C 和 C++ 中不这样做。是不是就不需要头文件了?

【问题讨论】:

  • C++ 编译器将按顺序读取 [也就是说,它将从上到下读取...] 这就是语言的工作原理。您关于“通过”两次然后查看函数原型的建议是可行的,但不幸的是,语言不是这样工作的。
  • C 作为一门语言在 30 多年前就已标准化,当时底层技术的能力远不如今天,而且成本也远高于今天。一个温和的建议:尝试了解一些事物的历史,并了解自从做出这些决定以来世界已经发生了巨大的变化,并且在您在开发工作中做出选择后将继续发生变化。总有一天,有人想知道“WTF?!”关于你的决定.... ;-)
  • C++ 并不完全正确,它有一个部分 2-pass 编译器。在类声明中内联编写的类方法可以引用出现在其下方的类成员。不知道为什么他们没有保持一致。
  • Pascal 在 C 出现之前在小型计算机上很流行,它被明确设计为可以使用递归下降解析器一次性编译。

标签: c++ c compiler-construction declaration


【解决方案1】:

简短的回答是,从定义 C 的时间到 25 年后出现 Java 的时间之间,计算能力和资源呈指数级增长。

更长的答案...

编译单元的最大尺寸——编译器在单个块中处理的代码块——将受到编译计算机的内存量的限制。为了处理您键入机器代码的符号,编译器需要将所有符号保存在一个查找表中,并在代码中遇到它们时引用它们。

当 C 语言于 1972 年创建时,计算资源更加稀缺且价格昂贵 - 一次存储复杂程序的整个符号表所需的内存在大多数系统中根本不可用。固定存储也很昂贵,而且速度极慢,因此像虚拟内存或将部分符号表存储在磁盘上这样的想法根本不允许在合理的时间范围内进行编译。

该问题的最佳解决方案是通过让人工提前在哪些编译单元中对符号表的哪些部分进行分类,将代码分成更小的部分。向程序员强加一个相当小的任务来声明他使用什么,从而节省了让计算机在整个程序中搜索程序员可以使用的任何东西的巨大努力。

它还使编译器不必对每个源文件进行两次传递:第一次对其中的所有符号进行索引,第二次对引用进行解析并查找它们。当您处理磁带时,查找时间以秒为单位,读取吞吐量以每秒字节数(不是千字节或兆字节)为单位,这非常有意义。

C++ 是在将近 17 年后创建的,它被定义为 C 的超集,因此必须使用相同的机制。

到 1995 年 Java 出现时,普通计算机已经拥有足够的内存来保存符号表,即使对于一个复杂的项目,也不再是一个沉重的负担。而且 Java 的设计初衷并不是为了向后兼容 C,所以它不需要采用遗留机制。 C# 也同样不受阻碍。

因此,他们的设计者选择将划分符号声明的负担从程序员身上转移到计算机上,因为它的成本与编译总工作量的比例是最小的。

【讨论】:

  • 优秀的总结。让我想起了在 2 个软盘驱动器 640K 的 PC 上编译 C 程序的“美好时光” - 花了大约 10 分钟,换了六个或更多软盘。对于一个包含不超过几百条语句的程序来说,所有这些!并以为我在天堂里拥有所有的力量。
  • 很好的答案,对于我们这些年轻人来说,了解一些历史观点总是很好的!
【解决方案2】:

底线:编译器技术的进步使得前向声明变得不必要了。此外,计算机的速度要快数千倍,因此可以进行必要的额外计算来处理缺少前向声明的情况。

C 和 C++ 较旧,并且在需要节省每个 CPU 周期的时候进行了标准化。

【讨论】:

  • :-) 换句话说 - C# 比 C++ 更好。
  • 您在这里遗漏了关键词:向后兼容性。您的最后一行听起来像是 C 和 C++ 只有一个版本的石器时代标准。它应该是“并且是首先标准化的......并且为了保持向后兼容性,方法保持不变。” @Franci:当你用 C# 编写完操作系统后,来找我。
  • @Franci:不......换句话说,现代语言编译器已经过时了前向声明,因为他们不必担心向后兼容性。它可以在 C++ 中完成。享受在 C# 萌芽中编写硬件驱动程序的乐趣。
  • @GMan - Save the Unicorns:你对“第一个标准化”和“向后兼容性”有很好的看法。关于 C# 中的操作系统:我给你Singularity。现在,诚然,内核的一些性能关键部分是用 C 编写的,但考虑到内核的某些部分经常用汇编编写,我想说它们已经进步了一点。
  • @Vlad:谁在乎?作为一名程序员,我不关心它是如何完成的,我关心的是我不必编写前向声明。当这成为性能问题时,请告诉我。
【解决方案3】:

不,它不会排除头文件。它将消除使用标头在同一文件中声明类/函数的要求。标题的主要原因是 not 在同一个文件中声明事物。头文件的主要原因是声明在其他文件中定义的东西。

无论好坏,C(和 C++)的语义规则都要求“单程”风格的行为。举个例子,考虑这样的代码:

int i;

int f() { 
     i = 1;
     int i = 2;
}

i=1 分配给全局不是f() 内部定义的那个。这是因为在分配时,尚未看到 i 的本地定义,因此未将其考虑在内。您仍然可以使用两遍编译器遵循这些规则,但这样做可能并非易事。我还没有检查过他们的规范以确定是否知道,但我的直接猜测是 Java 和 C# 在这方面与 C 和 C++ 不同。

编辑:由于评论说我的猜测不正确,我做了一些检查。根据 Java 语言参考第 14.4.2 节,Java 似乎遵循了与 C++ 几乎相同的规则相当(略有不同,但不是很多。

至少在我阅读C# language specification(警告:Word 文件)时,它不同的。它(第 3.7.1 节)说:“在局部变量声明(第 8.5.1 节)中声明的局部变量的范围是发生声明的块。”

这似乎是说,在 C# 中,局部变量应该在声明它的整个 整个 块中可见,因此使用类似于我给出的示例的代码,赋值将是局部变量,而不是全局变量。

所以,我猜对了一半:Java 遵循(在这方面与 C++ 几乎相同的规则,但 C# 没有。

【讨论】:

  • 你的猜测是错误的。
【解决方案4】:

这是因为 C/C++ 中的编译模块较小。在 C/C++ 中,每个 .c/.cpp 文件都是单独编译的,创建一个 .obj 模块。因此编译器需要在其他编译模块中声明的类型和变量的信息。此信息以前向声明的形式提供,通常在头文件中。

另一方面,C# 将多个 .cs 文件一次编译成一个大的编译模块。

事实上,当从 C# 程序中引用不同的编译模块时,编译器需要知道声明(类型名称等),就像 C++ 编译器一样。这些信息是直接从编译的模块中获得的。在 C++ 中,相同的信息被显式分离(这就是为什么您无法从 C++ 编译的 DLL 中找到变量名,但可以从 .NET 程序集中确定它的原因)。

【讨论】:

  • 不幸的是,这并不能解释 C# 如何管理包含数千个源文件和数百万行代码的 100 汇编解决方案,而不是 C++ 管理单个 .h 文件(可能仍需要转发声明,即使它需要的所有信息都在一个文件中)。
  • @Jason:单独编译的好处是不同的:当您只更改实现时,您的重新编译几乎是即时的。 (当然这会使第一次编译变慢。)我不知道为什么你的 C++ 不能管理单个头文件,我从来没有遇到过任何问题。
  • @Vlad:您说“...编译器需要有关类型的信息...在其他编译模块中声明”,但实际上,即使是单个头文件中的单个 C++ 类也可以 需要预先声明 - 即即使在一个编译模块中。也就是说,C++ 以线性方式解析代码(因此需要在引用未来类型时预先声明它们),而 C# 有效地为代码库构建了一个数据库,允许它随机访问所有类型。
  • @Jason:你是对的。这有时是一种优势,有时不是。例如,C# 需要在所有 .cs 文件发生任何变化时立即重新编译它们,因为它无法提前看到哪个模块需要哪些信息。在 C++ 中,如果没有更改标头,则只需要重新编译已更改的 .cpp 文件。另一方面,C++ 要求开发人员了解他需要包含哪些标头。
  • @Jason:有时需要预先声明,因为 C++ 在 1 遍中读取输入。因此,为了引用稍后在同一文件中声明的内容,您需要进行前向声明。但是,对于一个体面的开发人员来说,我不认为这是一个复杂的问题。
【解决方案5】:

C++ 中的前向声明是一种向编译器提供有关当前编译的源代码可能使用的其他代码片段的元数据的方法,因此它可以生成正确的代码。

该元数据可以来自链接库/组件的作者。但是,它也可以自动生成(例如,有一些工具可以为 COM 对象生成 C++ 头文件)。无论如何,表达元数据的 C++ 方式是通过您需要包含在源代码中的头文件。

C#/.Net 在编译时也会使用类似的元数据。但是,该元数据是在构建它适用的程序集时自动生成的,并且通常嵌入其中。因此,当您在 C# 项目中引用程序集时,您实际上是在告诉编译器“请在此程序集中查找您需要的元数据”。

换句话说,C# 中的元数据生成和使用对开发人员来说更加透明,让他们能够专注于真正重要的事情——编写自己的代码。

将有关代码的元数据与程序集捆绑在一起还有其他好处。反射、代码发射、动态序列化 - 它们都依赖于元数据才能在运行时生成正确的代码。

与此类似的 C++ 将是 RTTI,尽管由于实现不兼容而没有被广泛采用。

【讨论】:

    【解决方案6】:

    来自 Eric Lippert,C# 内部所有事物的博主:http://blogs.msdn.com/ericlippert/archive/2010/02/04/how-many-passes.aspx

    C# 语言不需要这样 声明发生在用法之前, 这有两个影响,再次,对 用户和编译器编写器。 [...]

    对编译器编写者的影响是 我们必须有一个“两次通过” 编译器。在第一遍中,我们看 用于声明和忽略主体。 一旦我们收集了所有 声明中的信息 我们会从标题中得到 C++,我们第二次通过 代码并为 身体。

    总而言之,使用某些东西不需要在 C# 中声明它,而在 C++ 中则需要。这意味着在 C++ 中,您需要显式声明事物,并且使用头文件这样做更方便和安全,因此您不会违反 One Definition Rule

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-06-13
      • 2016-08-21
      • 2019-10-10
      • 1970-01-01
      相关资源
      最近更新 更多