第一个问题:
为什么不包括保护我的头文件不被相互递归包含的保护措施?
他们是。
他们没有帮助的是相互包含的标头中数据结构的定义之间的依赖关系。要了解这意味着什么,让我们从一个基本的场景开始,看看为什么包含保护确实有助于相互包含。
假设您的相互包含文件a.h
和b.h
头文件的内容很简单,即问题文本中代码段中的省略号被替换为空字符串。在这种情况下,您main.cpp
将很高兴进行编译。这只是感谢您的包括后卫!
如果您不确定,请尝试将其删除:
#include "b.h"
#include "a.h"
#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"
指令,这将建立潜在的无限递归过程。当达到临界嵌套级别时,编译器将报告错误。
但是,如果存在include防护,则在步骤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"
in中的指令b.h
,并将追溯执行直到它替换中的原始#include
指令main.cpp
。
因此,包括卫队确实防止相互包容。但是,它们不能帮助相互包含文件中的类定义之间的依赖关系:
#ifndef A_H
#define A_H
#include "b.h"
struct A
{
};
#endif
#ifndef B_H
#define B_H
#include "a.h"
struct B
{
A* pA;
};
#endif
#include "a.h"
int main()
{
...
}
鉴于以上标题,main.cpp
将无法编译。
为什么会这样呢?
要查看发生了什么,再次执行步骤1-4就足够了。
很容易看出,前三步和第四步的大部分都不受此更改的影响(只需通读它们就可以说服)。但是,在步骤4的末尾会发生一些变化:用空字符串替换#include "a.h"
in指令之后b.h
,预处理器将开始解析的内容b.h
,尤其是的定义B
。不幸的是,B
提到class的定义,A
正是由于包含防护措施才被实现!
声明一个成员变量的类型以前没有声明,这当然是一个错误,编译器会礼貌地指出这一点。
我该怎么做才能解决我的问题?
您需要前向声明。
实际上,定义类的A
,以限定类不需要B
的,因为一个指针到A
正在被声明作为成员变量,而不是类型的对象A
。由于指针具有固定的大小,因此编译器不需要知道的确切布局,A
也不需要计算其大小即可正确定义class B
。因此,它是足够前瞻性声明类A
中b.h
,使编译器知道它的存在:
#ifndef B_H
#define B_H
struct A;
struct B
{
A* pA;
};
#endif
您main.cpp
现在将肯定可以编译。一些注意事项:
- 通过用
#include
in的前向声明替换指令来打破相互包含不仅b.h
足以有效地表达B
on的依赖关系A
:在可能/可行的情况下使用前向声明也被认为是良好的编程习惯,因为它有助于避免不必要的包含,因此减少整体编译时间。然而,消除了相互夹杂物后,main.cpp
将必须进行修改,以#include
两者a.h
和b.h
(如果后者需要在所有),因为b.h
没有更多的间接#include
至D a.h
;
- 尽管类的前向声明
A
足以使编译器声明指向该类的指针(或在可接受不完整类型的任何其他上下文中使用它),但取消引用指针A
(例如调用成员函数)或计算其大小是对不完整类型的非法操作:如果需要,A
编译器需要可用的完整定义,这意味着必须包括定义它的头文件。这就是为什么通常将类定义及其成员函数的实现拆分为该类的头文件和实现文件(类模板是该规则的例外)的原因:实现文件,#include
项目中的其他文件从不执行,可以安全地#include
使定义可见的所有必要标头。另一方面,#include
头文件不会其他头文件,除非它们确实需要这样做(例如,使基类的定义可见),并且将在可能/可行的情况下使用前向声明。
第二个问题:
为什么不包括防止多个定义的防护措施?
他们是。
他们没有保护您免受单个翻译单元中的多个定义的伤害。在StackOverflow的此问答中也对此进行了说明。
看到的太少了,请尝试删除包含保护并编译以下修改过的版本source1.cpp
(或source2.cpp
,对于重要内容):
#include "header.h"
#include "header.h"
int main()
{
...
}
编译器肯定会在这里抱怨f()
重新定义。显而易见:其定义被两次包含!但是,source1.cpp
当header.h
包含适当的包含保护时,以上内容将毫无问题地进行编译。那是意料之中的。
不过,即使在包括警卫都存在,编译器将停止与错误消息困扰你,则连接器将坚持一个事实,即合并来自汇编得到的目标代码时,被人发现多个定义source1.cpp
和source2.cpp
,并拒绝生成您可执行文件。
为什么会这样呢?
基本上,项目中的每个.cpp
文件(在此上下文中的技术术语是翻译单元)都是分别独立编译的。解析.cpp
文件时,预处理器将处理所有#include
指令并扩展其遇到的所有宏调用,并且此纯文本处理的输出将在输入中提供给编译器,以将其转换为目标代码。一旦编译器完成了为一个翻译单元生成目标代码的工作,它将继续进行下一个翻译单元,并且在处理前一个翻译单元时遇到的所有宏定义都将被遗忘。
实际上,使用n
翻译单元(.cpp
文件)编译项目就像执行同一程序(编译器)一样n
,每次都使用不同的输入:同一程序的不同执行不会共享先前程序执行的状态)。因此,每个翻译都是独立执行的,并且在编译其他翻译单元时,将不会记住在编译一个翻译单元时遇到的预处理器符号(如果暂时考虑一下,您将很容易意识到这实际上是一种理想的行为)。
因此,即使包含保护有助于您防止一个翻译单元中同一标头的递归互斥和冗余包含,它们也无法检测到同一定义是否包含在不同的翻译单元中。
但是,当合并从.cpp
项目的所有文件的编译生成的目标代码时,链接器将看到多次定义同一符号,因为这违反了一个定义规则。根据C ++ 11标准的第3.2 / 3段:
每个程序应准确地包含该程序中使用的每个非内联函数或变量的一个定义;无需诊断。该定义可以显式显示在程序中,可以在标准库或用户定义的库中找到,或者(在适当的情况下)可以隐式定义(请参见12.1、12.4和12.8)。内联函数应在使用的每个翻译单元中定义。
因此,链接器将发出错误并拒绝生成程序的可执行文件。
我该怎么做才能解决我的问题?
如果要将函数定义保留在#include
由多个转换单元d组成的头文件中(请注意,如果您的标头#include
仅由一个转换单元d不会出现问题),则需要使用inline
关键字。
否则,您只需要在中保留函数的声明header.h
,将其定义(正文)仅放入一个单独的.cpp
文件中(这是经典方法)。
的inline
关键字表示非绑定请求编译器直接在调用位置内联函数的身体,而不是建立一个堆栈帧用于常规函数调用。尽管编译器不必满足您的请求,但inline
关键字确实可以告诉链接程序允许多个符号定义。根据C ++ 11标准的3.2 / 5段:
一个类类型(第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
关键字还可以通过给您的函数内部链接来抑制链接器错误,从而使每个翻译单元都拥有该函数(及其局部静态变量)的私有副本。但是,这最终会导致更大的可执行文件,inline
通常应首选使用。
获得与static
关键字相同的结果的另一种方法是将函数f()
放在未命名的名称空间中。根据C ++ 11标准的第3.5 / 4段:
未命名的名称空间或在未命名的名称空间中直接或间接声明的名称空间具有内部链接。所有其他名称空间都有外部链接。如果名称空间范围的名称是以下名称,则没有在上面内部进行链接的名称空间范围与封闭的名称空间具有相同的链接:
- 一个变量; 要么
—功能;要么
—一个命名类(第9条),或在typedef声明中定义的未命名类,其中该类具有用于链接目的的typedef名称(7.1.3);要么
—在typedef声明中定义的命名枚举(7.2)或未命名的枚举,其中该枚举具有用于链接目的的typedef名称(7.1.3);要么
—属于具有链接的枚举的枚举数;要么
—模板。
出于与上述相同的原因,inline
应首选关键字。