【问题标题】:Why does C++ need a separate header file?为什么 C++ 需要单独的头文件?
【发布时间】:2023-03-15 20:16:01
【问题描述】:

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

C++ 于 1998 年被批准,为什么要这样设计呢?单独的头文件有什么好处?


跟进问题:

当我只包含 .h 文件时,编译器如何找到其中包含代码的 .cpp 文件?它是假定 .cpp 文件与 .h 文件同名,还是它实际上会查看目录树中的所有文件?

【问题讨论】:

标签: c++ language-design


【解决方案1】:

它不需要单独的头文件,其功能与 main 中的相同。仅当您使用多个代码文件开发应用程序并且使用先前未声明的函数时才需要它。

这确实是一个范围问题。

【讨论】:

    【解决方案2】:

    第一个优势:如果您没有头文件,则必须在其他源文件中包含源文件。这将导致包含文件在包含文件更改时重新编译。

    第二个优势:它允许在不同单位(不同的开发人员、团队、公司等)之间共享接口而不共享代码。

    【讨论】:

    • 你是在暗示那个,例如在 C# 中'你必须在其他源文件中包含源文件'?因为显然你没有。对于第二个优点,我认为这太依赖于语言:您不会在例如使用 .h 文件。德尔福
    • 反正你要重新编译整个项目,那么第一个优势真的算吗?
    • 好的,但我认为这不是语言功能。在定义“问题”之前处理 C 声明更为实际。这就像有人说的“这不是一个错误,而是一个特性”:)
    • @Marius:是的,这很重要。链接整个项目不同于编译和链接整个项目。随着项目中文件数量的增加,编译所有文件真的很烦人。 @Vlagged:你是对的,但我没有将 c++ 与另一种语言进行比较。我比较了仅使用源文件与使用源文件和头文件。
    • C# 不包含其他源文件,但您仍然必须引用模块 - 这使得编译器获取源文件(或反映到二进制文件中)以解析您的代码的符号使用。
    【解决方案3】:

    C++ 于 1998 年被批准,为什么要这样设计呢?单独的头文件有什么好处?

    实际上头文件在第一次检查程序时变得非常有用,检查头文件(仅使用文本编辑器)可以让您了解程序的体系结构,这与其他必须使用复杂工具的语言不同查看类及其成员函数。

    【讨论】:

      【解决方案4】:

      C++ 这样做是因为 C 是这样做的,所以真正的问题是为什么 C 这样做呢? Wikipedia 对此说了一点。

      较新的编译语言(例如 Java、C#) 不使用 forward 声明;标识符是 从源代码自动识别 文件并直接从动态读取 库符号。这意味着标题 不需要文件。

      【讨论】:

      • +1 一针见血。这真的不需要冗长的解释。
      • 它没有击中我的头 :( 我仍然需要查找为什么 C++ 必须使用前向声明以及为什么它不能识别源文件中的标识符并直接从动态库中读取符号,以及为什么 C++ 那样做只是因为 C 那样做:p
      • 你是一个更好的程序员,因为你这样做了@AlexanderTaylor :)
      【解决方案5】:

      我认为头文件背后的真正(历史)原因是让编译器开发人员更容易......但是,头文件确实具有优势。
      更多讨论请查看this previous post...

      【讨论】:

        【解决方案6】:

        您似乎在询问如何将定义与声明分开,尽管头文件还有其他用途。

        答案是 C++ 并不“需要”这个。如果您将所有内容标记为内联(对于在类定义中定义的成员函数,这无论如何都是自动的),那么就不需要分离。您可以在头文件中定义所有内容。

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

        1. 缩短构建时间。
        2. 在没有定义源的情况下链接代码。
        3. 为了避免将所有内容标记为“内联”。

        如果您更一般的问题是“为什么 C++ 与 Java 不同?”,那么我不得不问,“您为什么要编写 C++ 而不是 Java?” ;-p

        不过,更严重的是,原因是 C++ 编译器不能像 javac 那样直接进入另一个翻译单元并弄清楚如何使用它的符号。需要头文件向编译器声明它在链接时可以使用的内容。

        所以#include 是一个直接的文本替换。如果您在头文件中定义所有内容,预处理器最终会为项目中的每个源文件创建一个巨大的副本和粘贴,并将其输入编译器。 C++ 标准在 1998 年获得批准的事实与此无关,而是 C++ 的编译环境如此紧密地基于 C 的事实。

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

        编译器如何找到包含代码的.cpp文件

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

        【讨论】:

        • 添加到“你可能想要分离的原因是:”&我认为头文件最重要的功能是:将代码结构设计与实现分离,因为:A.当你进入涉及许多对象的真正复杂的结构更容易筛选头文件并记住它们如何协同工作,并由您的头文件补充。 B. 一个人不负责定义所有对象结构,而另一个人负责实现,它使事情井井有条。总的来说,我认为它使复杂的代码更具可读性。
        • 以最简单的方式,我认为头文件与 cpp 文件分离的用处是将接口与实现分开,这对大中型项目真正有帮助。
        • @AndresCanella 不,它没有。它使阅读和维护非您自己的代码成为一场噩梦。要完全理解代码中的某些内容,您需要跳过 2n 个文件而不是 n 个文件。这不是 Big-Oh 符号,与仅 n 相比,2n 有很大不同。
        • 我认为标题有帮助的谎言。例如,检查 minix 源代码,很难跟踪它从哪里开始传递控制,在哪里声明/定义事物。如果它是通过分离的动态模块构建的,那么通过理解一件事然后跳转到它是可以消化的一个依赖模块。相反,您需要关注标题,它会使阅读以这种方式编写的任何代码地狱。相比之下,nodejs 无需任何 ifdef 就可以清楚地知道什么来自哪里,并且您可以轻松识别出什么来自哪里。
        • “你为什么要写 C++ 而不是 [x]”。我们写 C++ 不是因为我们想要,我们写 C++ 是因为我们必须:P
        【解决方案7】:

        嗯,你可以在没有头文件的情况下完美地开发 C++。事实上,一些大量使用模板的库不使用头文件/代码文件范例(参见 boost)。但是在 C/C++ 中你不能使用没有声明的东西。一种实用的方法 处理就是使用头文件。另外,您无需共享代码/实现即可获得共享界面的优势。而且我认为 C 的创建者没有想到它:当您使用共享头文件时,您必须使用著名的:

        #ifndef MY_HEADER_SWEET_GUARDIAN
        #define MY_HEADER_SWEET_GUARDIAN
        
        // [...]
        // my header
        // [...]
        
        #endif // MY_HEADER_SWEET_GUARDIAN
        

        这并不是真正的语言特性,而是一种处理多重包含的实用方法。

        所以,我认为在创建 C 时,前向声明的问题被低估了,现在当使用像 C++ 这样的高级语言时,我们必须处理这类事情。

        我们可怜的 C++ 用户的另一个负担 ...

        【讨论】:

          【解决方案8】:

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

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

          void SomeFunction() {
              SomeOtherFunction();
          }
          
          void SomeOtherFunction() {
              printf("What?");
          }
          

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

          void SomeOtherFunction();
          
          void SomeFunction() {
              SomeOtherFunction();
          }
          
          void SomeOtherFunction() {
              printf("What?");
          }
          

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

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

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

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

          那是 C。 C++ 引入了类和私有/公共修饰符,因此尽管您仍然可以询问是否需要它们,但 C++ AFAIK 仍然需要在使用它们之前声明函数。此外,许多 C++ 开发人员现在或曾经也是 C 开发人员,并将他们的概念和习惯转移到 C++ - 为什么要更改未损坏的内容?

          【讨论】:

          • 为什么编译器不能跑通代码,找到所有的函数定义?这似乎很容易编程到编译器中。
          • 如果您源,而您通常没有。编译的 C++ 是有效的机器代码,只有足够的附加信息来加载和链接代码。然后,将 CPU 指向入口点,让它运行。这与 Java 或 C# 根本不同,在 Java 或 C# 中,代码被编译成包含其内容元数据的中间字节码。
          • 是的 - 用磁带海量存储在 16 苦味上进行编译并非易事。
          • @Puddle 我不认为这是真正的原因,因为在 70 年代开发 C 时,共享源代码是常态而不是例外。我相信这是因为随机访问文件并不容易——当时,使用磁带很普遍,因此只能通过文件前进,永远不会向后或跳跃来编译语言。 .h 文件似乎是一种向前推进声明的好方法,而不会引入更大的冲突实现。
          • @MichaelStum 但为什么呢?他们为什么要保留它?语言是关于理解程序员所写内容的目的。每个人都可以理解如何根据所有类创建标题。如果它实际上什么都不做,只是帮助 c++ 编译,那将是一项毫无意义的任务。我们已经继续前进,如果它什么都不做,可以让它自动化。如果它没有其他用途...
          【解决方案9】:

          如果您希望编译器自动找出其他文件中定义的符号,您需要强制程序员将这些文件放在预定义的位置(如 Java 包结构决定项目的文件夹结构)。我更喜欢头文件。此外,您需要使用您使用的库的来源或某种统一的方式将编译器所需的信息放入二进制文件中。

          【讨论】:

            【解决方案10】:

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

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

            C++ 的重点是进化的,是现有实践的平稳延续,只添加新功能而不会(过于频繁地)破坏对其用户社区足够有效的东西。

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

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

            【讨论】:

            • 如何集成 C# 和 C++?通过 COM?
            • 主要有三种方式,“最好的”取决于你现有的代码。这三个我都用过。我使用最多的是 COM,因为我现有的代码已经围绕它设计,所以它实际上是无缝的,对我来说效果很好。在一些奇怪的地方,我使用 C++/CLI,它为您还没有 COM 接口的任何情况提供了令人难以置信的平滑集成(即使您有 COM 接口,您也可能更喜欢使用现有的 COM 接口)。最后是 p/invoke,它基本上可以让您调用从 DLL 公开的任何类 C 函数,因此可以让您直接从 C# 调用任何 Win32 API。
            【解决方案11】:

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

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

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

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

            【讨论】:

            【解决方案12】:

            有些人认为头文件是一种优势:

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

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

            为了向后兼容,C++ 保留了这个系统。

            今天,这毫无意义。它效率低下、容易出错且过于复杂。如果是目标的话,还有更好的方法来分离接口和实现。

            然而,C++0x 的其中一项建议是添加一个适当的模块系统,允许将代码编译为类似于 .NET 或 Java 的更大模块,一次完成且无需标头。这个提议在 C++0x 中没有被淘汰,但我相信它仍然属于“我们愿意稍后再做”的类别。也许在 TR2 或类似版本中。

            【讨论】:

            • 这是页面上的最佳答案。谢谢!
            • 这个答案应该被接受,因为它真正解释了为什么 C++ 是这样设计的,而不是“为什么你可能想要分离”
            • 我喜欢这个。可用性应始终放在首位。我希望这是 C++ 的发展方向。
            • C++20: modules
            【解决方案13】:

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

            头文件主要是为了实现文件的单独编译,减少依赖关系。

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

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

            当然,如果 bar.h 中的函数在我尝试链接我的程序时没有正文,那么它不会链接,我会得到一个错误。

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

            另外,如果你在 *.cpp 文件中改变你的代码的实现,但根本不改变头文件,你只需要编译 *.cpp 文件而不是所有使用它的东西。当然,如果你把大量的实现放到头文件中,那么这个就变得没那么有用了。

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多