为什么有头文件和.cpp文件?[关闭]


483

为什么C ++具有头文件和.cpp文件?



它是一个常见的OOP范例,.h是一个类声明,而cpp是定义。一个人不需要知道它是如何实现的,他/她只应该知道接口。
Manish Kakati

这是c ++将接口与实现分开的最佳部分。我们将接口分开,而不是将所有代码都保存在一个文件中总是好事。总有一些代码,例如内联函数,它们是头文件的一部分。看到头文件时,该外观很好,显示了已声明的函数和类变量的列表。
Miank

有时候,头文件对于编译至关重要,而不仅仅是组织偏好设置或分发预编译库的方式。假设您有一个game.c取决于physics.c和math.c的结构;physics.c也取决于math.c。如果您包括.c文件而永远忘记.h文件,则您将拥有math.c中的重复声明,并且没有编译的希望。这对我来说最有意义的是为什么标头文件很重要。希望它可以帮助别人。
Samy Bencherif

我认为这与以下事实有关:扩展中仅允许使用字母数字字符。我什至不知道那是真的,只是猜测
user12211554

Answers:


201

好吧,主要原因是要从实现中分离接口。标头声明一个类(或正在实现的任何东西)将做什么,而cpp文件定义了它将如何执行那些功能。

这减少了依赖性,因此使用标头的代码不一定需要了解实现的所有细节以及仅为此所需的任何其他类/标头。当实现中的某些内容发生更改时,这将减少编译时间,并减少所需的重新编译量。

它不是完美的,通常您会使用Pimpl Idiom之类的技术来正确分离接口和实现,但这是一个好的开始。


177
不是真的 标头仍然包含实现的主要部分。自从什么时候私有实例变量是类接口的一部分?私人成员职能?那么,他们在公开可见的标头中到底在做什么?它与模板之间的距离越来越远。
jalf

13
这就是为什么我说它不完美,并且需要Pimpl习惯用法来实现更多分离。模板是完全不同的蠕虫病毒-即使大多数编译器都完全支持“ exports”关键字,它还是语法上的糖,而不是真正的分隔符。
Joris Timmermans

4
其他语言如何处理呢?例如-Java?Java中没有头文件概念。
Lazer 2010年

8
@Lazer:Java更易于解析。Java编译器可以在不知道其他文件中所有类的情况下解析文件,并在以后检查类型。在C ++中,许多构造在没有类型信息的情况下都是模棱两可的,因此C ++编译器需要有关引用类型的信息来解析文件。这就是为什么它需要标题。
Niki 2010年

15
@nikie:“轻松”解析与它有什么关系?如果Java的语法至少与C ++一样复杂,它仍然可以只使用Java文件。无论哪种情况,C怎么办?C易于解析,但同时使用标头和c文件。
Thomas Eding

609

C ++编译

C ++的编译分为两个主要阶段:

  1. 第一个是将“源”文本文件编译为二进制“对象”文件:CPP文件是已编译文件,并且在不了解其他CPP文件(甚至库)的情况下进行编译,除非通过原始声明或标头包含。CPP文件通常被编译为.OBJ或.O“对象”文件。

  2. 第二个是将所有“对象”文件链接在一起,从而创建最终的二进制文件(库或可执行文件)。

HPP在所有这些过程中适合什么位置?

可怜的寂寞CPP文件...

每个CPP文件的编译都独立于所有其他CPP文件,这意味着如果A.CPP需要B.CPP中定义的符号,例如:

// A.CPP
void doSomething()
{
   doSomethingElse(); // Defined in B.CPP
}

// B.CPP
void doSomethingElse()
{
   // Etc.
}

它不会编译,因为A.CPP无法知道“ doSomethingElse”的存在...除非A.CPP中有声明,例如:

// A.CPP
void doSomethingElse() ; // From B.CPP

void doSomething()
{
   doSomethingElse() ; // Defined in B.CPP
}

然后,如果您有使用相同符号的C.CPP,则可以复制/粘贴声明...

复制/粘贴警告!

