为什么C++需要单独的头文件?

Mar*_*ius 124 c++ language-design

我从来没有真正理解为什么C++需要一个单独的头文件,其功能与.cpp文件相同.它使创建类和重构它们变得非常困难,并且它为项目添加了不必要的文件.然后是必须包含头文件的问题,但必须明确检查它是否已被包含.

C++于1998年获得批准,那为什么这样设计呢?单独的头文件有哪些优点?


跟进问题:

当我包含的所有内容都是.h文件时,编译器如何找到带有代码的.cpp文件?是否假设.cpp文件与.h文件具有相同的名称,或者它是否实际查看目录树中的所有文件?

Ste*_*sop 99

您似乎要求将定义与声明分开,尽管头文件还有其他用途.

答案是C++并不"需要"这个.如果您将所有内联标记(无论如何对于类定义中定义的成员函数都是自动的),则不需要分离.您只需在头文件中定义所有内容即可.

您可能想要分开的原因是:

  1. 改善构建时间.
  2. 在没有定义源的情况下链接代码.
  3. 避免将所有内容标记为"内联".

如果您更普遍的问题是"为什么C++与Java不相同?",那么我不得不问,"为什么要编写C++而不是Java?" ;-p

但更严重的是,原因是C++编译器不能直接进入另一个翻译单元,并以javac可以做到的方式找出如何使用它的符号.需要头文件来向编译器声明在链接时可以获得的内容.

#include是一个直接的文本替代.如果在头文件中定义所有内容,预处理器最终会在项目中创建每个源文件的大量复制和粘贴,并将其提供给编译器.C++标准在1998年被批准的事实与此无关,这是因为C++的编译环境与C语言的编译环境密切相关.

转换我的评论以回答您的后续问题:

编译器如何找到带有代码的.cpp文件

它没有,至少在它编译使用头文件的代码时没有.您要链接的函数甚至不需要编写,更不用说编译器知道.cpp它们将在哪个文件中.调用代码在编译时需要知道的所有内容都在函数声明中表示.在链接时,您将提供.o文件列表,或静态或动态库,有效的标头是一个承诺,函数的定义将在某处.

  • @AndresCanella不,不.它使阅读和维护不是你自己的代码成为一场噩梦.要完全理解代码中的内容,您需要跳过2n个文件而不是n个文件.这只是不是Big-Oh表示法,2n与n相比有很大的不同. (4认同)
  • “你为什么写 C++ 而不是 [x]”。我们编写 C++ 不是因为我们想这样做,我们编写 C++ 是因为我们必须这样做:P (4认同)
  • 添加到"您可能想要分离的原因是:"我认为头文件最重要的功能是:将代码结构设计与实现分开,因为:A.当您进入涉及许多对象的非常复杂的结构时更容易筛选头文件并记住它们如何协同工作,并由标题注释补充.B.让一个人不负责定义所有的对象结构,而其他一些人则负责实现它保持组织有序.总之,我认为它使复杂的代码更具可读性. (2认同)
  • 我认为这是标题帮助的谎言。例如,检查 minix 源代码,很难跟踪它从哪里开始到哪里传递控制,在哪里声明/定义事物..如果它是通过单独的动态模块构建的,那么通过理解一件事然后跳转到一个依赖模块。相反,您需要遵循标头,它会使阅读以这种方式编写的任何代码变得地狱。相比之下,nodejs 在没有任何 ifdef 的情况下清楚地说明了什么来自哪里,并且您可以轻松识别出什么来自哪里。 (2认同)

Don*_*yrd 83

C++就是这样做的,因为C就是这样做的,所以真正的问题是为什么C这样做呢?维基百科对此有所说明.

