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

为什么c++有头文件和。cpp文件?

330221 次浏览

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

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

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

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

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

它并不完美,您通常会求助于Pimpl成语之类的技术来正确地分离接口和实现,但这是一个良好的开端。

因为设计库格式的人不想为很少使用的信息(如C预处理器宏和函数声明)“浪费”空间。

因为你需要这些信息来告诉你的编译器“当链接器完成它的工作时,这个函数稍后可用”,他们必须拿出第二个文件来存储这些共享信息。

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

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

在一个项目中,头文件至少有两个用途:

  • 清晰性,即通过保持接口与实现分离,更容易阅读代码
  • 编译时间。通过尽可能只使用接口,而不是完整的实现,可以减少编译时间,因为编译器可以简单地对接口进行引用,而不必解析实际代码(理想情况下,只需要一次完成)。

c++编译

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

  1. 第一个是“source”的编译;文本文件转换为二进制“对象”;files: 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.c p没有办法知道“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.h ppp包括在b.c cpp中?

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

结论

因此,头文件是必要的,因为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

因为C语言是这个概念的发源地,它已经有30年的历史了,在当时,它是将多个文件中的代码链接在一起的唯一可行的方法。

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

因为c++从C继承了它们。

回应MadKeithV的回答

这减少了依赖,这样使用头的代码就不会 必然需要知道实现的所有细节和任何 其他类/头只需要这样做。这将减少 编译次数,以及需要的重新编译量

另一个原因是头文件为每个类提供了唯一的id。

如果我们有

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


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

我们会有错误,当我们试图构建项目时,因为A是B的一部分,有了头文件,我们就可以避免这种头痛……