为什么我的包含保护不能阻止递归包含和多个符号定义?

Why aren't my include guards preventing recursive inclusion and multiple symbol definitions?

提问人:Andy Prowl 提问时间:2/16/2013 最后编辑:DeduplicatorAndy Prowl 更新时间:10/14/2019 访问量:30916

问:

关于包括防护装置的两个常见问题:

  1. 第一个问题:

    为什么不包含保护我的头文件免受相互递归包含的影响?每次我写如下内容时,我都会不断收到关于不存在的符号的错误,这些符号显然存在,甚至更奇怪的语法错误:

    “啊”

    #ifndef A_H
    #define A_H
    
    #include "b.h"
    
    ...
    
    #endif // A_H
    

    “呵呵”

    #ifndef B_H
    #define B_H
    
    #include "a.h"
    
    ...
    
    #endif // B_H
    

    “主.cpp”

    #include "a.h"
    int main()
    {
        ...
    }
    

    为什么我在编译“main.cpp”时遇到错误?我需要做些什么来解决我的问题?


  1. 第二个问题:

    为什么不包含防护来防止多个定义?例如,当我的项目包含两个包含相同标头的文件时,有时链接器会抱怨某些符号被多次定义。例如:

    “标题.h”

    #ifndef HEADER_H
    #define HEADER_H
    
    int f()
    {
        return 0;
    }
    
    #endif // HEADER_H
    

    “来源1.cpp”

    #include "header.h"
    ...
    

    “来源2.cpp”

    #include "header.h"
    ...
    

    为什么会这样?我需要做些什么来解决我的问题?

C 头文件 C++-faq include-guards

评论

3赞 Luchian Grigore 2/16/2013
我看不出这与 stackoverflow.com/questions/553682/ 有什么不同...... 而且 stackoverflow.com/questions/14425262/......
1赞 Andy Prowl 2/16/2013
@LuchianGrigore:第一个问答与包含警卫没有直接关系,或者至少 IMO 没有解释为什么包含警卫会带来依赖关系的麻烦。第二个确实解决了两个问题之一(第二个),但方式不那么广泛和详细。我想把这两个关于包括警卫的问答放在一起,因为在我看来,它们密切相关。
1赞 Andy Prowl 2/18/2013
@sbi:我没问题,你去掉标签没问题。我只是说,由于这是关于 C++ 的常见问题,它应该被标记为 faq-c++。
1赞 Andy Prowl 2/18/2013
@sbi:嗯,在过去的几天里,我看到至少 4 个关于 SO 的问题,这些问题来自初学者,他们对多个定义或相互包含感到困惑,所以从我的观点来看,这是一个反复出现的问题。这就是为什么我一开始就费心写这整篇文章的原因:否则我为什么要为初学者写一个问答?但是,当然,我知道每个人都对“频繁”的东西有主观的看法,而我的看法可能与你不符。尽管我仍然认为这应该被标记为 c++-faq,但我并不反对具有更多经验的高代表用户来强制执行他的观点。
1赞 Jonathan Wakely 2/19/2013
对我来说似乎是一个常见问题解答

答:

136赞 Andy Prowl 2/16/2013 #1

第一个问题:

为什么不包含保护我的头文件免受相互递归包含的影响?

他们是

他们没有帮助的是相互包含的标头中数据结构定义之间的依赖关系。要了解这意味着什么,让我们从一个基本场景开始,看看为什么包含保护确实有助于相互包含。

假设您的相互包含和头文件包含琐碎的内容,即问题文本中代码部分中的省略号被替换为空字符串。在这种情况下,你会很高兴地编译。这要归功于你的警卫!a.hb.hmain.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 指令而被读取的源文件中,最高可达实现定义的嵌套限制

那么这是怎么回事呢?

  1. 解析时,预处理器将满足指令。该指令告诉预处理器处理头文件,获取该处理的结果,并将字符串替换为该结果;main.cpp#include "a.h"a.h#include "a.h"
  2. 在处理时,预处理器将满足指令,并且适用相同的机制:预处理器应处理头文件,获取其处理结果,并用该结果替换指令;a.h#include "b.h"b.h#include
  3. 在处理时,指令将告诉预处理器处理并用结果替换该指令;b.h#include "a.h"a.h
  4. 预处理器将再次开始解析,再次满足指令,这将建立一个潜在的无限递归过程。当达到临界嵌套级别时,编译器将报告错误。a.h#include "b.h"