较新的编译语言(如Java,C#)不使用前向声明; 标识符自动从源文件中识别,并直接从动态库符号中读取.这意味着不需要头文件.

  • +1击中头部的钉子.这真的不需要冗长的解释. (9认同)
  • 它没有打我的头钉:(我还是要回去看看C++为什么必须使用前置声明,为什么它不能从源文件识别标识,并直接从动态读取库符号,为什么C++做的是方式只是因为C这样做:p (5认同)
  • 你是一个更好的程序员@AlexanderTaylor :) (2认同)

jal*_*alf 57

有些人认为头文件是一个优点:

  • 声称它启用/强制/允许分离接口和实现 - 但通常情况并非如此.头文件充满了实现细节(例如,类的成员变量必须在头中指定,即使它们不是公共接口的一部分),并且函数可以并且通常类声明中内联定义在标题中,再次破坏了这种分离.
  • 有时可以说它可以改善编译时间,因为每个翻译单元都可以独立处理.然而,在编译时,C++可能是最慢的语言.部分原因是同一标题中的许多重复包含.多个翻译单元包含大量标题,需要对它们进行多次解析.

最终,标题系统是设计C时70年代的神器.那时,计算机的内存非常少,将整个模块保留在内存中并不是一种选择.编译器必须从顶部开始读取文件,然后线性地完成源代码.标头机制实现了这一点.编译器不必考虑其他转换单元,只需从上到下读取代码即可.

C++保留了这个系统以实现向后兼容.

今天,没有任何意义.它效率低,容易出错且过于复杂.如果是目标,那么有更好的方法来分离界面和实现.

但是,C++ 0x的提议之一是添加一个适当的模块系统,允许将代码编译为类似于.NET或Java,编译成更大的模块,一次性完成,无需头文件.这个提议没有在C++ 0x中得到削减,但我相信它仍然在"我们以后喜欢这样做"的类别.也许在TR2或类似的.

  • 这个答案应该是公认的,因为它真正解释了为什么 C++ 是这样设计的,而不是“为什么你可能想要分离” (7认同)
  • C++20:[模块](https://en.cppreference.com/w/cpp/language/modules) (7认同)
  • 这是该页面上的最佳答案。谢谢你! (4认同)
  • 我喜欢这个。可用性应始终放在首位。我希望这就是 C++ 的发展方向。 (2认同)

Mic*_*tum 26

对于我(有限 - 我通常不是C开发人员)的理解,这是根植于C.记住C不知道什么类或命名空间,它只是一个长程序.此外,必须在使用它们之前声明函数.

例如,以下应该给出编译器错误:

void SomeFunction() {
    SomeOtherFunction();
}

void SomeOtherFunction() {
    printf("What?");
}
Run Code Online (Sandbox Code Playgroud)

错误应该是"未声明SomeOtherFunction",因为您在声明之前调用它.解决这个问题的一种方法是将SomeOtherFunction移到SomeFunction上方.另一种方法是首先声明函数签名:

void SomeOtherFunction();

void SomeFunction() {
    SomeOtherFunction();
}

void SomeOtherFunction() {
    printf("What?");
}
Run Code Online (Sandbox Code Playgroud)

这让编译器知道:在代码中的某处,有一个名为SomeOtherFunction的函数返回void并且不接受任何参数.因此,如果您尝试调用SomeOtherFunction的代码,请不要惊慌,而是去寻找它.

现在,假设您在两个不同的.c文件中有SomeFunction和SomeOtherFunction.然后你必须在Some.c中#include"SomeOther.c".现在,向SomeOther.c添加一些"私有"函数.由于C不知道私有函数,因此Somec中也可以使用该函数.

这是.h文件的来源:它们指定要从.c文件中"导出"的所有函数(和变量),可以在其他.c文件中访问.这样,您获得了类似公共/私人范围的东西.此外,您可以将此.h文件提供给其他人而无需共享源代码 - .h文件也可以对编译的.lib文件起作用.

因此,主要原因是为了方便,源代码保护以及在应用程序的各个部分之间进行一些解耦.

那是C.C++引入了类和私有/公共修饰符,因此当您仍然可以询问是否需要它们时,C++ AFAIK仍然需要在使用它们之前声明函数.此外,许多C++开发人员也是C devleopers,并将他们的概念和习惯接管到C++ - 为什么要改变未破坏的东西?

  • 为什么编译器无法运行代码并找到所有函数定义?看起来很容易编程到编译器中. (5认同)
  • 如果您*拥有*您通常没有的来源.编译的C++实际上是机器代码,只有足够的附加信息来加载和链接代码.然后,将CPU指向入口点,然后让它运行.这与Java或C#根本不同,后者将代码编译为包含其内容元数据的中间字节码. (3认同)
  • 是的 - 使用磁带质量torage编译16 bitter是非常重要的. (2认同)
  • @Puddle 我认为这不是真正的原因,因为在 70 年代开发 C 时,共享源代码是常态而不是例外。我相信这是因为随机访问文件并不容易——当时,使用磁带很常见,所以语言只能通过文件前进来编译,永远不会向后或跳来跳去。.h 文件似乎是一种很好的方式来推进声明,而不会引入更大的冲突实现。 (2认同)

ere*_*der 10

第一个优点:如果您没有头文件,则必须在其他源文件中包含源文件.这将导致包含文件在包含的文件更改时再次编译.

第二个优点:它允许共享接口,而无需在不同的单元(不同的开发人员,团队,公司等)之间共享代码.

  • 您是否暗示,例如在 C# 中“您必须将源文件包含在其他源文件中”?因为显然你不知道。对于第二个优点,我认为这太依赖于语言:你不会在例如 Delphi 中使用 .h 文件 (2认同)

Mar*_*sky 6

嗯,C++ 于 1998 年获得批准,但它的使用时间比这更长,而且批准主要是确定当前的使用情况,而不是强加结构。因为 C++ 是基于 C 的,而且 C 有头文件,所以 C++ 也有头文件。

头文件的主要原因是启用文件的单独编译,并最大限度地减少依赖关系。

假设我有 foo.cpp,我想使用 bar.h/bar.cpp 文件中的代码。

我可以在 foo.cpp 中 #include "bar.h",然后编程和编译 foo.cpp,即使 bar.cpp 不存在。头文件向编译器承诺 bar.h 中的类/函数将在运行时存在,并且它已经拥有它需要知道的一切。

当然,如果当我尝试链接我的程序时 bar.h 中的函数没有主体,那么它不会链接并且我会收到错误消息。

一个副作用是您可以在不泄露源代码的情况下为用户提供头文件。

另一种情况是,如果您更改了 *.cpp 文件中代码的实现,但根本不更改标头,则只需编译 *.cpp 文件而不是使用它的所有内容。当然,如果你把大量的实现放到头文件中,那么这就没那么有用了。


Dan*_*ker 5

C++ 旨在将现代编程语言功能添加到 C 基础结构中,而不会不必要地更改与 C 语言本身无关的任何内容。

是的,在这一点上(第一个 C++ 标准发布 10 年后,它的使用量开始大幅增长 20 年后),很容易问为什么它没有合适的模块系统。显然,当今设计的任何新语言都不会像 C++ 那样工作。但这不是 C++ 的重点。

C++ 的要点是进化,是现有实践的顺利延续,只添加新功能,而不会(经常)破坏对其用户社区来说足够有效的东西。

这意味着与其他语言相比,它使某些事情变得更困难(特别是对于开始新项目的人而言),而使某些事情变得更容易(特别是对于那些维护现有代码的人而言)。

因此,与其期望 C++ 变成 C#(这毫无意义,因为我们已经有了 C#),为什么不直接选择适合工作的工具呢?我自己,努力用现代语言编写大量新功能(我碰巧使用 C#),并且我将大量现有的 C++ 保留在 C++ 中,因为重写它没有真正的价值全部。无论如何,它们集成得非常好,所以基本上是无痛的。

  • 如何集成 C# 和 C++?通过COM? (2认同)
  • 主要有三种方法,“最佳”取决于您现有的代码。三个我都用过。我最常用的是 COM,因为我现有的代码是围绕它设计的,所以它几乎是无缝的,对我来说效果很好。在一些奇怪的地方,我使用 C++/CLI,它为您还没有 COM 接口的任何情况提供了令人难以置信的平滑集成(并且您可能更喜欢它而不是使用现有的 COM 接口,即使您确实有它们)。最后还有 p/invoke,它基本上可以让您调用从 DLL 公开的任何类似 C 的函数,因此可以让您直接从 C# 调用任何 Win32 API。 (2认同)

Voi*_*ter 5

对头文件的需求源于编译器用于了解其他模块中的函数和/或变量的类型信息的限制.已编译的程序或库不包含编译器绑定到其他编译单元中定义的任何对象所需的类型信息.

为了弥补这一限制,C和C++允许声明,并且这些声明可以在预处理器的#include指令的帮助下包含在使用它们的模块中.

另一方面,Java或C#等语言包括在编译器输出(类文件或程序集)中绑定所需的信息.因此,不再需要维护模块的客户端包含独立声明.

绑定信息未包含在编译器输出中的原因很简单:在运行时不需要它(在编译时进行任何类型检查).这只会浪费空间.请记住,C/C++来自可执行文件或库的大小确实很重要的时间.