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


138

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

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


后续问题:

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


2
如果要编辑单个文件,请仅签出lzz(www.lazycplusplus.com)。
理查德·科登

Answers:


105

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

答案是C ++不需要。如果将所有内容都标记为内联(对于类定义中定义的成员函数而言,这始终是自动的),则无需进行分隔。您可以在头文件中定义所有内容。

您可能分开的原因是:

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

如果您更笼统的问题是“为什么C ++与Java不一样?”,那么我不得不问,“为什么用C ++而不是Java?” ;-p

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

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

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

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

它不会,至少在编译使用头文件的代码时不会。您要链接的函数甚至都不需要编写,不必介意编译器知道.cpp它们将在哪个文件中。调用代码在编译时需要知道的所有内容都在函数声明中表示。在链接时,您将提供.o文件列表,静态或动态库,并且有效的头文件保证了函数的定义将在某个地方。


3
要添加到“您可能想要分离的原因是:”头文件的最重要功能是:要将代码结构设计与实现分离,因为:A.当您进入涉及许多对象的非常复杂的结构时,它在头文件中进行筛选并记住它们是如何协同工作的,要容易得多,并以头文件注释为补充。B.让一个人不负责定义所有对象结构,而其他人则负责实现,这使事情保持组织。总的来说,我认为这使复杂的代码更具可读性。
Andres Canella 2012年

以最简单的方式,我可以想到头文件与cpp文件分离的用处是将接口与实现分开,这对于中/大型项目确实有帮助。
克里希纳(Krishna Oza)

我打算将其包含在(2)中,“在没有定义的情况下链接到代码”。好的,因此您可能仍可以使用其中的定义访问git repo,但要点是,您可以将代码与其依赖项的实现分开编译,然后进行链接。如果从字面上看您想要的只是将接口/实现分成不同的文件,而不必考虑分别构建,那么只需将foo_interface.h包含foo_implementation.h即可实现。
史蒂夫·杰索普

4
@AndresCanella不,不是。它使阅读和维护自己的代码成为噩梦。为了完全理解代码中的内容,您需要跳过2n个文件而不是n个文件。这不是Big-Oh表示法,2n与n相比有很大的不同。
Błażej米哈利克

1
我第二个说法是,标题可以帮忙。例如,检查minix源,很难跟踪从哪里开始到控制权的传递,在哪里声明/定义的事情..如果它是通过分离的动态模块构建的,则可以通过理解一件事然后跳转到它来进行消化。依赖模块。取而代之的是,您需要遵循标头,这使得读取以这种方式编写的任何代码都变得非常糟糕。相反,nodejs在没有任何ifdef的情况下可以清楚地知道源于何处,并且您可以轻松地确定源于何处。
德米特里

91

C ++之所以这样做,是因为C是那样做的,所以真正的问题是C为什么要那样做?维基百科对此有些话。

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


13
+1击中头部的指甲。这确实不需要冗长的解释。
MSalters

6
它并没有触动我的头:(我仍然必须探究C ++为什么必须使用前向声明,为什么它不能识别源文件中的标识符并直接从动态库符号中读取,以及C ++为什么这样做只是因为C那样做:p
亚历山大·泰勒

3
这样您就可以成为一个更好的程序员@AlexanderTaylor :)
唐纳德·伯德

66

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

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

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

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

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

但是,针对C ++ 0x的建议之一是添加一个适当的模块系统,从而允许将类似于.NET或Java的代码编译为更大的模块,这些操作一次完成就没有头文件。该提议并未在C ++ 0x上有所作为,但我认为它仍属于“我们希望以后再做”类别。也许在TR2或类似产品中。


这是页面上的最佳答案。谢谢!
Chuck Le Butt

29

据我(有限的-我通常不是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 ++中-为什么要改变那些没有被破坏的东西?


5
为什么编译器无法遍历代码并找到所有函数定义?似乎很容易将其编程到编译器中。
Marius

3
如果您来源,而您通常没有。编译后的C ++实际上是机器代码,仅具有足够的附加信息来加载和链接代码。然后,将CPU指向入口点,然后使其运行。这从根本上不同于Java或C#,在Java或C#中,代码被编译为中间字节码,其中包含其内容中的元数据。
DevSolar

我猜想在1972年,对于编译器而言,这可能是一个相当昂贵的操作。
Michael Stum

正是迈克尔·斯图姆所说的。定义此行为后,只有一个翻译单元才能进行线性扫描,这是唯一可以实际实现的方法。
jalf

3
是的-用磁带大量折磨在16位苦味上进行编译并非易事。
MSalters

11

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

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


1
您是否暗示,例如在C#中,“您必须将源文件包括在其他源文件中”?因为显然你没有。对于第二个优点,我认为这与语言有关:您不会在例如Delphi中使用.h文件
Vlagged

无论如何,您都必须重新编译整个项目,那么第一个优势真的很重要吗?
马吕斯2009年

好的,但是我不认为这是语言功能。在定义“问题”之前处理C声明更为实际。就像有人说“那不是功能的错误” :)
Neuro

@Marius:是的,确实很重要。链接整个项目不同于编译和链接整个项目。随着项目中文件数量的增加,将它们全部编译变得非常烦人。@Vlagged:您是对的,但我没有将C ++与另一种语言进行比较。我比较了仅使用源文件与使用源和头文件。
erelender

C#在其他文件中不包含源文件,但是您仍然必须引用模块-这使编译器获取源文件(或反映为二进制文件)以解析代码使用的符号。
gbjbaanb

5

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

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

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

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


我同意你的看法。我有类似的想法在这里:stackoverflow.com/questions/3702132/...
smwikipedia

4

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

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

C ++的要点是不断发展,是现有实践的平滑延续,仅添加新功能而不会(经常)破坏对其用户社区有效的工作。

这意味着与其他语言相比,它使某些事情(特别是对于开始新项目的人员)更加困难,并且使某些事情(尤其是对于维护现有代码的人们)更加轻松。

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


您如何集成C#和C ++?通过COM?
彼得·莫滕森

1
有三种主要方法,“最佳”方法取决于您现有的代码。我已经用完了这三个。我使用最多的是COM,因为我现有的代码已经围绕它进行了设计,因此它实际上是无缝的,对我来说效果很好。在某些奇怪的地方,我使用C ++ / CLI可以在没有COM接口的任何情况下提供令人难以置信的平滑集成(即使您有COM接口,您可能更喜欢使用现有的COM接口)。最后是p / invoke,它基本上可以让您调用从DLL公开的任何类似C的函数,因此可以直接从C#调用任何Win32 API。
丹尼尔·艾威克

4

嗯,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文件,而不是使用它的所有文件。当然,如果您在头文件中放入了很多实现,那么它的用处就会减少。



1

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

实际上,头文件在首次检查程序时变得非常有用,签出头文件(仅使用文本编辑器)可以使您大致了解程序的体系结构,这与其他语言不同,在其他语言中,您必须使用复杂的工具来查看类和他们的成员职能。



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 ++用户带来的另一个负担...


1

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

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.