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

时间:2009-08-20 12:44:34

标签: c++ language-design

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

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


跟进问题:

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

13 个答案:

答案 0 :(得分:95)

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

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

您可能希望分开的原因是:

  1. 改善构建时间。
  2. 在没有定义来源的情况下链接代码。
  3. 避免将所有内容标记为“内联”。
  4. 如果您更普遍的问题是“为什么C ++与Java不相同?”,那么我不得不问,“为什么要编写C ++而不是Java?” ;-p

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

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

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

      

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

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

答案 1 :(得分:80)

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

  

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

答案 2 :(得分:53)

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

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

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

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

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

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

答案 3 :(得分:26)

对于我(有限 - 我通常不是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 devleopers并将他们的概念和习惯接管到C ++ - 为什么要改变未被破解的东西?

答案 4 :(得分:10)

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

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

答案 5 :(得分:5)

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

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

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

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

答案 6 :(得分:4)

C ++旨在为C基础架构添加现代编程语言功能,而不会不必要地改变任何与C语言无关的C语言。

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

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

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

因此,不要期望C ++变成C#(因为我们已经拥有C#而没有意义),为什么不选择合适的工具呢?我自己,我努力用现代语言编写重要的新功能块(我碰巧使用C#),并且我在C ++中保留了大量现有的C ++,因为重写它没有实际价值所有。无论如何,它们整合得非常好,所以它在很大程度上是无痛的。

答案 7 :(得分:3)

它不需要具有与main相同功能的单独头文件。如果您使用多个代码文件开发应用程序并且使用之前未声明的函数,则只需要它。

这确实是一个范围问题。

答案 8 :(得分:3)

嗯,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文件而不是使用它的所有内容。当然,如果你将大量的实现放入头文件中,那么这就变得不那么有用了。

答案 9 :(得分:1)

  

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

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

答案 10 :(得分:1)

我认为头文件背后的真实(历史)原因使编译器开发人员更容易......但是,头文件会带来好处。
请查看this previous post以获取更多讨论...

答案 11 :(得分:1)

好吧,你可以完美地开发没有头文件的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 ++用户的另一个负担......

答案 12 :(得分:1)

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