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


136

我一直看到人们写

#ifndef CLASS_H
#define CLASS_H

//blah blah blah

#endif

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

假设我有 main.cpp,并且main.cpp包括class.h。该class.h文件没有include任何内容,那么怎么main.cpp知道class.cpp


5
“导入”可能不是您要在此处使用的词。包括。
凯特·格雷戈里

5
在C ++中,文件和类之间没有一对一的关联。您可以根据需要将多个类放入一个文件中(甚至可以将一个类拆分为多个文件,尽管这很少有帮助)。因此,宏应该是FILE_H,而不是CLASS_H
2010年

1
请参阅我的包括后卫建议

Answers:


302

首先,解决您的第一个查询:

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

#ifndef FILE_H
#define FILE_H

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

#endif

这是一种防止多次包含头文件的预处理器技术,由于各种原因,头文件可能会出现问题。在项目编译期间,通常会编译每个.cpp文件。简单来说,这意味着编译器将获取您的.cpp文件,打开#included它的任何文件,将它们全部连接成一个大文本文件,然后执行语法分析,最后将其转换为一些中间代码,对其他文件进行优化/执行任务,最后生成目标体系结构的程序集输出。因此,如果文件#included在一个.cpp下多次出现文件,编译器将其文件内容追加两次,因此,如果该文件中有定义,则会出现编译器错误,提示您重新定义了变量。当文件在编译过程中由预处理器步骤处理时,第一次到达其内容时,前两行将检查是否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当且仅当他们宣布class.h

我希望这可以帮到你。


在过去的几天里,我试图理解.h和.cpp。这个答案节省了我的时间和兴趣,以学习C ++。写得好。谢谢贾斯汀!
Rajkumar R

你真的解释得太好了!如果使用图像,也许答案会很好
alamin

13

CLASS_H是一个包括防护件 ; 它用于避免同一头文件被多次(通过不同的路由)包含在同一CPP文件(或更准确地说是同一转换单元)中,否则将导致多定义错误。

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

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


2
“在同一CPP文件中”应显示为“在同一翻译单元中”。
dreamlax 2010年

@dreamlax:好点-那就是我本来要写的,但是后来我发现,一个不理解包括卫兵的人只会被术语“翻译单位”所迷惑。我将编辑答案以在括号中添加“翻译单位”,这应该是两全其美的方法。
Martin B 2010年

6

至少在编译阶段没有。

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

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

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


4

那就是声明和定义之间的区别。头文件通常仅包含声明,而源文件包含定义。

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

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

你也是说#include而不是导入。


3

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

不需要对源文件本身执行此操作,因为(通常)其他文件不包含该文件。

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


2

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

链接器链接使用class.h中声明的函数/类的位置与其在class.cpp中的实现之间的链接


1

.cpp文件不包含(使用#include)到其他文件中。因此,它们不需要包括防护。仅因为您在其中指定了所有内容,Main.cpp才会知道实现该类的名称和签名-这是头文件的目的。(由您来确保准确描述您在中实现的代码。)由于链接程序的努力,可执行文件中的可执行代码将可用。class.cppclass.hclass.hclass.cppclass.cppmain.cpp


1

通常期望诸如.cpp文件之类的代码模块被编译一次并链接到多个项目中,以避免不必要的重复逻辑编译。例如,g++ -o class.cpp将产生class.o,然后您可以将其从多个项目链接到using g++ main.cpp class.o

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

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

重要的是要意识到,头文件是那些模块的符号定义的引入。一旦意识到这一点,便可以理解多个包含可能导致符号的重新定义(这会导致错误),因此我们使用include防护措施来防止此类重新定义。


0

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

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.