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

And*_*owl 70 c++ c++-faq include-guards header-files

关于包括警卫的两个常见问题:

  1. 第一个问题:

    为什么不包括保护我的头文件不受相互的,递归包含的警卫?我不断收到有关不存在的符号的错误,这些符号显然存在,甚至每次我写下类似的东西时都会出现更奇怪的语法错误:

    "啊"

    #ifndef A_H
    #define A_H
    
    #include "b.h"
    
    ...
    
    #endif // A_H
    
    Run Code Online (Sandbox Code Playgroud)

    "BH"

    #ifndef B_H
    #define B_H
    
    #include "a.h"
    
    ...
    
    #endif // B_H
    
    Run Code Online (Sandbox Code Playgroud)

    "的main.cpp"

    #include "a.h"
    int main()
    {
        ...
    }
    
    Run Code Online (Sandbox Code Playgroud)

    为什么我会收到编译"main.cpp"的错误?我需要做些什么来解决我的问题?


  1. 第二个问题:

    为什么不包括防止多个定义的警卫?例如,当我的项目包含两个包含相同标题的文件时,链接器有时会抱怨多次定义某个符号.例如:

    "header.h"

    #ifndef HEADER_H
    #define HEADER_H
    
    int f()
    {
        return 0;
    }
    
    #endif // HEADER_H
    
    Run Code Online (Sandbox Code Playgroud)

    "source1.cpp"

    #include "header.h"
    ...
    
    Run Code Online (Sandbox Code Playgroud)

    "source2.cpp"

    #include "header.h"
    ...
    
    Run Code Online (Sandbox Code Playgroud)

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

And*_*owl 129

第一个问题:

为什么不包括保护我的头文件不受相互的,递归包含的警卫?

他们是.

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

假设您的相互包含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()
{
    ...
}
Run Code Online (Sandbox Code Playgroud)

您会注意到编译器会在达到包含深度限制时报告失败.此限制是特定于实现的.根据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_H.由于宏A_H尚未定义,因此将继续处理以下文本.后续的指令(#defines A_H)定义了宏A_H.然后,预处理器将满足该指令#include "b.h":预处理器现在应处理头文件b.h,获取其处理结果,并#include用该结果替换该指令;
  3. 处理时b.h,预处理器将满足该指令#ifndef B_H.由于宏B_H尚未定义,因此将继续处理以下文本.后续的指令(#defines B_H)定义了宏B_H.然后,该指令#include "a.h"将告诉预处理器处理a.h并用预处理的结果替换#include指令;b.ha.h
  4. 编译器将a.h再次开始预处理,并再次满足该#ifndef A_H指令.但是,在先前的预处理期间,A_H已定义宏.因此,编译器这次将跳过以下文本,直到#endif找到匹配指令,并且此处理的输出是空字符串(#endif当然,假设没有任何内容遵循指令).因此,预处理器将使用空字符串替换#include "a.h"指令b.h,并将追溯执行,直到它替换原始#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()
{
    ...
}
Run Code Online (Sandbox Code Playgroud)

鉴于上面的标题,main.cpp将无法编译.

为什么会这样?

要了解发生了什么,再次完成步骤1-4就足够了.

很容易看出前三个步骤和第四步的大部分步骤都不受这种变化的影响(只需通读它们就可以确信).但是,在步骤4结束时发生了一些不同的事情:在用空字符串替换#include "a.h"指令后b.h,预处理器将开始解析内容b.h,特别是定义B.不幸的是,B提到类的定义,A之前从未遇到过,因为包含警卫!

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

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

你需要前瞻声明.

实际上,定义类的A,以限定类不需要B的,因为一个指针到A正在被声明作为成员变量,而不是类型的对象A.由于指针具有固定的大小,因此编译器不需要知道确切的布局,A也不需要计算其大小以正确定义类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
Run Code Online (Sandbox Code Playgroud)

你的main.cpp意志现在肯定会编译.几句话:

  1. 不仅通过用#include前向声明替换指令来打破相互包容,b.h足以有效地表达Bon 的依赖性A:尽可能使用前向声明/实际也被认为是一种很好的编程实践,因为它有助于避免不必要的包含,因此减少整体编译时间.然而,消除了相互夹杂物后,main.cpp将必须进行修改,以#include两者a.h和b.h(如果需要后者的话),因为b.h没有更多的间接#included到a.h;
  2. 虽然类的前向声明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()
{
    ...
}
Run Code Online (Sandbox Code Playgroud)

编译器肯定会抱怨f()重新定义.这很明显:它的定义被包含两次!但是,source1.cpp 当header.h包含适当的包含警卫时,上面的编译没有问题.这是预期的.

不过,即使在包括警卫都存在,编译器将停止与错误消息困扰你,则连接器将坚持一个事实,即合并来自汇编得到的目标代码时,被人发现多个定义source1.cpp和source2.cpp,并拒绝生成您可执行文件.

为什么会这样?

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

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

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

然而,当合并从.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); 要么

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

- 属于具有链接的枚举的枚举器; 要么

- 一个模板.

出于同样的原因,inline关键字应该是首选.

  • @AndyProwl - 通常的答案是社交病.不要让你失望.伟大的帖子...... +1 (4认同)
  • @AndyProwl感谢您抽出宝贵时间并撰写如此有用且广泛的解释,+ 1 (3认同)