为什么在.h文件中使用#ifndef CLASS_H和#define CLASS_H而在.cpp中没有?

时间:2010-07-14 13:54:19

标签: c++

我一直看到人们写

class.h

#ifndef CLASS_H
#define CLASS_H

//blah blah blah

#endif

问题是,他们为什么不为包含类函数定义的.cpp文件执行此操作?

假设我有main.cppmain.cpp包含class.hclass.h文件未导入任何内容,因此main.cpp如何知道class.cpp中的内容?

9 个答案:

答案 0 :(得分:261)

首先,要解决您的第一次询问:

当你在 .h 文件中看到这个时:

#ifndef FILE_H
#define FILE_H

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

#endif

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

并解决你的第二个问题:

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

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

我希望这很有帮助。

答案 1 :(得分:12)

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

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

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

答案 2 :(得分:5)

它没有 - 至少在编译阶段。

将c ++程序从源代码转换为机器代码分三个阶段进行:

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

答案 3 :(得分:3)

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

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

因此,这就是为什么您将在一个或多个源文件中包含头文件但不会在另一个源文件中包含源文件的原因。

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

答案 4 :(得分:3)

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

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

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

答案 5 :(得分:2)

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

链接器链接在 class.h 中声明的函数/类的位置和 class.cpp

中的实现之间的链接

答案 6 :(得分:1)

.cpp个文件未包含(使用#include)到其他文件中。因此他们不需要包括防护。 Main.cpp只知道您在class.cpp中实现的类的名称和签名,因为您已在class.h中指定了所有这些 - 这是头文件的目的。 (由您来确保class.h准确描述您在class.cpp中实现的代码。)class.cpp中的可执行代码将可用于{{1}中的可执行代码感谢链接器的努力。

答案 7 :(得分:1)

通常需要将.cpp文件等代码模块编译一次并链接到多个项目中,以避免不必要的重复逻辑编译。例如,g++ -o class.cpp会产生class.o,您可以将其从多个项目链接到使用g++ main.cpp class.o

我们可以使用#include作为我们的链接器,正如您似乎暗示的那样,但是当我们知道如何使用我们的编译器正确地链接以减少击键次数和减少浪费的重复编译时,这将是愚蠢的,而不是我们的代码有更多的击键和更浪费的重复编译...

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

重要的是要认识到头文件引入了这些模块的符号定义;一旦实现了,那么有意义的是,多个包含可能导致符号的重新定义(这会导致错误),所以我们使用包含保护来防止这种重新定义。

答案 8 :(得分:0)

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

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