【问题标题】:Why have header files and .cpp files? [closed]为什么要有头文件和 .cpp 文件? [关闭]
【发布时间】:2010-09-24 22:27:22
【问题描述】:

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

【问题讨论】:

  • 它是一种常见的OOP范式,.h是类声明,cpp是定义。不需要知道它是如何实现的,他/她应该只知道接口。
  • 这是 c++ 将接口与实现分离的最佳部分。它总是很好,而不是将所有代码保存在单个文件中,我们将接口分开。一些代码总是在那里,比如作为头文件一部分的内联函数。当看到一个头文件显示声明的函数列表和类变量时看起来不错。
  • 有时候头文件对于编译来说是必不可少的——不仅仅是组织偏好或分发预编译库的方式。假设您有一个结构,其中 game.c 依赖于两个 Physics.c 和 math.c; physics.c 也依赖于 math.c。如果您包含 .c 文件并永远忘记了 .h 文件,您将有来自 math.c 的重复声明并且没有编译希望。这就是为什么头文件很重要对我来说最有意义的原因。希望它可以帮助别人。
  • 我认为这与扩展名中只允许使用字母数字字符这一事实有关。我什至不知道这是不是真的,只是猜测

标签: c++ header-files


【解决方案1】:

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 可能需要包含使用 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

【讨论】:

  • @nimcap:You still have to copy paste the signature from header file to cpp file, don't you?:不需要。只要 CPP“包含”HPP,预编译器就会自动将 HPP 文件的内容复制粘贴到 CPP 文件中。我更新了答案以澄清这一点。
  • 谢谢,您的复制/粘贴概念很有帮助。但是你的观点“它不会编译,因为 A.cpp 无法知道“doSomethingElse”的存在”对我来说似乎是错误的。在编译 A.cpp 时,编译器从调用本身知道 doSomethingElse 的参数类型和返回值;它可以假设 doSomethingElse 是在另一个模块中定义的,并依赖链接器来填充依赖项(如果找不到它的定义或参数/返回值的类型在 A.cpp 和 B.cpp 中不兼容,则返回错误)。我仍然没有得到标题的必要性。看起来,它们只是一个非常丑陋的任意设计。
  • @Bob:While compiling A.cpp, compiler knows the types of arguments and return value of doSomethingElse from the call itself。不,它没有。它只知道用户提供的类型,有一半的时间,它甚至不会费心去读取返回值。然后,发生隐式转换。然后,当你有代码:foo(bar),你甚至不能确定foo 是一个函数。所以编译器必须能够访问头文件中的信息来决定源代码是否正确编译......然后,一旦代码被编译,链接器只会将函数调用链接在一起。
  • @Bob : [继续] ...现在,我猜链接器可以完成编译器完成的工作,这将使您的选择成为可能。 (我想这是下一个标准的“模块”命题的主题)。 Seems, they're just a pretty ugly arbitrary design.:如果 C++ 是在 2012 年创建的,确实如此。但请记住,C++ 是在 1980 年代基于 C 构建的,当时,约束条件完全不同(IIRC,出于采用目的,决定保留与 C 相同的链接器)。
  • 为什么不能将 B.CPP 包含在 A.CPP 中?
【解决方案2】:

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

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

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

【讨论】:

  • 不是真的。标头仍然包含实现的主要部分。从什么时候开始私有实例变量成为类接口的一部分?私有成员函数?那么他们在公开可见的标题中到底在做什么呢?它与模板进一步分离。
  • 这就是为什么我说它并不完美,需要Pimpl成语来进行更多的分离。模板是完全不同的蠕虫——即使大多数编译器完全支持“exports”关键字,它仍然是语法糖而不是真正的分离。
  • 其他语言如何处理这个问题?例如 - Java? Java中没有头文件的概念。
  • @Lazer:Java 更易于解析。 Java 编译器可以在不知道其他文件中的所有类的情况下解析文件,然后检查类型。在 C++ 中,许多结构在没有类型信息的情况下是模棱两可的,因此 C++ 编译器需要有关引用类型的信息来解析文件。这就是为什么它需要标题。
  • @nikie:解析的“轻松”与它有什么关系?如果 Java 有一个至少和 C++ 一样复杂的语法,它仍然可以只使用 java 文件。在这两种情况下,C 呢? C 易于解析,但同时使用头文件和 c 文件。
【解决方案3】:

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

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

【讨论】:

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

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

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

【讨论】:

    【解决方案5】:

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

    【讨论】:

    • 为什么从 C 继承 C++ 是不幸的?
    • 这怎么可能是答案?
    • @ShuvoSarker 因为数千种语言已经证明,对于 C++ 让程序员编写两次函数签名没有技术解释。 “为什么?”的答案是“历史”。
    • @Boris 很有趣,C 实际上不需要写两次。和 C 最初根本不需要原型,因为它运行在允许这种实现的平台上。他们甚至没有堆栈寄存器,“堆栈”只是由生成的代码管理的内存区域。这是 C++ 的东西,现代平台转向基于寄存器或混合调用函数的方式,因此如果我们隐藏实现并且如果我们可以重载,则需要单独的原型。相当多的经典(Fortran、Pascal)和现代语言也是如此。没有这样的通常是解释器的签名
    • 为什么 tf 这个有 +20 分?
    【解决方案6】:

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

    由于您需要该信息来告诉您的编译器“此函数稍后在链接器执行其工作时可用”,因此他们必须提供第二个文件来存储此共享信息。

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

    【讨论】:

    • 行不通,循环引用。也就是说,在从 b.cpp 构建 b.lib 之前,您不能从 a.cpp 构建 a.lib,但您也不能在 a.lib 之前构建 b.lib。
    • Java 解决了这个问题,Python 可以做到,任何现代语言都可以做到。但在 C 语言被发明的时候,RAM 非常昂贵和稀缺,它根本不是一种选择。
    【解决方案7】:

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

    【讨论】:

      【解决方案8】:

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

      在单个项目中,使用头文件,恕我直言,至少有两个目的:

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

      【讨论】:

      • 为什么库供应商不能只提供生成的“头”文件?一个无预处理器的“头”文件应该提供更好的性能(除非实现真的被破坏了)。
      • 我认为头文件是生成还是手写无关紧要,问题不是“为什么人们自己编写头文件?”,而是“为什么我们有头文件”。预处理器免费标头也是如此。当然,这样会更快。
      【解决方案9】:

      回复MadKeithV's answer

      这减少了依赖性,因此使用标头的代码不会 必然需要知道实施的所有细节和任何 仅为此需要的其他类/标题。这将减少 编译时间,以及何时需要重新编译的数量 实现中的某些内容发生了变化。

      另一个原因是标题为每个类提供了唯一的 id。

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

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

      当我们尝试构建项目时,我们会遇到错误,因为 A 是 B 的一部分,使用 headers 我们可以避免这种头痛......

      【讨论】:

      • 这专门叫抽象对吗?
      猜你喜欢
      • 1970-01-01
      • 2011-03-11
      • 2021-09-15
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-12-23
      • 2010-10-26
      相关资源
      最近更新 更多