关于include guards的两个常见问题:
- FIRST QUESTION:
为什么不包括保护我的头文件的警卫 mutual, recursive inclusion ?我不断收到有关不存在的符号的错误,这些符号显然存在,甚至每次我写下类似的东西时都会出现更奇怪的语法错误:
“啊”
#ifndef A_H
#define A_H
#include "b.h"
...
#endif // A_H
“b.h”
#ifndef B_H
#define B_H
#include "a.h"
...
#endif // B_H
“的main.cpp”
#include "a.h"
int main()
{
...
}
为什么我会收到编译“main.cpp”的错误?我需要做些什么来解决我的问题?
- SECOND QUESTION:
为什么不包括防范 multiple definitions ?例如,当我的项目包含两个包含相同 Headers 的文件时,链接器有时会抱怨多次定义某个符号 . 例如:
“header.h”
#ifndef HEADER_H
#define HEADER_H
int f()
{
return 0;
}
#endif // HEADER_H
“source1.cpp”
#include "header.h"
...
“source2.cpp”
#include "header.h"
...
为什么会这样?我需要做些什么来解决我的问题?
2 回答
They are .
他们没有帮助的是相互包含 Headers 中数据结构定义之间的依赖关系 . 要了解这意味着什么,让我们从一个基本场景开始,看看为什么包括警卫确实有助于相互包含 .
假设您的相互包含
a.h
和b.h
头文件具有琐碎的内容,即问题文本的代码部分中的省略号将替换为空字符串 . 在这种情况下,您的main.cpp
将很乐意编译 . 这只是因为你的包括警卫!如果您不相信,请尝试删除它们:
您会注意到编译器会在达到包含深度限制时报告失败 . 此限制是特定于实现的 . 根据C 11标准第16.2 / 6段:
So what's going on ?
解析
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"
指令,这将 Build 一个潜在无限的递归过程 . 达到关键嵌套级别时,编译器将报告错误 .但是,在步骤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
并将b.h
中的#include
指令替换为预处理a.h
的结果;编译器将再次开始预处理
a.h
,并再次满足#ifndef A_H
指令 . 但是,在先前的预处理期间,已定义宏A_H
. 因此,编译器将跳过以下文本,直到匹配#endif
找到指令,并且此处理的输出是空字符串(当然,假设#endif
指令之后没有任何内容) . 因此,预处理器将使用空字符串替换b.h
中的#include "a.h"
指令,并将追溯执行,直到它替换main.cpp
中的原始#include
指令 .因此, include guards do protect against mutual inclusion . 但是,他们无法帮助相互包含文件中类的定义之间的依赖关系:
鉴于上述 Headers ,
main.cpp
将无法编译 .要了解发生了什么,再次完成步骤1-4就足够了 .
很容易看出前三个步骤和第四步的大部分步骤都不受这种变化的影响(只需通读它们就可以确信) . 但是,在步骤4结束时发生了一些不同的事情:在用空字符串替换
b.h
中的#include "a.h"
指令之后,预处理器将开始解析b.h
的内容,特别是B
的定义 . 不幸的是,B
的定义提到了类A
,这是因为包含警卫之前从未见过的!声明一个先前未声明的类型的成员变量当然是一个错误,编译器会礼貌地指出它 .
你需要forward declarations .
实际上,为了定义类
B
,类A
的定义不是必需的,因为指向A
的指针被声明为成员变量,而不是A
类型的对象 . 由于指针具有固定大小,因此编译器不需要知道A
的确切布局,也不需要计算其大小以正确定义类B
. 因此,b.h
中的 forward-declare 类A
就足够了,并使编译器意识到它的存在:您的
main.cpp
现在肯定会编译 . 几句话:不仅通过用
b.h
中的前向声明替换#include
指令来打破相互包含,足以有效地表达B
对A
的依赖性:使用前向声明尽可能/实际也被认为是 good programming practice ,因为它有助于避免不必要的夹杂物,从而减少整体编译时间 . 但是,在消除相互包含之后,必须将main.cpp
修改为#include
a.h
和b.h
(如果后者根本需要),因为b.h
不再间接#include
d到a.h
;虽然类
A
的前向声明足以让编译器声明指向该类的指针(或者在可接受不完整类型的任何其他上下文中使用它),但是解引用指向A
的指针(例如调用成员函数)或计算它的大小是对不完整类型的非法操作:如果需要,编译器需要A
的完整定义,这意味着必须包含定义它的头文件 . 这就是为什么类定义及其成员函数的实现通常被拆分为头文件和该类的实现文件(类模板是此规则的一个例外):实现文件,它们永远不会被其他文件所覆盖 . 项目,可以安全#include
所有必要的 Headers ,使定义可见 . 另一方面,头文件不会#include
其他头文件,除非他们真的需要这样做(例如,使基类的定义可见),并且只要可能/实际使用前向声明 .They are .
他们没有保护您的是在单独的翻译单元中的多个定义 . 这也在StackOverflow的this Q&A中进行了解释 .
太看到了,尝试删除包含警卫并编译以下修改版本的
source1.cpp
(或source2.cpp
,因为它重要):编译器肯定会抱怨
f()
被重新定义 . 这很明显:它的定义被包含两次!但是,以上source1.cpp
will compile without problems when header.h contains the proper include guards . 这是预期的 .尽管如此,即使存在包含保护并且编译器将不再使用错误消息打扰您,链接器仍将坚持在找到多个定义时合并从
source1.cpp
和source2.cpp
的编译中获取的目标代码,并拒绝生成您的可执行文件 .基本上,您项目中的每个
.cpp
文件(此上下文中的技术术语是翻译单元)都是单独编译的 . 解析.cpp
文件时,预处理器将处理所有#include
指令并展开它遇到的所有宏调用,并且此纯文本处理的输出将在编译器的输入中给出,以便将其转换为目标代码 . 一旦编译器完成为一个翻译单元生成目标代码,它将继续下一个翻译单元,并且将忘记在处理前一个翻译单元时遇到的所有宏定义 .实际上,使用
n
转换单元(.cpp
文件)编译项目就像执行相同的程序(编译器)n
次,每次使用不同的输入:同一程序的不同执行 won't share the state of the previous program execution(s) . 因此,每个翻译都是独立执行的,编译一个翻译单元时遇到的预处理程序符号在编译其他翻译单元时将不会被记住(如果你考虑一下,你很容易意识到这实际上是一种理想的行为) .因此,即使包含保护可以帮助您防止在一个转换单元中同一标头的递归相互包含和冗余包含,它们也无法检测相同的定义是否包含在不同的转换单元中 .
然而,当合并从项目的所有
.cpp
文件的编译生成的目标代码时,链接器将看到相同的符号被定义多次,并且因为这违反了One Definition Rule . 根据C 11标准的第3.2 / 3段:因此,链接器将发出错误并拒绝生成程序的可执行文件 .
如果您希望将函数定义保存在多个翻译单元的头文件中(注意,如果您的 Headers 仅由一个翻译单元
#include
d,则不会出现问题),您需要使用inline
关键字 .否则,您需要在
header.h
中仅保留函数的声明,将其定义(正文)仅放在一个单独的.cpp
文件中(这是经典方法) .inline
关键字表示对编译器的非绑定请求,以内联函数's body directly at the call site, rather than setting up a stack frame for a regular function call. Although the compiler doesn' t必须满足您的请求,inline
关键字确实成功告诉链接器容忍多个符号定义 . 根据C 11标准第3.2 / 5段:The above Paragraph basically lists all the definitions which are commonly put in header files ,因为它们可以安全地包含在多个翻译单元中 . 相反,所有其他具有外部链接的定义都属于源文件 .
使用
static
关键字而不是inline
关键字也会通过赋予函数internal linkage来抑制链接器错误,从而使每个转换单元保存该函数(及其本地静态变量)的私有副本 . 但是,这最终会导致更大的可执行文件,并且通常应优先使用inline
.实现与
static
关键字相同结果的另一种方法是将函数f()
放在未命名的命名空间中 . 根据C 11标准第3.5 / 4段:出于上述相同的原因,应首选
inline
关键字 .首先,你应该100%确定你在“包含警卫”中没有重复 .
使用此命令
你将1)突出显示所有包含警卫,获得包含每个包含名称的计数器的唯一行,对结果进行排序,仅打印计数器并包含名称并删除那些非常独特的名称 .
提示:这相当于获取重复包含名称的列表