第一个问题:
为什么不包含保护我的头文件免受相互递归包含的保护?
他们是。
他们没有帮助的是相互包含的标头中数据结构定义之间的依赖关系。要了解这意味着什么,让我们从一个基本场景开始,看看为什么包含守卫确实有助于相互包含。
假设您的相互包含文件a.h
和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"
in 指令b.h
,并将追溯执行,直到它替换 .in 中的原始#include
指令main.cpp
。
因此,包含守卫确实可以防止相互包含。但是,它们无法帮助解决相互包含文件中类定义之间的依赖关系:
//================================================
// 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"
指令后,预处理器将开始解析 的内容,特别是. 不幸的是,提到了 class的定义,这完全是因为包含保护!b.h
b.h
B
B
A
声明一个之前没有声明过的类型的成员变量当然是一个错误,编译器会礼貌地指出这一点。
我需要做什么来解决我的问题?
您需要前向声明。
实际上,定义classA
不需要定义 class B
,因为指向的指针被A
声明为成员变量,而不是 type 的对象A
。由于指针的大小是固定的,编译器不需要知道确切的布局,A
也不需要计算它的大小来正确定义 class B
。因此,向前声明类A
并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
你main.cpp
现在肯定会编译。几点说明:
- 不仅通过用
#include
前向声明替换指令来打破相互包含b.h
足以有效地表达 on 的依赖关系B
:A
尽可能/实用地使用前向声明也被认为是一种良好的编程习惯,因为它有助于避免不必要的包含,因此减少整体编译时间。然而,在消除相互包含之后,main.cpp
将不得不修改为#include
两者a.h
和b.h
(如果需要后者),因为b.h
不再#include
通过a.h
;
- 虽然类的前向声明
A
足以让编译器声明指向该类的指针(或在可接受不完整类型的任何其他上下文中使用它),但取消引用指针A
(例如调用成员函数)或计算其大小是对不完整类型的非法操作:如果需要,A
编译器需要获得完整的定义,这意味着必须包含定义它的头文件。这就是为什么类定义及其成员函数的实现通常被拆分为该类的头文件和实现文件(类模板是此规则的例外):实现文件,#include
项目中的其他文件永远不会, 可以安全#include
使定义可见的所有必要标题。另一方面,头文件不会#include
其他头文件,除非它们确实需要这样做(例如,使基类的定义可见),并且将尽可能/实用地使用前向声明。
第二个问题:
为什么不包括防止多个定义的警卫?
他们是。
他们没有保护您免受不同翻译单元中的多个定义的影响。这在 StackOverflow 上的问答中也有解释。
太看到了,尝试删除包含防护并编译以下修改版本source1.cpp
(或source2.cpp
,重要的是):
//================================================
// source1.cpp
//
// Good luck getting this to compile...
#include "header.h"
#include "header.h"
int main()
{
...
}
编译器肯定会在这里抱怨f()
被重新定义。这很明显:它的定义被包含了两次!但是,当包含正确的 include guards时,上面的source1.cpp
代码将毫无问题地编译header.h
。这是预期的。
尽管如此,即使存在包含保护并且编译器将不再用错误消息打扰您,链接器source1.cpp
仍会坚持在合并从and的编译中获得的目标代码时会找到多个定义source2.cpp
,并且会拒绝生成您的可执行。
为什么会这样?
基本上,项目中的每个.cpp
文件(这里的技术术语是翻译单元)都是单独独立编译的。解析.cpp
文件时,预处理器将处理所有#include
指令并展开它遇到的所有宏调用,并且这种纯文本处理的输出将作为输入提供给编译器,以将其翻译成目标代码。一旦编译器完成了为一个翻译单元生成目标代码,它将继续执行下一个翻译单元,并且在处理前一个翻译单元时遇到的所有宏定义都将被遗忘。
实际上,使用n
翻译单元(.cpp
文件)编译项目就像执行相同的程序(编译器)n
多次,每次使用不同的输入:相同程序的不同执行不会共享先前程序执行的状态(s ) . 因此,每个翻译都是独立执行的,编译一个翻译单元时遇到的预处理器符号在编译其他翻译单元时不会被记住(如果你想一想,你会很容易意识到这实际上是一种可取的行为)。
因此,即使包含保护可以帮助您防止在一个翻译单元中递归相互包含和冗余包含相同的标头,它们也无法检测相同的定义是否包含在不同的翻译单元中。
.cpp
然而,当合并项目所有文件编译生成的目标代码时,链接器将看到同一个符号被多次定义,因为这违反了一个定义规则。根据 C++11 标准的第 3.2/3 段:
每个程序都应包含该程序中 odr 使用的每个非内联函数或变量的准确定义;无需诊断。定义可以显式出现在程序中,可以在标准或用户定义库中找到,或者(在适当时)隐式定义(参见 12.1、12.4 和 12.8)。内联函数应在使用它的每个翻译单元中定义。
因此,链接器将发出错误并拒绝生成程序的可执行文件。
我需要做什么来解决我的问题?
如果你想将你的函数定义保存在一个#include
由多个#include
翻译单元组成的头文件中(注意,如果你的头文件只由一个翻译单元组成,不会出现问题),你需要使用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);或者
— 命名枚举 (7.2),或在 typedef 声明中定义的未命名枚举,其中枚举具有用于链接目的的 typedef 名称 (7.1.3);或者
— 属于具有链接的枚举的枚举数;或者
——一个模板。
出于与上述相同的原因,inline
应首选关键字。