我总是看到人们写这样的代码
class.h
#ifndef CLASS_H #define CLASS_H //blah blah blah #endif
问题是,为什么不对包含类函数定义的 .cpp 文件也这样做呢?
假设我有 main.cpp,main.cpp包括 class.h。class.h文件不 include任何东西,那么 main.cpp如何知道什么是在 class.cpp?
main.cpp
include
class.cpp
这就是声明和定义的区别。头文件通常只包含声明,而源文件包含定义。
为了使用某些东西,你只需要知道它的声明,而不是它的定义。只有连接器需要知道定义。
因此,这就是为什么您将在一个或多个源文件中包含一个头文件,但不会在另一个源文件中包含源文件的原因。
而且你是指 #include而不是导入。
#include
CLASS_H是 包括警卫; 它用于避免同一个头文件在同一个 CPP 文件(或者,更准确地说,同一个 翻译小组)中被多次(通过不同的路由)包含,这将导致多重定义错误。
CLASS_H
在 CPP 文件上不需要包含保护,因为根据定义,CPP 文件的内容只读取一次。
您似乎将 include 守卫解释为具有与其他语言(如 Java)中的 import语句相同的函数; 但事实并非如此。#include本身大致相当于其他语言中的 import。
import
Cpp 不需要知道 Class.cpp中有什么。它只需要知道要使用的函数/类的声明,这些声明位于 同学们中。
链接器链接使用 同学们中声明的函数/类的位置和它们在 Class.cpp中的实现
.cpp文件不包含在其他文件中(使用 #include)。因此,他们不需要包括守卫。Main.cpp将知道您在 class.cpp中实现的类的名称和签名,这仅仅是因为您在 class.h中指定了所有这些内容——这是头文件的用途。(确保 class.h准确地描述在 class.cpp中实现的代码取决于您自己。)由于链接器的努力,class.cpp中的可执行代码将可用于 main.cpp中的可执行代码。
.cpp
Main.cpp
因为 Headerfiles 定义了类包含的内容(成员、数据结构) ,而 cpp 文件实现了它。
当然,这样做的主要原因是你可以包括一个。H 文件多次在其他。H 文件,但这将导致类的多个定义,这是无效的。
对于头文件就是这样做的,所以内容在每个预处理的源文件中只出现一次,即使它包含不止一次(通常是因为它包含在其他头文件中)。第一次包含它时,符号 CLASS_H(称为 包括警卫)尚未定义,因此包含了文件的所有内容。这样做定义了符号,因此如果再次包含该符号,则跳过文件内容(在 #ifndef/#endif块内)。
#ifndef
#endif
没有必要为源文件本身这样做,因为(通常)它不包含在任何其他文件中。
对于您的最后一个问题,class.h应该包含类的定义,以及它的所有成员、关联函数和其他任何东西的声明,以便包含它的任何文件都有足够的信息来使用该类。函数的实现可以放在单独的源文件中; 您只需要声明来调用它们。
它不会-至少在编译阶段不会。
C + + 程序从源代码到机器代码的转换分为三个阶段:
#include "class.h
总之,声明可以通过头文件共享,而声明到定义的映射则由链接器完成。
首先,回答你的第一个问题:
当你在 。 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并继续执行。这可以防止重新定义错误。
#included
FILE_H
还有第二个问题:
在 C + + 编程中,作为一般实践,我们将开发分为两种文件类型。一个是带有 。 h扩展名的,我们称之为“头文件”它们通常提供函数、类、结构、全局变量、 typedef、预处理宏和定义等的声明。基本上,它们只是为您提供有关代码的信息。然后我们有 。 cpp扩展名,我们称之为“代码文件”这将为那些需要定义的函数、类成员、任何结构成员、全局变量等提供定义。因此,。 h文件声明代码,而 。 cpp文件实现该声明。出于这个原因,我们通常在编译期间将每个 。 cpp文件编译成一个对象,然后链接这些对象(因为您几乎从未看到一个 。 cpp文件包含另一个 。 cpp文件)。
如何解决这些外部性是连接器的工作。当编译器处理 Main.cpp时,它通过包含 同学们来获得 Class.cpp中代码的声明。它只需要知道这些函数或变量是什么样子的(这是声明提供的)。因此,它将您的 Main.cpp文件编译成某个目标文件(称为 主要目标)。类似地,Class.cpp被编译成 Class.obj文件。要生成最终的可执行文件,需要调用一个链接器将这两个对象文件链接在一起。对于任何未解析的外部变量或函数,编译器将在访问发生的地方放置存根。然后链接器将获取这个存根并在另一个列出的对象文件中查找代码或变量,如果找到了,它将把两个对象文件中的代码合并到一个输出文件中,并用函数或变量的最终位置替换这个存根。这样,main.cpp 中的代码就可以调用函数并在 Class.cpp中使用变量,如果它们在 同学们中声明的话。
希望这对你有帮助。
一般期望代码模块(如 .cpp文件)只编译一次,并在多个项目中链接,以避免不必要的重复编译逻辑。例如,g++ -o class.cpp将生成 class.o,然后您可以将其从多个项目链接到使用 g++ main.cpp class.o。
g++ -o class.cpp
class.o
g++ main.cpp class.o
我们可以使用 #include作为我们的链接器,就像你似乎暗示的那样,但是当我们知道如何使用编译器以更少的击键和更少的重复编译来正确地链接,而不是我们的代码以更多的击键和更多的重复编译来正确地链接时,那将是愚蠢的。
但是,仍然需要将头文件包含到每个多个项目中,因为这为每个模块提供了接口。没有这些头,编译器就不会知道 .o文件引入的任何符号。
.o
重要的是要意识到头文件是引入这些模块的符号定义的东西; 一旦意识到这一点,那么多个包含可能会导致符号的重新定义(这会导致错误) ,这是有意义的,所以我们使用包含保护来防止这种重新定义。