【问题标题】:Why use #ifndef CLASS_H and #define CLASS_H in .h file but not in .cpp?为什么在 .h 文件中使用 #ifndef CLASS_H 和 #define CLASS_H 而不是在 .cpp 中?
【发布时间】:2011-03-15 21:05:03
【问题描述】:

我一直看到人们写作

类.h

#ifndef CLASS_H
#define CLASS_H

//blah blah blah

#endif

问题是,他们为什么不对包含类函数定义的 .cpp 文件也这样做?

假设我有main.cpp,而main.cpp 包括class.hclass.h 文件没有 include 任何内容,那么 main.cpp 怎么知道 class.cpp 中的内容?

【问题讨论】:

  • "import" 可能不是您想在这里使用的词。包括。
  • 在 C++ 中,文件和类之间没有一对一的关联。您可以根据需要将任意数量的类放入一个文件中(或者甚至将一个类拆分到多个文件中,尽管这很少有帮助)。因此宏应该是FILE_H,而不是CLASS_H
  • 见我的include guard advice

标签: c++


【解决方案1】:

首先,解决您的第一个问题:

当您在 .h 文件中看到此内容时:

#ifndef FILE_H
#define FILE_H

/* ... Declarations etc here ... */

#endif

这是一种防止头文件被多次包含的预处理器技术,由于各种原因,这可能会出现问题。在您的项目编译期间,每个 .cpp 文件(通常)都会被编译。简单来说,就是编译器会把你的.cpp文件,打开任何#included的文件,拼接成一个大文本文件,然后进行语法分析,最后转换它到一些中间代码,优化/执行其他任务,最后为目标架构生成汇编输出。因此,如果一个文件在一个 .cpp 文件下多次出现#included,编译器会将其文件内容附加两次,因此如果该文件中有定义,则会出现编译器错误告诉你你重新定义了一个变量。当编译过程中的预处理器步骤处理文件时,第一次到达其内容时,前两行将检查是否为预处理器定义了FILE_H。如果不是,它将定义FILE_H 并继续处理它与#endif 指令之间的代码。下一次预处理器看到该文件的内容时,对FILE_H 的检查将是错误的,因此它将立即向下扫描到#endif 并在它之后继续。这样可以防止重新定义错误。

并解决您的第二个问题:

在 C++ 编程中,我们通常将开发分为两种文件类型。一种是扩展名为 .h,我们称之为“头文件”。它们通常提供函数、类、结构、全局变量、typedef、预处理宏和定义等的声明。基本上,它们只是为您提供有关代码的信息。然后我们有 .cpp 扩展名,我们称之为“代码文件”。这将为那些函数、类成员、任何需要定义的结构成员、全局变量等提供定义。因此 .h 文件声明代码,而 .cpp 文件实现该声明。出于这个原因,我们通常在编译期间将每个 .cpp 文件编译成一个对象,然后链接这些对象(因为您几乎从未见过一个 .cpp 文件包含另一个 .cpp 文件)。

如何解决这些外部因素是链接器的工作。当您的编译器处理 ma​​in.cpp 时,它会通过包含 class.h 来获取 class.cpp 中代码的声明。它只需要知道这些函数或变量是什么样的(这是声明给你的)。因此它会将您的 ma​​in.cpp 文件编译成某个目标文件(称为 ma​​in.obj)。同样,class.cpp 被编译成 class.obj 文件。为了生成最终的可执行文件,调用链接器将这两个目标文件链接在一起。对于任何未解析的外部变量或函数,编译器将在访问发生的地方放置一个存根。然后,链接器将获取此存根并在另一个列出的目标文件中查找代码或变量,如果找到,它将两个目标文件中的代码组合到一个输出文件中,并将存根替换为函数的最终位置或多变的。这样,您在 main.cpp 中的代码可以调用函数并使用 class.cpp 中的变量,当且仅当它们在 class.h 中声明时。

我希望这会有所帮助。

【讨论】:

  • 过去几天我一直在尝试理解 .h 和 .cpp。这个答案节省了我学习 C++ 的时间和兴趣。写得很好。谢谢贾斯汀!
  • 你的解释真的很棒!如果是图片的话,也许答案会很好
  • @justin-summerlin 你的这一行“正因为如此,如果一个文件在一个 .cpp 文件下被多次#included,编译器将附加它的文件内容两次,所以如果里面有定义那个文件,你会得到一个编译器错误,告诉你你重新定义了一个变量”。我尝试删除 ifndef 并编译了我的代码。尽管它是在两个文件中导入的,但它运行成功。我在 linux 中尝试使用命令“g++ first.cpp second.cpp”。除了这个,我同意你的其他理由。它只是为了防止任何标识符的双重声明,但它不会抛出错误。
  • 晶莹剔透!谢谢贾斯汀的解释。
【解决方案2】:

CLASS_H 是一个include guard;它用于避免在同一个 CPP 文件(或者更准确地说,同一个 translation unit)中多次(通过不同的路由)包含同一个头文件,这会导致多重定义错误。

CPP 文件不需要包含保护,因为根据定义,CPP 文件的内容只能读取一次。

