Why have header files and .cpp files?
为什么C ++有头文件和.cpp文件?
C ++编译
C ++中的编译分为两个主要阶段:
第一种是将"源"文本文件编译成二进制"对象"文件:CPP文件是编译文件,编译时不知道其他CPP文件(甚至库),除非通过原始声明或标题包含。 CPP文件通常编译为.OBJ或.O"对象"文件。
第二个是将所有"对象"文件链接在一起,从而创建最终的二进制文件(库或可执行文件)。
HPP在哪里适合所有这些过程?
糟糕的寂寞CPP档案......
每个CPP文件的编译独立于所有其他CPP文件,这意味着如果A.CPP需要在B.CPP中定义的符号,例如:
1 2 3 4 5 6 7 8 9 10 11 | // A.CPP void doSomething() { doSomethingElse(); // Defined in B.CPP } // B.CPP void doSomethingElse() { // Etc. } |
它不会编译因为A.CPP无法知道"doSomethingElse"存在...除非A.CPP中有声明,例如:
1 2 3 4 5 6 7 | // A.CPP void doSomethingElse() ; // From B.CPP void doSomething() { doSomethingElse() ; // Defined in B.CPP } |
然后,如果您有使用相同符号的C.CPP,则复制/粘贴声明...
COPY / PASTE ALERT!
是的,有一个问题。复制/粘贴是危险的,难以维护。这意味着如果我们有一些方法可以不复制/粘贴,并且仍然声明??符号,那将会很酷......我们怎么能这样做?通过包含一些文本文件,通常后缀为.h,.hxx,.h ++或者我喜欢的C ++文件,.hpp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | // 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 } |
实质上,包含文件将解析然后将其内容复制粘贴到CPP文件中。
例如,在以下代码中,使用A.HPP标头:
1 2 3 | // A.HPP void someFunction(); void someOtherFunction(); |
......来源B.CPP:
1 2 3 4 5 6 7 | // B.CPP #include"A.HPP" void doSomething() { // Etc. } |
......将在纳入之后成为:
1 2 3 4 5 6 7 8 | // B.CPP void someFunction(); void someOtherFunction(); void doSomething() { // Etc. } |
一件小事 - 为什么在B.CPP中包括B.HPP?
在当前情况下,这不是必需的,并且B.HPP具有
结论
因此头文件是必需的,因为C ++编译器无法单独搜索符号声明,因此,您必须通过包含这些声明来帮助它。
最后一句话:您应该在HPP文件的内容周围放置标题保护,以确保多个包含不会破坏任何内容,但总而言之,我认为HPP文件存在的主要原因如上所述。
1 2 3 4 5 6 | #ifndef B_HPP_ #define B_HPP_ // The declarations in the B.hpp file #endif // B_HPP_ |
那么,主要原因是将接口与实现分离。标题声明"什么"类(或正在实现的任何内容)将执行,而cpp文件定义它将如何执行这些功能。
这减少了依赖性,因此使用头的代码不一定需要知道实现的所有细节以及仅为此所需的任何其他类/头。这将减少编译时间以及实现中的某些内容更改时所需的重新编译量。
它并不完美,你通常会采用像Pimpl Idiom这样的技术来正确分离界面和实现,但这是一个好的开始。
因为C这个概念起源于30年前,当时它是将多个文件中的代码链接在一起的唯一可行方法。
今天,这是一个非常糟糕的黑客,完全破坏了C ++中的编译时间,导致无数的不必要的依赖(因为头文件中的类定义暴露了太多关于实现的信息),等等。
因为在C ++中,最终的可执行代码不携带任何符号信息,所以它或多或少都是纯机器代码。
因此,您需要一种方法来描述一段代码的接口,该代码与代码本身是分开的。此描述位于头文件中。
因为C ++从C继承了它们。不幸的是。
因为设计库格式的人不想为C预处理器宏和函数声明等很少使用的信息"浪费"空间。
由于您需要该信息告诉编译器"当链接器正在执行其工作时此函数可用",因此他们必须提供第二个文件,以便存储此共享信息。
C / C ++之后的大多数语言都将这些信息存储在输出中(例如,Java字节码),或者它们根本不使用预编译格式,总是以源代码形式分发并动态编译(Python,Perl)。
它是声明接口的预处理器方式。您将接口(方法声明)放入头文件,并将实现放入cpp。使用您的库的应用程序只需要知道接口,他们可以通过#include访问它。
通常,您需要具有接口的定义,而无需发送整个代码。例如,如果您有一个共享库,则可以随附一个头文件,该文件定义共享库中使用的所有函数和符号。如果没有头文件,则需要发送源代码。
在单个项目中,使用头文件,恕我直言,至少有两个目的:
- 清晰度,即通过将接口与实现分开,更容易阅读代码
- 编译时间。通过尽可能使用接口,而不是完整的实现,编译时间可以减少,因为编译器可以简单地引用接口而不必解析实际的代码(理想情况下,只需要完成一次)。
回应MadKeithV的回答,
This reduces dependencies so that code that uses the header doesn't
necessarily need to know all the details of the implementation and any
other classes/headers needed only for that. This will reduce
compilation times, and also the amount of recompilation needed when
something in the implementation changes.
另一个原因是标题为每个类提供唯一的id。
所以,如果我们有类似的东西
1 2 3 4 5 6 7 8 | class A {..}; class B : public A {...}; class C { include A.cpp; include B.cpp; ..... }; |
当我们尝试构建项目时,我们会有错误,因为A是B的一部分,有标题我们会避免这种头痛......