但是,当存在包含保护时,不会在步骤 4 中设置无限递归。让我们看看为什么:

  1. (和以前一样)解析时,预处理器将满足指令。这告诉预处理器处理头文件,获取该处理的结果,并用该结果替换字符串;main.cpp#include "a.h"a.h#include "a.h"
  2. 在处理时,预处理器将满足指令。由于尚未定义宏,因此它将继续处理以下文本。后续指令 () 定义宏 。然后,预处理器将满足指令:预处理器现在应处理头文件,获取其处理结果,并用该结果替换指令;a.h#ifndef A_HA_H#defines A_HA_H#include "b.h"b.h#include
  3. 在处理时,预处理器将满足指令。由于尚未定义宏,因此它将继续处理以下文本。后续指令 () 定义宏 。然后,该指令将告诉预处理器处理并用预处理结果替换指令b.h#ifndef B_HB_H#defines B_HB_H#include "a.h"a.h#includeb.ha.h;
  4. 编译器将再次开始预处理,并再次满足指令。但是,在前面的预处理中,已经定义了宏。因此,编译器这次将跳过以下文本,直到找到匹配的指令,并且此处理的输出是空字符串(当然,假设指令后面没有任何东西)。因此,预处理器将用空字符串替换 中的指令,并跟踪执行,直到替换 中的原始指令。a.h#ifndef A_HA_H#endif#endif#include "a.h"b.h#includemain.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"b.hb.hBBA

当然,声明以前未声明的类型的成员变量是一个错误,编译器会礼貌地指出这一点。

我需要做些什么来解决我的问题?

您需要转发声明

实际上,为了定义 class ,不需要定义 class,因为指向的指针被声明为成员变量,而不是 类型的对象。由于指针具有固定大小,因此编译器不需要知道其确切的布局,也不需要计算其大小即可正确定义类。因此,转发声明类并让编译器知道它的存在就足够了:ABAAABAb.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

  1. 不仅通过用前向声明替换指令来破坏相互包含,就足以有效地表达 on 的依赖性:在可能/实际的情况下使用前向声明也被认为是一种良好的编程实践,因为它有助于避免不必要的包含,从而减少整体编译时间。但是,在消除相互包含之后,将不得不修改为两者和(如果后者需要),因为不再间接地通过#includeb.hBAmain.cpp#includea.hb.hb.h#includea.h;
  2. 虽然类的正向声明足以让编译器声明指向该类的指针(或在可接受不完整类型的任何其他上下文中使用它),但取消引用指针(例如调用成员函数)或计算其大小是对不完整类型的非法操作:如果需要,编译器需要提供完整的定义, 这意味着必须包含定义它的头文件。这就是为什么类定义及其成员函数的实现通常被拆分为头文件和该类的实现文件(类模板是此规则的例外):实现文件永远不会被项目中的其他文件所依赖,可以安全地使用所有必要的头文件来使定义可见。另一方面,头文件不会使用其他头文件,除非它们确实需要这样做(例如,使基类的定义可见),并且将尽可能/实际使用前向声明。AAA#include#include#include

第二个问题:

为什么不包含防护来防止多个定义

他们是

他们没有保护您免受不同翻译单元中的多个定义的影响。这在StackOverflow的问答中也有解释。

也看到了这一点,请尝试删除包含保护并编译以下修改后的版本(或,对于它的重要性):source1.cppsource2.cpp

//================================================
// source1.cpp
//
// Good luck getting this to compile...

#include "header.h"
#include "header.h"

int main()
{
    ...
}

编译器肯定会在这里抱怨被重新定义。这是显而易见的:它的定义被包括了两次!但是,header.h 包含正确的包含保护时,上述内容将毫无问题地编译。这是意料之中的。f()source1.cpp

尽管如此,即使存在包含保护,并且编译器将停止用错误消息打扰您,链接器仍将坚持在合并从 和 编译中获得的目标代码时找到多个定义的事实,并且会拒绝生成您的可执行文件。source1.cppsource2.cpp

为什么会这样?

基本上,项目中的每个文件(此处的技术术语是翻译单元)都是单独和独立编译的。解析文件时,预处理器将处理所有指令并扩展它遇到的所有宏调用,并且这种纯文本处理的输出将在输入中提供给编译器,以便将其转换为目标代码。一旦编译器完成为一个翻译单元生成目标代码,它将继续执行下一个翻译单元,并且在处理前一个翻译单元时遇到的所有宏定义都将被遗忘。.cpp.cpp#include

事实上,使用翻译单元(文件)编译项目就像执行相同的程序(编译器)次数,每次都使用不同的输入:同一程序的不同执行不会共享先前程序执行的状态。因此,每个翻译都是独立执行的,编译一个翻译单元时遇到的预处理器符号在编译其他翻译单元时不会被记住(如果你考虑一下,你会很容易意识到这实际上是一种理想的行为)。n.cppn

因此,即使包含保护可帮助您防止递归相互包含和同一标头在一个翻译单元中的冗余包含,它们也无法检测同一定义是否包含在不同的翻译单元中。

然而,当合并从项目的所有文件的编译中生成的对象代码时,链接器看到同一符号被多次定义,因为这违反了一个定义规则。根据 C++11 标准的第 3.2/3 段:.cpp

每个程序都应包含该程序中使用的每个非内联函数或变量的一个定义;无需诊断。定义可以显式出现在程序中,也可以在标准或用户定义的库中找到,或者(在适当的情况下)隐式定义(参见 12.1、12.4 和 12.8)。内联函数应在使用 ODR 的每个翻译单元中定义。