是的,有问题。复制/粘贴很危险,并且难以维护。这意味着如果我们有某种方法可以不复制/粘贴并仍然声明符号,那将很酷……我们该怎么做?通过包含一些文本文件,通常以.h,.hxx,.h ++为后缀,或者,我偏爱C ++文件的.hpp:

// B.HPP (here, we decided to declare every symbol defined in B.CPP)
void doSomethingElse() ;

// A.CPP
#include "B.HPP"

void doSomething()
{
   doSomethingElse() ; // Defined in B.CPP
}

// B.CPP
#include "B.HPP"

void doSomethingElse()
{
   // Etc.
}

// C.CPP
#include "B.HPP"

void doSomethingAgain()
{
   doSomethingElse() ; // Defined in B.CPP
}

include工作如何?

从本质上讲,包括一个文件将解析然后将其内容粘贴到CPP文件中。

例如,在以下代码中,带有A.HPP标头:

// A.HPP
void someFunction();
void someOtherFunction();

...来源B.CPP:

// B.CPP
#include "A.HPP"

void doSomething()
{
   // Etc.
}

...包含后将变为:

// B.CPP
void someFunction();
void someOtherFunction();

void doSomething()
{
   // Etc.
}

一件事-为什么在B.CPP中包含B.HPP?

在当前情况下,这是不需要的,并且B.HPP具有doSomethingElse函数声明,而B.CPP具有doSomethingElse函数定义(本身就是声明)。但是在更一般的情况下,如果B.HPP用于声明(和内联代码),则可能没有相应的定义(例如,枚举,普通结构等),因此,如果B.CPP需要使用include。使用来自B.HPP的那些声明。总而言之,默认情况下,包含源头是“好习惯”。

结论

因此,头文件是必需的,因为C ++编译器无法单独搜索符号声明,因此,您必须通过包括这些声明来提供帮助。

最后一句话:您应该在HPP文件的内容周围放置标题防护,以确保多个包含内容不会破坏任何内容,但是总而言之,我相信上面已经解释了存在HPP文件的主要原因。

#ifndef B_HPP_
#define B_HPP_

// The declarations in the B.hpp file

#endif // B_HPP_

甚至更简单

#pragma once

// The declarations in the B.hpp file

2
@nimcap::You still have to copy paste the signature from header file to cpp file, don't you?不需要。只要CPP“包括” HPP,预编译器就会自动将HPP文件的内容复制粘贴到CPP文件中。我更新了答案以澄清这一点。
paercebal 2012年

7
@鲍勃:While compiling A.cpp, compiler knows the types of arguments and return value of doSomethingElse from the call itself。不,不是。它只知道用户提供的类型,这将有一半的时间甚至不会花时间读取返回值。然后,发生隐式转换。然后,当您拥有代码:时foo(bar),您甚至无法确定它foo是一个函数。因此,编译器必须有权访问头文件中的信息,以决定源代码是否正确编译。然后,一旦编译了代码,链接器将链接函数调用。
paercebal

3
@Bob:[继续] ...现在,我想链接器可以完成编译器完成的工作,这将使您的选择成为可能。(我猜这是下一个标准的“模块”主张的主题)。Seems, they're just a pretty ugly arbitrary design.:如果确实在2012年创建了C ++。但是请记住,C ++是在1980年代建立在C之上的,当时的约束条件是完全不同的(IIRC,出于采用目的,决定保持与C相同的链接器)。
paercebal

