第一个问题:
为什么不包括保护我的头文件免受相互、递归包含?
They are.
他们没有提供帮助的是相互包含的标头中数据结构定义之间的依赖关系。为了了解这意味着什么,让我们从一个基本场景开始,看看为什么包含防护确实有助于相互包含。
假设你的相互包含a.h
and b.h
头文件具有简单的内容,即问题文本中的代码部分中的省略号被替换为空字符串。在这种情况下,你的main.cpp
将愉快地编译。这都要归功于你们的守护者!
如果您不相信,请尝试删除它们:
//================================================
// a.h
#include "b.h"
//================================================
// b.h
#include "a.h"
//================================================
// main.cpp
//
// Good luck getting this to compile...
#include "a.h"
int main()
{
...
}
您会注意到,当达到包含深度限制时,编译器将报告失败。此限制是特定于实现的。根据 C++11 标准第 16.2/6 段:
#include 预处理指令可能会出现在由于另一个文件中的 #include 指令而已读取的源文件中,达到实现定义的嵌套限制.
发生什么了?
- 解析时
main.cpp
,预处理器将满足指令#include "a.h"
。该指令告诉预处理器处理头文件a.h
,获取该处理的结果,并替换字符串#include "a.h"
得到这个结果;
- 加工时
a.h
,预处理器将满足指令#include "b.h"
,并且应用相同的机制:预处理器应处理头文件b.h
,取其处理结果,并替换#include
指令结果;
- 加工时
b.h
,指令#include "a.h"
会告诉预处理器进行处理a.h
并将该指令替换为结果;
- 预处理器将开始解析
a.h
再次,将会遇见#include "b.h"
再次指令,这将建立一个潜在的无限递归过程。当达到临界嵌套级别时,编译器将报告错误。
当包含守卫存在时但是,第 4 步中不会设置无限递归。让我们看看为什么:
- (和之前一样) 解析时
main.cpp
,预处理器将满足指令#include "a.h"
。这告诉预处理器处理头文件a.h
,获取该处理的结果,并替换字符串#include "a.h"
得到这个结果;
- 加工时
a.h
,预处理器将满足指令#ifndef A_H
。自宏观A_H
尚未定义,它将继续处理下面的文本。随后的指令(#defines A_H
) 定义宏A_H
。然后,预处理器将满足指令#include "b.h"
:预处理器现在应该处理头文件b.h
,取其处理结果,并替换#include
指令结果;
- 加工时
b.h
,预处理器将满足指令#ifndef B_H
。自宏观B_H
尚未定义,它将继续处理下面的文本。随后的指令(#defines B_H
) 定义宏B_H
。然后,指令#include "a.h"
会告诉预处理器进行处理a.h
并替换#include
指令中b.h
与预处理的结果a.h
;
- 编译器将开始预处理
a.h
再次,并遇见#ifndef A_H
再次指令。然而,在之前的预处理过程中,宏A_H
已被定义。因此,编译器这次会跳过下面的文本,直到匹配到#endif
找到指令,并且此处理的输出是空字符串(假设后面没有任何内容)#endif
当然是指令)。因此,预处理器将取代#include "a.h"
指令中b.h
与空字符串,并将追溯执行,直到它替换原来的#include
指令中main.cpp
.
Thus, 包括警卫确实可以防止相互包容。然而,他们无能为力类的定义之间的依赖关系在相互包含的文件中:
//================================================
// a.h
#ifndef A_H
#define A_H
#include "b.h"
struct A
{
};
#endif // A_H
//================================================
// b.h
#ifndef B_H
#define B_H
#include "a.h"
struct B
{
A* pA;
};
#endif // B_H
//================================================
// main.cpp
//
// Good luck getting this to compile...
#include "a.h"
int main()
{
...
}
鉴于上述标头,main.cpp
不会编译。
为什么会发生这种情况?
要了解发生了什么情况,只需再次执行步骤 1-4 即可。
很容易看出,前三个步骤和第四步的大部分内容不受此更改的影响(只需通读它们即可确信)。然而,在第 4 步结束时会发生一些不同的情况:替换#include "a.h"
指令中b.h
对于空字符串,预处理器将开始解析b.h
并且,特别是,的定义B
。不幸的是,定义B
提到类A
,以前从未遇到过because包含守卫!
声明先前未声明的类型的成员变量当然是一个错误,编译器会礼貌地指出这一点。
我需要做什么来解决我的问题?
你需要前向声明.
事实上,定义班级的A
不需要定义类B
,因为一个pointer to A
被声明为成员变量,而不是类型的对象A
。由于指针具有固定大小,因此编译器不需要知道指针的确切布局A
也不计算其大小以正确定义类B
。因此,这足以前向声明 class A
in b.h
并使编译器知道它的存在:
//================================================
// b.h
#ifndef B_H
#define B_H
// Forward declaration of A: no need to #include "a.h"
struct A;
struct B
{
A* pA;
};
#endif // B_H
Your main.cpp
现在肯定会编译。几点说明:
- 不仅通过替换来打破相互包含
#include
带有前向声明的指令b.h
足以有效地表达依赖关系B
on A
:只要可能/实用,使用前向声明也被认为是一种良好的编程习惯,因为它有助于避免不必要的包含,从而减少总体编译时间。但消除相互包含后,main.cpp
必须修改为#include
both a.h
and b.h
(如果确实需要后者),因为b.h
不再是间接的#include
d 通过a.h
;
- 虽然类的前向声明
A
足以让编译器声明指向该类的指针(或在可接受不完整类型的任何其他上下文中使用它),取消引用指向该类的指针A
(例如调用成员函数)或计算其大小illegal对不完整类型的操作:如果需要,请提供完整的定义A
需要可供编译器使用,这意味着必须包含定义它的头文件。这就是为什么类定义及其成员函数的实现通常分为头文件和该类的实现文件(类模板是此规则的一个例外):实现文件,这些文件永远不会#include
d 由项目中的其他文件,可以安全地#include
使定义可见的所有必要的标题。另一方面,头文件不会#include
其他头文件unless他们确实需要这样做(例如,定义基类可见),并将尽可能/实用地使用前向声明。
第二个问题:
为什么不包括警卫阻止多重定义?
They are.
他们无法保护您免受多重定义的侵害在单独的翻译单元中。这也解释在本次问答在 StackOverflow 上。
太看到了,尝试删除包含防护并编译以下修改版本source1.cpp
(or source2.cpp
,重要的是):
//================================================
// source1.cpp
//
// Good luck getting this to compile...
#include "header.h"
#include "header.h"
int main()
{
...
}
编译器肯定会在这里抱怨f()
被重新定义。这是显而易见的:它的定义被包含了两次!然而,上述source1.cpp
编译时不会出现问题header.h
包含适当的包含防护。这是预期的。
尽管如此,即使存在包含保护并且编译器将不再用错误消息来打扰您,linker将坚持这样一个事实:在合并从编译中获得的目标代码时会发现多个定义source1.cpp
and source2.cpp
,并将拒绝生成您的可执行文件。
为什么会发生这种情况?
基本上,每个.cpp
文件(本文中的技术术语是翻译单位)在您的项目中是单独编译的并且独立地。当解析一个.cpp
文件,预处理器将处理所有#include
指令并展开它遇到的所有宏调用,并且此纯文本处理的输出将作为编译器的输入给出,以将其转换为目标代码。一旦编译器完成为一个翻译单元生成目标代码,它将继续处理下一个翻译单元,并且在处理前一个翻译单元时遇到的所有宏定义都将被忘记。
事实上,编译一个项目n
翻译单位(.cpp
文件)就像执行同一个程序(编译器)n
次,每次都有不同的输入:同一程序的不同执行不会共享先前程序执行的状态。因此,每个翻译都是独立执行的,编译一个翻译单元时遇到的预处理器符号在编译其他翻译单元时不会被记住(如果你想一下,你会很容易意识到这实际上是一种理想的行为)。
因此,即使包含防护可以帮助您防止递归相互包含和多余的在一个翻译单元中包含相同的标头,它们无法检测到相同的定义是否包含在一个翻译单元中。不同的翻译单位。
然而,当合并所有编译生成的目标代码时.cpp
您的项目的文件,链接器will看到同一个符号被定义多次,因为这违反了一种定义规则。根据 C++11 标准第 3.2/3 段:
每个程序都应准确包含每个的一个定义非内联在该程序中使用 odr 的函数或变量;无需诊断。该定义可以显式地出现在程序中,可以在标准或用户定义的库中找到,或者(在适当的情况下)它是隐式定义的(参见12.1、12.4和12.8)。内联函数应在每个使用 odr 的翻译单元中定义.
因此,链接器将发出错误并拒绝生成程序的可执行文件。
我需要做什么来解决我的问题?
If您想将函数定义保存在头文件中#include
d by multiple翻译单元(注意,如果您的标题是#include
d 只是通过one翻译单元),您需要使用inline
关键词。
否则,您只需要保留宣言你的功能在header.h
,将其定义(主体)放入one分离.cpp
仅文件(这是经典方法)。
The inline
关键字表示对编译器的非绑定请求,要求编译器直接在调用站点内联函数体,而不是为常规函数调用设置堆栈帧。尽管编译器不必满足您的请求,但inline
关键字确实成功地告诉链接器容忍多个符号定义。根据C++11标准第3.2/5段:
a 可以有多个定义类类型(第 9 条)、枚举类型(7.2)、具有外部链接的内联函数(7.1.2),类模板(第14条),非静态函数模板(14.5.6),类模板的静态数据成员(14.5.1.3),类模板的成员函数(14.5.1.1),或模板专门化,在程序中未指定某些模板参数(14.7、14.5.5),前提是每个定义出现在不同的翻译单元中,并且定义满足以下要求 [...]
上面的段落基本上列出了头文件中常见的所有定义,因为它们可以安全地包含在多个翻译单元中。相反,具有外部链接的所有其他定义都属于源文件。
使用static
关键字而不是inline
关键字还可以通过提供您的函数来抑制链接器错误内部联系,从而使每个翻译单元拥有一个私有的copy该函数(及其局部静态变量)的。然而,这最终会导致更大的可执行文件,并且使用inline
一般情况下应该是首选。
另一种方法可以达到与static
关键字是放置函数f()
in an 未命名的命名空间。根据 C++11 标准第 3.5/4 段:
未命名的命名空间或在未命名的命名空间内直接或间接声明的命名空间具有内部链接。所有其他名称空间都有外部链接。具有命名空间范围但未在上面给出内部链接的名称与封闭命名空间具有相同的链接,如果它是以下名称:
- 一个变量;或者
— 一个函数; or
— 命名类(第 9 条),或在 typedef 声明中定义的未命名类,其中该类具有用于链接目的的 typedef 名称(7.1.3);或者
— 命名枚举 (7.2),或 typedef 声明中定义的未命名枚举,其中枚举具有用于链接目的的 typedef 名称 (7.1.3);或者
— 属于具有链接的枚举的枚举器;或者
— 一个模板。
出于与上述相同的原因,inline
应该优先考虑关键字。