因此,链接器将发出错误并拒绝生成程序的可执行文件。

我需要做些什么来解决我的问题?

如果要将函数定义保留在由多个翻译单元 d 的头文件中(请注意,如果标头仅由一个翻译单元 d ,则不会出现问题),则需要使用关键字。#include#includeinline

否则,您只需要将函数的声明保留在 中,将其定义(body)放入一个单独的文件中(这是经典方法)。header.h.cpp

该关键字表示对编译器的非绑定请求,以直接在调用站点内联函数的主体,而不是为常规函数调用设置堆栈帧。尽管编译器不必满足请求,但关键字确实成功地告诉链接器允许多个符号定义。根据 C++11 标准第 3.2/5 段:inlineinline

类类型(第 9 条)、枚举类型 (7.2)、具有外部链接的内联函数 (7.1.2)、类模板(第 14 条)、非静态函数模板 (14.5.6)、类模板的静态数据成员 (14.5.1.3)、类模板的成员函数 (14.5.1.1) 或未指定某些模板参数的模板专用化 (14.7, 14.5.5) 在程序中,前提是每个定义出现在不同的翻译单元中,并且这些定义满足以下要求 [...]

上面的段落基本上列出了通常放在头文件中的所有定义,因为它们可以安全地包含在多个翻译单元中。相反,所有其他具有外部链接的定义都属于源文件。

使用关键字而不是关键字还可以通过为函数提供内部链接来抑制链接器错误,从而使每个翻译单元都包含该函数(及其局部静态变量)的私有副本。但是,这最终会导致更大的可执行文件,并且通常应首选使用 。staticinlineinline

实现与关键字相同的结果的另一种方法是将函数放在未命名的命名空间中。根据 C++11 标准的第 3.5/4 段:staticf()

未命名命名空间或在未命名命名空间中直接或间接声明的命名空间具有内部链接。所有其他命名空间都具有外部链接。具有命名空间范围的名称,如果上面没有给予内部链接,则该名称与封闭命名空间具有相同的链接,如果它是以下名称:

— 变量;或

函数;或

— 一个命名类(第 9 条),或在 typedef 声明中定义的未命名类,其中该类具有用于链接目的的 typedef 名称 (7.1.3);或

— 命名枚举 (7.2),或在 typedef 声明中定义的未命名枚举,其中枚举具有用于链接目的的 typedef 名称 (7.1.3);或

— 属于具有链接的枚举的枚举器;或

— 模板。

出于上述相同原因,应首选关键字。inline

评论

1赞 aschepler 2/19/2013
好。在讨论ODR的两种版本时,我想补充指出,引用的3.2/3列出了我们通常放在头文件中的定义,而所有其他带有外部链接的定义都属于源文件。然后是一份通俗易懂的清单,用于“哪种ODR适用于我的定义?
0赞 Andy Prowl 2/19/2013
@aschepler:你的意思是 3.2/4(“如果......,T 型必须是完整的”)还是 3.2/5(“类类型(第 9 条)、枚举类型(7.2)、具有外部链接的内联函数(7.1.2)、类模板(第 14 条)、[...],并且前提是定义满足以下要求 [...]”)?我认为同时提到这两者是有用的,另一方面,很难在足够短的时间内做到这一点,并且通过冗长的解释,重点将从包括警卫转移,这是本次问答的主题。也许有一个链接到这个的新FAQ条目?
4赞 Jim Balter 3/16/2013
@AndyProwl -- 通常的答案是社会病。不要让你失望。很棒的帖子... +1
1赞 Andy Prowl 6/4/2015
@Andrew:谢谢你,我很高兴你找到了能量:D
3赞 v.tralala 5/8/2017
@AndyProwl感谢您抽出宝贵时间撰写如此有用且广泛的解释,+1
-2赞 fiorentinoing 11/21/2018 #2

首先,您应该 100% 确定“包含防护”中没有重复项。

使用此命令

grep -rh "#ifndef" * 2>&1 | uniq -c | sort -rn | awk '{print $1 " " $3}' | grep -v "^1\ "

您将 1) 突出显示所有包含守卫,获取每个包含名称的计数器的唯一行,对结果进行排序,仅打印计数器和包含名称并删除真正唯一的守卫。

提示:这相当于获取重复的包含名称列表

-1赞 VonC 10/14/2019 #3

fiorentinoing 的答案在 Git 2.24(2019 年第 4 季度)中得到了回应,其中 Git 代码库中正在进行类似的代码清理。

请参阅 René Scharfe (rscharfecommit 2fe4439(2019 年 10 月 3 日)。
(由 Junio C Hamano -- gitster --提交 a4c5d9f 中合并,2019 年 10 月 11 日)

treewide:删除重复的指令#include

发现于:

git grep '^#include ' '*.c' | sort | uniq -d