1
@paercebal感谢您的解释和注意,paercebal!我为什么不能确定那foo(bar)是一个函数-如果它是作为指针获得的呢?实际上,谈到糟糕的设计,我指责C,而不是C ++。我真的不喜欢纯C的一些约束,例如具有头文件或使函数返回一个且只有一个值,同时在输入上采用多个参数(让输入和输出以相似的方式运行是不自然的, ;为什么
要有

1
@Bobo::Why can't I be sure, that foo(bar) is a functionfoo可能是一种类型,因此您将有一个称为的类构造函数。In fact, speaking of bad design, I blame C, not C++:我可以在很多事情上归咎于C,但是在70年代进行设计不会是其中之一。再次,那个时间的约束... such as having header files or having functions return one and only one value:元组可以帮助减轻这种情况,以及通过引用传递参数。现在,检索返回的多个值的语法是什么,更改语言是否值得?
paercebal

93

因为C的概念起源于30年,而那时C是将多个文件中的代码链接在一起的唯一可行方法。

如今,这是一个可怕的骇客,它完全破坏了C ++中的编译时间,导致无数不必要的依赖(因为头文件中的类定义暴露了太多有关实现的信息),等等。


3
我想知道为什么头文件(或编译/链接实际需要的文件)不是简单地“自动生成”的?
Mateen Ulhaq,

54

因为在C ++中,最终的可执行代码不包含任何符号信息,所以它或多或少是纯机器代码。

因此,您需要一种描述一段代码界面的方法,该方法与代码本身是分开的。该描述在头文件中。


16

因为C ++从C继承了它们。不幸的是。


4
为什么不幸的是从C继承C ++?
Lokesh

3
@Lokesh由于其行李:(
陈力

1
这怎么可能是答案?
Shuvo Sarker

13
@ShuvoSarker,因为已经有成千上万种语言进行了演示,但是对于C ++而言,没有技术性的说明使程序员两次编写函数签名。答案为“为什么?” 是“历史”。
鲍里斯(Boris)

15

因为设计库格式的人不希望“浪费”一些很少使用的信息,例如C预处理器宏和函数声明。

由于您需要该信息来告诉编译器“链接器完成其工作后,此功能才可用”,因此他们不得不拿出另一个文件来存储此共享信息。

C / C ++之后的大多数语言都将此信息存储在输出中(例如Java字节码),或者根本不使用预编译格式,总是以源代码形式分发并即时编译(Python,Perl)。


循环引用无效。即,您无法在从b.cpp构建b.lib之前从a.cpp构建a.lib,但是您也不能在a.lib之前构建b.lib。
MSalters

19
Java解决了这个问题,Python可以做到,任何现代语言都可以做到。但是在发明C的时候,RAM是如此的昂贵和稀缺,这不是一个选择。
亚伦·迪古拉

6

这是声明接口的预处理器方式。您将接口(方法声明)放入头文件中,并将实现放入cpp中。使用您的库的应用程序只需要知道接口,就可以通过#include访问该接口。


4

通常,您将需要定义接口而不必交付整个代码。例如,如果您有一个共享库,则将附带一个头文件,该头文件定义了共享库中使用的所有功能和符号。如果没有头文件,则需要发送源代码。

在单个项目中,至少将IMHO头文件用于两个目的:

  • 明确性,即通过将接口与实现分开,可以更轻松地读取代码
  • 编译时间。通过仅在可能的地方使用接口,而不是完整的实现,可以减少编译时间,因为编译器可以简单地引用该接口,而不必解析实际的代码(理想情况下,只需完成此操作即可)一次)。

3
库供应商为什么不能只运送生成的“头”文件?不含预处理器的“头”文件应具有更好的性能(除非实现被真正破坏了)。
Tom Hawtin-大头钉

我认为这与头文件是生成还是手写无关,问题不在于“人们为什么自己编写头文件?”,而是“我们为什么要有头文件”。预处理程序可用的标头也是如此。当然,这会更快。

-5

回应MadKeithV的回答

这减少了依赖性,因此使用标头的代码不一定需要了解实现的所有细节以及仅为此所需的任何其他类/标头。这将减少编译时间,并且当实现中的某些内容发生更改时也将减少重新编译的次数。

另一个原因是,标头为每个类赋予唯一的ID。

所以如果我们有类似的东西

class A {..};
class B : public A {...};

class C {
    include A.cpp;
    include B.cpp;
    .....
};

当我们尝试构建项目时,由于A是B的一部分,所以我们会遇到错误,并且使用标头可以避免这种麻烦……

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.