为什么在.h文件中使用#ifndef CLASS_H和#define CLASS_H而不是在.cpp文件中?
我一直看到人们写
class.h
#ifndef CLASS_H #define CLASS_H //blah blah blah #endif
问题是,他们为什么不为包含类函数定义的.cpp文件做这件事?
假设我有main.cpp
, main.cpp
包含class.h
。 class.h
文件不会导入任何内容,那么main.cpp
如何知道class.cpp
什么?
首先,解决你的第一个问题:
当你在.h文件中看到这个时:
#ifndef FILE_H #define FILE_H /* ... Declarations etc here ... */ #endif
这是防止头文件被多次包含的预处理器技术,由于各种原因这可能是有问题的。 编译项目期间,每个.cpp文件(通常)都被编译。 简单来说,这意味着编译器会把你的.cpp文件打开,打开它包含的任何文件,将它们连接成一个大文本文件,然后执行语法分析,最后将它转换成一些中间代码,优化/执行其他任务,最后生成目标体系结构的汇编输出。 因此,如果一个文件在一个.cpp文件中被多次#included
,编译器会将其文件内容附加两次,所以如果在该文件中有定义,将会得到一个编译器错误,告诉你重新定义了一个variables。 当编译过程中的预处理器步骤处理文件时,第一次到达其内容时,前两行将检查是否为预处理器定义了FILE_H
。 如果没有,它将定义FILE_H
并继续处理它和#endif
指令之间的代码。 当预处理器下次看到该文件的内容时,对FILE_H
的检查将是错误的,所以它将立即扫描到#endif
并在其后继续。 这可以防止重新定义错误。
并解决你的第二个担心:
在C ++编程中,我们将开发分为两种文件types。 一个是扩展名为.h ,我们称之为“头文件”。 它们通常提供函数,类,结构体,全局variables,types定义,预处理macros和定义等的声明。基本上,它们只是提供有关代码的信息。 然后我们有.cpp扩展名,我们称之为“代码文件”。 这将为这些函数,类成员,需要定义的所有结构成员,全局variables等提供定义。因此, .h文件声明了代码, .cpp文件实现了该声明。 由于这个原因,我们通常在编译期间将每个.cpp文件编译成一个对象,然后链接这些对象(因为几乎从不会看到一个.cpp文件包含另一个.cpp文件)。
如何解决这些外部问题是链接器的工作。 当你的编译器处理main.cpp时 ,它通过包含class.h来获取class.cpp中的代码的声明。 它只需要知道这些函数或variables是什么样的(这是一个声明给你的)。 所以它将你的main.cpp文件编译成一些目标文件(称之为main.obj )。 同样, class.cpp被编译成一个class.obj文件。 为了生成最终的可执行文件,调用链接器将这两个目标文件链接在一起。 对于任何未parsing的外部variables或函数,编译器会在发生访问的地方放置一个存根。 然后链接程序将取出该存根,并在另一个列出的目标文件中查找代码或variables,如果find,则将来自两个目标文件的代码合并到一个输出文件中,并将该存根replace为该函数的最终位置,或者variables。 这样,main.cpp中的代码就可以调用函数,并且可以在class.cpp中使用variables。如果它们是在class.h中声明的 。
我希望这可以帮到你。
CLASS_H
是一名包括警卫 ; 它被用来避免在相同的CPP文件(或者更准确地说,相同的翻译单元 )内多次(通过不同的路由)包括相同的头文件,这将导致多重定义错误。
在CPP文件中不需要包含警卫,因为根据定义,CPP文件的内容只能被读取一次。
你似乎已经解释了包含警卫具有与其他语言(如Java)中的import
语句相同的function; 但事实并非如此。 #include
本身大致等同于其他语言的import
。
它不 – 至less在编译阶段。
从源代码到机器码的c ++程序的翻译分三个阶段进行:
- 预处理 – 预处理器parsing所有以#开始的行的源代码并执行指令。 在你的情况下,你的文件
class.h
的内容被插入代替行#include "class.h
。因为你可能在几个地方包含你的头文件,所以#ifndef
子句避免重复的声明错误,因为只有头文件被包含时,预处理器指令才是未定义的。 - 编译 – 编译器现在将所有预处理的源代码文件翻译成二进制对象文件。
- 链接 – 链接器链接(因此名称)在一起的目标文件。 对您的类或其方法之一(应在class.h中定义并在class.cpp中定义)的引用将parsing为其中一个对象文件中的相应偏移量。 我写'你的一个目标文件',因为你的类不需要在一个名为class.cpp的文件中定义,它可能在一个链接到你的项目的库中。
总之,声明可以通过头文件共享,而声明到定义的映射是由链接器完成的。
这是声明和定义之间的区别。 头文件通常只包含声明,源文件包含定义。
为了使用你只需要知道它的声明不是它的定义。 只有链接器需要知道定义。
所以这就是为什么你会在一个或多个源文件中包含一个头文件,但是你不会在另一个源文件中包含源文件。
你的意思是#include
而不是导入。
main.cpp不需要知道class.cpp中的内容 。 它只需要知道它使用的函数/类的声明,这些声明在class.h中 。
链接器链接在class.h中声明的函数/类使用的地方和它们在class.cpp中的实现之间的链接
这是为头文件完成的,即使它包含多次(通常是因为它包含在其他头文件中),内容也只会在每个预处理的源文件中出现一次。 第一次被包含的时候,符号CLASS_H
(被称为包含守卫 )还没有被定义,所以包含了文件的所有内容。 这样做会定义符号,因此如果再次包含该符号,则会跳过文件内容( #ifndef
/ #endif
块内)。
因为(通常)没有任何其他文件包含,所以不需要为源文件本身执行此操作。
对于最后一个问题, class.h
应该包含类的定义,所有成员的声明,关联的函数以及任何其他的,这样任何包含它的文件都有足够的信息来使用这个类。 函数的实现可以放在一个单独的源文件中; 你只需要声明来调用它们。
.cpp
文件不包括(使用#include
)到其他文件中。 所以他们不需要包括守卫。 Main.cpp
将知道你在class.cpp
实现的类的名字和签名,只是因为你已经在class.h
指定了所有这些 – 这是头文件的目的。 (这取决于你确保class.h
准确地描述了你在class.cpp
实现的代码。) class.cpp
的可执行代码将被提供给main.cpp
的可执行代码,这要归功于连接。
它由于Headerfiles定义了什么类包含(成员,数据结构)和CPP文件实现它。
当然,这样做的主要原因是你可以在其他.h文件中多次包含一个.h文件,但这会导致一个类的多个定义,这是无效的。
一般希望将诸如.cpp
文件之类的代码模块编译一次并链接到多个项目中,以避免不必要的逻辑重复编译。 例如, g++ -o class.cpp
会产生class.o
,然后你可以从多个项目链接到使用g++ main.cpp class.o
。
我们可以使用#include
作为链接器,正如你似乎暗示的那样,但是如果我们知道如何使用我们的编译器进行正确的链接,而且键盘输出更less,编译浪费更less,而不是我们的代码更多的按键和编辑更浪费重复…
然而,这些头文件仍然需要包含在多个项目中,因为这为每个模块提供了接口。 如果没有这些头文件,编译器就不会知道.o
文件引入的任何符号。
意识到头文件是为这些模块引入符号的定义是很重要的。 一旦意识到这一点,多重包含可能会导致符号的重新定义(这会导致错误),因此我们使用包含警卫来防止这种重新定义。