为什么有头文件和.cpp文件?

464 c++ header-files

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

pae*_*bal 582

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.
}
Run Code Online (Sandbox Code Playgroud)

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

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

void doSomething()
{
   doSomethingElse() ; // Defined in B.CPP
}
Run Code Online (Sandbox Code Playgroud)

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

COPY/PASTE ALERT!

是的,有一个问题.复制/粘贴是危险的,难以维护.这意味着如果我们有一些方法可以不复制/粘贴,并且仍然声明​​符号,那将会很酷......我们怎么做呢?通过包含一些文本文件,通常后缀为.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
}
Run Code Online (Sandbox Code Playgroud)

include工作怎么样?

实质上,包含文件将解析然后将其内容复制粘贴到CPP文件中.

例如,在以下代码中,使用A.HPP标头:

// A.HPP
void someFunction();
void someOtherFunction();
Run Code Online (Sandbox Code Playgroud)

......来源B.CPP:

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

void doSomething()
{
   // Etc.
}
Run Code Online (Sandbox Code Playgroud)

......将在纳入之后成为:

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

void doSomething()
{
   // Etc.
}
Run Code Online (Sandbox Code Playgroud)

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

在当前情况下,这不是必需的,并且B.HPP具有doSomethingElse函数声明,并且B.CPP具有doSomethingElse函数定义(其本身是声明).但在更一般的情况下,B.HPP用于声明(和内联代码),可能没有相应的定义(例如,枚举,普通结构等),因此如果B.CPP可能需要包含使用B.HPP的声明.总而言之,一个源默认包含它的标题是"好品味".

结论

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

最后一句话:您应该在HPP文件的内容周围放置标题保护,以确保多个包含不会破坏任何内容,但总而言之,我认为HPP文件存在的主要原因如上所述.

#ifndef B_HPP_
#define B_HPP_

// The declarations in the B.hpp file

#endif // B_HPP_
Run Code Online (Sandbox Code Playgroud)

  • @Bob:`在编译A.cpp时,编译器知道参数的类型并从调用本身返回doSomethingElse的值.不,它没有.它只知道用户提供的类型,一半的时间,甚至都不会费心去读取返回值.然后,发生隐式转换.然后,当你有代码:`foo(bar)`时,你甚至不能确定`foo`是一个函数.因此,编译器必须能够访问头中的信息以确定源是否正确编译...然后,一旦代码被编译,链接器就会将函数调用链接在一起. (6认同)
  • 为什么你不能把 B.CPP 包括到 A.CPP 中? (3认同)
  • @nimcap:`你仍然需要将签名从头文件复制粘贴到cpp文件,不是吗?`:不需要.只要CPP"包含"HPP,预编译器就会自动将HPP文件内容的复制粘贴到CPP文件中.我更新了答案以澄清这一点. (2认同)
  • 谢谢,您的复制/粘贴概念很有帮助。但是你的观点“它不会编译,因为 A.cpp 无法知道“doSomethingElse”存在”在我看来是错误的。在编译 A.cpp 时,编译器从调用本身知道 doSomethingElse 的参数类型和返回值;它可以假设在另一个模块中定义了 doSomethingElse 并依赖链接器来填充依赖项(如果找不到它的定义或参数类型/返回值在 A.cpp 和 B.cpp 中不兼容,则返回错误)。我仍然没有得到标题的必要性。看起来,它们只是一个非常丑陋的任意设计。 (2认同)
  • @Bob:[继续] ...现在,链接器可以完成编译器完成的工作,我猜,这将使您的选项成为可能.(我想这是下一个标准的"模块"命题的主题)."看起来,它们只是一个非常丑陋的任意设计."如果C++是在2012年创建的,的确如此.但是请记住,C++是在20世纪80年代基于C构建的,当时的约束条件差别很大(IIRC,为了采用目的而决定保留与C相同的链接器). (2认同)

Jor*_*ans 195

那么,主要原因是将接口与实现分离.标题声明"什么"类(或正在实现的任何内容)将执行,而cpp文件定义它将如何执行这些功能.

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

它并不完美,你通常会采用像Pimpl Idiom这样的技术来正确分离界面和实现,但这是一个好的开始.

  • 不是真的.标题仍然包含实现的主要部分.从什么时候私有实例变量成为类接口的一部分?私人会员职能?然后他们在公开可见的标题中做了什么呢?它与模板进一步分开. (167认同)
  • @nikie:解析的"缓解"与它有什么关系?如果Java的语法至少和C++一样复杂,那么它仍然只能使用java文件.在任何一种情况下,C呢?C很容易解析,但同时使用头文件和c文件. (14认同)
  • 这就是为什么我说它并不完美,而Pimpl成语需要更多的分离.模板是一个完全不同的蠕虫 - 即使大多数编译器完全支持"exports"关键字,它仍然是我的语法糖而不是真正的分离. (12认同)
  • @Lazer:Java更易于解析.Java编译器可以在不知道其他文件中的所有类的情况下解析文件,并在以后检查类型.在C++中,许多构造都是模糊的,没有类型信息,因此C++编译器需要有关引用类型的信息来解析文件.这就是它需要标题的原因. (8认同)
  • 其他语言如何处理?例如 - Java?Java中没有头文件概念. (4认同)
  • 在Java中,更强烈地支持"接口"的概念.在我看来,这些接口是"头"文件的原始想法的进一步发展 - 包含*仅*'什么'. (2认同)
  • @Lazer:Java不仅解析起来更简单,而且没有模板(是的,它有泛型,但那些甚至不在运行中). (2认同)

jal*_*alf 89

因为C这个概念起源于30年前,当时它是将多个文件中的代码链接在一起的唯一可行方法.

今天,这是一个非常糟糕的黑客,完全破坏了C++中的编译时间,导致无数的不必要的依赖(因为头文件中的类定义暴露了太多关于实现的信息),等等.

  • 我想知道为什么头文件(或编译/链接实际需要的任何文件)不是简单地“自动生成”? (7认同)
  • 它早于 K&R C。在此之前几乎所有语言都使用相同的范例,但像 Pascal 这样的语言是一个例外,它具有称为“单元”的特殊编译单元,该单元既是标头又是实现,而主单元称为“程序”。这一切都是为了将​​程序划分为可由编译器管理的代码片段,并减少编译时间\允许增量编译。 (2认同)

unw*_*ind 53

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

因此,您需要一种方法来描述一段代码的接口,该代码与代码本身是分开的.此描述位于头文件中.


Aar*_*lla 14

因为设计库格式的人不想为C预处理器宏和函数声明等很少使用的信息"浪费"空间.

由于您需要该信息告诉编译器"当链接器正在执行其工作时此函数可用",因此他们必须提供第二个文件,以便存储此共享信息.

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

  • Java解决了这个问题,Python可以做到,任何现代语言都可以做到.但是在C被发明的时候,RAM是如此昂贵和稀缺,它只是不是一种选择. (18认同)

and*_*ref 14

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

  • @ShuvoSarker,因为已经有成千上万种语言进行了演示,但是对于C ++来说,没有技术性的说明使程序员两次编写函数签名。答案为“为什么?” 是“历史”。 (8认同)
  • @Lokesh 因为它的行李:( (4认同)
  • 为什么从C继承C++是不幸的? (3认同)
  • 这怎么可能是答案? (3认同)

Mar*_*wis 6

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


小智 5

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

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

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

  • 库供应商为什么不能只运送生成的“头”文件?不含预处理器的“头”文件应具有更好的性能(除非实现确实被破坏)。 (2认同)