为什么我们需要同时包含.h
和.cpp
文件,而仅通过包含.cpp
文件就可以使其起作用?
例如:创建一个file.h
包含声明,然后创建一个file.cpp
包含定义,并将两者都包含在中main.cpp
。
或者:在中创建一个file.cpp
包含声明/定义的声明(没有原型)main.cpp
。
两者都为我工作。我看不出有什么区别。也许对编译和链接过程有一些了解可能会有所帮助。
为什么我们需要同时包含.h
和.cpp
文件,而仅通过包含.cpp
文件就可以使其起作用?
例如:创建一个file.h
包含声明,然后创建一个file.cpp
包含定义,并将两者都包含在中main.cpp
。
或者:在中创建一个file.cpp
包含声明/定义的声明(没有原型)main.cpp
。
两者都为我工作。我看不出有什么区别。也许对编译和链接过程有一些了解可能会有所帮助。
Answers:
尽管您可以包括.cpp
提到的文件,但这不是一个好主意。
如前所述,声明属于头文件。当它们包含在多个编译单元中时,这些不会引起任何问题,因为它们不包括实现。多次包含一个函数或类成员的定义通常会导致问题(但并非总是如此),因为链接器会感到困惑并抛出错误。
应该发生的是,每个.cpp
文件都包含程序子集的定义,例如类,按逻辑组织的函数组,全局静态变量(请尽量使用)。
然后,每个编译单元(.cpp
文件)都包含编译其包含的定义所需的任何声明。它跟踪其引用但不包含的函数和类,因此链接器可以在稍后将目标代码组合到可执行文件或库中时解析它们。
例
Foo.h
->包含类Foo的声明(接口)。Foo.cpp
->包含类Foo的定义(实现)。Main.cpp
->包含主要方法,程序入口点。此代码实例化Foo并使用它。双方Foo.cpp
并Main.cpp
需要包括Foo.h
。Foo.cpp
需要它是因为它正在定义支持类接口的代码,因此它需要知道该接口是什么。Main.cpp
之所以需要它,是因为它正在创建Foo并调用其行为,因此它必须知道该行为是什么,Foo在内存中的大小以及如何找到其功能等,但它尚不需要实际的实现。
编译器将生成Foo.o
从Foo.cpp
里面包含了所有的编译形式Foo类代码。它还生成Main.o
包括main方法和对类Foo的未解析的引用。
现在到了连接器,它结合了两个目标文件Foo.o
,并Main.o
转换成可执行文件。它看到了未解析的Foo引用,Main.o
但是看到了Foo.o
包含必需的符号,因此可以说“连接点”。Main.o
现在,将函数调用连接到已编译代码的实际位置,因此在运行时,程序可以跳转到正确的位置。
如果您将Foo.cpp
文件包括在中Main.cpp
,则将有Foo类的两个定义。链接器将看到此消息并说“我不知道该选哪个,所以这是一个错误。” 编译步骤将成功,但链接不会成功。(除非您不进行编译,Foo.cpp
但是为什么要在单独的.cpp
文件中进行编译?)
最后,不同文件类型的想法与C / C ++编译器无关。它编译“文本文件”,希望其中包含所需语言的有效代码。有时它可能能够基于文件扩展名分辨语言。例如,.c
在没有编译器选项的情况下编译文件,它将假定为C,而a .cc
或.cpp
扩展名将告诉它假定为C ++。但是,我可以很容易地告诉编译器将一个.h
甚至一个.docx
文件编译为C ++,.o
如果它包含纯文本格式的有效C ++代码,它将发出一个object()文件。这些扩展更多地是为了程序员的利益。如果看到Foo.h
和Foo.cpp
,则立即假定第一个包含类的声明,第二个包含定义。
Foo.cpp
在Main.cpp
你不需要包含.h
文件,你少了一个文件,你还是赢在分裂码成单独文件的可读性,和你的编译命令更加简单。虽然我了解头文件的重要性,但我不认为这是事实
If you had included the Foo.cpp file in Main.cpp, there would be two definitions of class Foo.
关于该问题的最重要的句子。
阅读来自角色C和C ++预处理器,其在概念上是C或C ++编译器的第一“阶段”(历史上它是一个单独的程序/lib/cpp
;现在,出于性能原因是集成编译器内部的正常cc1
或cc1plus
)。请特别阅读GNU cpp
预处理程序的文档。因此,实际上,编译器在概念上首先会对您的编译单元(或翻译单元)进行预处理,然后对预处理后的表单进行处理。
如果包含标头文件,则可能需要始终包含标头文件file.h
(由惯例和习惯决定):
typedef
,struct
,class
等...)static inline
功能定义注意将这些放在头文件中是惯例(和方便)的问题。
当然,您的实现file.cpp
需要以上所有,因此#include "file.h"
首先要。
这是一个约定(但很常见)。您可以避免头文件,并将其内容复制并粘贴到实现文件(即翻译单元)中。但是您不希望这样做(除非自动生成C或C ++代码;否则,您可以使生成器程序进行该复制和粘贴,从而模仿预处理器的角色)。
关键是预处理器仅执行文本操作。您可以(原则上)完全通过复制和粘贴来避免使用它,或者用另一个“预处理器”或C代码生成器(例如gpp或m4)替换它。
另一个问题是最新的C(或C ++)标准定义了几个标准头文件。大多数实现实际上将这些标准标头实现为(特定于实现的)文件,但我相信,符合规范的实现可能会通过一些魔术(例如使用某些数据库或某些)来实现标准包含(如#include <stdio.h>
C或#include <vector>
C ++)。编译器内部的信息)。
如果使用GCC编译器(例如gcc
或g++
),则可以使用-H
标志获取有关每个包含项的信息,并使用-C -E
标志获取经过预处理的表单。当然,还有许多其他影响预处理的编译器标志(例如-I /some/dir/
,添加/some/dir/
用于搜索包含的文件,以及-D
预定义一些预处理器宏,等等,等等。)。
从所选择的答案为什么我们需要写一个头文件?是一个合理的解释,但我想补充其他细节。
头文件的合理性似乎在教学和讨论C / C ++时容易迷失方向。
头文件提供了两个应用程序开发问题的解决方案:
C / C ++可以从小型程序扩展到大型的数百万行,数千个文件程序。应用程序开发可以从一个开发人员的团队扩展到数百个开发人员。
作为开发人员,您可以戴几顶帽子。特别是,您可以是函数和类的接口的用户,或者可以是函数和类的接口的编写器。
使用函数时,您需要了解函数接口,要使用的参数,函数返回的内容以及函数的功能。无需查看实现即可轻松将其记录在头文件中。您什么时候读过的实现printf
?购买我们每天都在使用。
当您是界面开发人员时,帽子会改变另一个方向。头文件提供了公共接口的声明。头文件定义了另一个实现需要什么才能使用此接口。头文件中不会(也不应)声明此新接口的内部和私有信息。公共头文件应该是使用该模块所需的所有文件。
对于大规模开发,编译和链接可能需要很长时间。从数分钟到数小时(甚至数天!)。将软件分为接口(头)和实现(源),提供了一种仅编译需要编译的文件而不是重新构建所有内容的方法。
此外,头文件允许开发人员提供库(已编译)以及头文件。库的其他用户可能永远看不到正确的实现,但是仍可以将库与头文件一起使用。您每天都使用C / C ++标准库执行此操作。
即使您正在开发小型应用程序,使用大型软件开发技术也是一个好习惯。但是我们还需要记住为什么要使用这些习惯。
您可能会问,为什么不将所有代码都放在一个文件中。
最简单的答案是代码维护。
在某些情况下,创建类是合理的:
完全内联到头文件中是合理的时候,该类实际上是一个具有几个基本getter和setters的数据结构,并且也许是一个采用值初始化其成员的构造函数。
(需要全部内联的模板是一个稍微不同的问题)。
在头中创建类的其他时间是您可能在多个项目中使用该类,并且特别希望避免在库中链接时。
您可能将整个类包含在编译单元中而根本不公开其标头的时候是:
一个“ impl”类,仅由其实现的类使用。它是该类的实现细节,在外部不使用。
由某种工厂方法创建的抽象基类的实现,该工厂方法返回指向基类的指针/引用/智能指针。工厂方法不会由类本身公开。(此外,如果类具有通过静态实例在表上注册自己的实例,则甚至不需要通过工厂公开它)。
“ functor”类型类。
换句话说,您不希望任何人包含标头。
我知道您可能在想...如果只是出于可维护性,通过包含cpp文件(或完全内联的标头),您可以轻松地编辑文件以“查找”代码并重新构建。
但是,“可维护性”不仅仅是使代码看起来整洁。这是变化影响的问题。众所周知,如果您不更改标头,而只是更改实现(.cpp)
文件,则不需要重建另一个源,因为应该没有副作用。
这样就可以更安全地进行此类更改,而不必担心连锁效应,而这实际上就是“可维护性”的含义。
Snowman的示例需要一些扩展名才能确切说明为什么需要.h文件。
将另一个班级Bar添加到剧本中,这也取决于班级Foo。
Foo.h->包含类Foo的声明
Foo.cpp->包含类Foo的定义(实现)
Main.cpp->使用类型为Foo的变量。
Bar.cpp->也使用Foo类型的变量。
现在,所有cpp文件都需要包含Foo.h。将Foo.cpp包含在多个其他cpp文件中将是一个错误。链接器将失败,因为将多次定义类Foo。
.cpp
文件,因此根本不会进行任何链接。
如果将整个代码写在同一个文件中,那么它将使您的代码很难看。
其次,您将无法与他人分享您的书面课堂。根据软件工程,您应该分别编写客户端代码。客户端不应该知道您的程序如何工作。它们只需要输出。如果将整个程序写在同一个文件中,将会泄漏程序的安全性。