您似乎将包含保护解释为与其他语言(例如 Java)中的 import 语句具有相同的功能;然而,事实并非如此。 #include 本身大致相当于其他语言中的import

【讨论】:

  • “在同一个 CPP 文件内”应改为“在同一个翻译单元内”。
  • @dreamlax: 好点子——这就是我最初要写的,但后来我发现不懂包含守卫的人只会对“翻译单元”这个词感到困惑.我将编辑答案以在括号中添加“翻译单元”——这应该是两全其美。
【解决方案3】:

至少在编译阶段不会。

c++ 程序从源代码到机器代码的翻译分三个阶段进行:

  1. 预处理 - 预处理器解析所有以 # 开头的行的源代码并执行指令。在您的情况下,您的文件class.h 的内容被插入到#include "class.h 行的位置。由于您可能在多个位置包含在头​​文件中,#ifndef 子句避免重复声明错误,因为预处理器指令仅在第一次包含头文件时未定义。
  2. 编译 - 编译器现在将所有预处理的源代码文件转换为二进制目标文件。
  3. 链接 - 链接器将目标文件链接在一起(因此得名)。对您的类或其方法之一的引用(应在 class.h 中声明并在 class.cpp 中定义)被解析为其中一个目标文件中的相应偏移量。我写了“你的目标文件之一”,因为你的类不需要在名为 class.cpp 的文件中定义,它可能在链接到你的项目的库中。

总而言之,声明可以通过头文件共享,而声明到定义的映射由链接器完成。

【讨论】:

    【解决方案4】:

    这就是声明和定义的区别。头文件通常只包含声明,而源文件包含定义。

    为了使用某些东西,您只需要知道它的声明而不是它的定义。只有链接器需要知道定义。

    所以这就是为什么你会在一个或多个源文件中包含一个头文件,但你不会在另一个源文件中包含一个源文件。

    你的意思是#include而不是导入。

    【讨论】:

    • 但是链接器如何知道它的定义是在那个特定的 .cpp 文件中定义的。
    【解决方案5】:

    这是针对头文件完成的,因此内容在每个预处理的源文件中只出现一次,即使它被包含多次(通常是因为它被包含在其他头文件中)。第一次包含时,符号CLASS_H(称为include 保护)尚未定义,因此文件的所有内容都包含在内。这样做会定义符号,因此如果再次包含它,则会跳过文件的内容(在 #ifndef/#endif 块内)。

    源文件本身没有必要这样做,因为(通常)它不包含在任何其他文件中。

    对于您的最后一个问题,class.h 应该包含类的定义、其所有成员的声明、关联函数以及其他任何内容,以便包含它的任何文件都有足够的信息来使用该类。函数的实现可以放在单独的源文件中;您只需要声明即可调用它们。

    【讨论】:

      【解决方案6】:

      ma​​in.cpp 不必知道 class.cpp 中的内容。它只需要知道它要使用的函数/类的声明,这些声明在 class.h 中。

      class.h 中声明的函数/类的使用位置与 class.cpp

      中的实现之间的链接器链接

      【讨论】:

        【解决方案7】:

        .cpp 文件不包含(使用#include)到其他文件中。因此他们不需要包括守卫。 Main.cpp 将知道您在 class.cpp 中实现的类的名称和签名,因为您已经在 class.h 中指定了所有内容 - 这是头文件的目的。 (由您确保class.h 准确描述您在class.cpp 中实现的代码。)class.cpp 中的可执行代码将可用于main.cpp 中的可执行代码,感谢链接器。

        【讨论】:

          【解决方案8】:

          一般希望.cpp文件等代码模块编译一次,在多个项目中链接,避免不必要的逻辑重复编译。例如,g++ -o class.cpp 将生成 class.o,然后您可以将其从多个项目链接到使用 g++ main.cpp class.o

          我们可以使用#include 作为我们的链接器,正如你所暗示的那样,但是当我们知道如何使用我们的编译器正确链接时,这将是愚蠢的更多的击键和更浪费的编译重复......

          但是,仍然需要将头文件包含到多个项目中的每个项目中,因为这为每个模块提供了接口。如果没有这些头文件,编译器就不会知道.o 文件引入的任何符号。

          重要的是要意识到头文件是为这些模块引入符号定义的东西;一旦意识到这一点,那么多个包含可能会导致符号的重新定义(这会导致错误)是有道理的,因此我们使用包含保护来防止此类重新定义。

          【讨论】:

            【解决方案9】:

            因为 Headerfiles 定义了类包含的内容(成员、数据结构),而 cpp 文件实现了它。

            当然,这样做的主要原因是你可以在其他 .h 文件中多次包含一个 .h 文件,但这会导致一个类的多个定义,这是无效的。

            【讨论】:

              猜你喜欢
              • 2010-12-11
              • 2011-01-03
              • 2012-02-28
              • 2017-12-22
              • 2014-10-11
              • 2010-10-26
              • 2018-02-28
              • 2011-08-25
              • 1970-01-01
              相关资源
              最近更新 更多