首页 文章

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

提问于
浏览
66

关于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 回答

  • 125

    第一个问题:为什么不包括保护我的头文件不受相互的,递归包含的警卫?

    They are .

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

    假设您的相互包含 a.hb.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指令而被读取,直到实现定义的嵌套限制 .

    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 . 但是,他们无法帮助相互包含文件中类的定义之间的依赖关系:

    //================================================
    // 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()
    {
        ...
    }
    

    鉴于上述 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-declareA 就足够了,并使编译器意识到它的存在:

    //================================================
    // 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 现在肯定会编译 . 几句话:

    • 不仅通过用 b.h 中的前向声明替换 #include 指令来打破相互包含,足以有效地表达 BA 的依赖性:使用前向声明尽可能/实际也被认为是 good programming practice ,因为它有助于避免不必要的夹杂物,从而减少整体编译时间 . 但是,在消除相互包含之后,必须将 main.cpp 修改为 #include a.hb.h (如果后者根本需要),因为 b.h 不再间接 #include d到 a.h ;

    • 虽然类 A 的前向声明足以让编译器声明指向该类的指针(或者在可接受不完整类型的任何其他上下文中使用它),但是解引用指向 A 的指针(例如调用成员函数)或计算它的大小是对不完整类型的非法操作:如果需要,编译器需要 A 的完整定义,这意味着必须包含定义它的头文件 . 这就是为什么类定义及其成员函数的实现通常被拆分为头文件和该类的实现文件(类模板是此规则的一个例外):实现文件,它们永远不会被其他文件所覆盖 . 项目,可以安全 #include 所有必要的 Headers ,使定义可见 . 另一方面,头文件不会 #include 其他头文件,除非他们真的需要这样做(例如,使基类的定义可见),并且只要可能/实际使用前向声明 .

    第二个问题:为什么不包括防止多个定义的警卫?

    They are .

    他们没有保护您的是在单独的翻译单元中的多个定义 . 这也在StackOverflow的this Q&A中进行了解释 .

    太看到了,尝试删除包含警卫并编译以下修改版本的 source1.cpp (或 source2.cpp ,因为它重要):

    //================================================
    // source1.cpp
    //
    // Good luck getting this to compile...
    
    #include "header.h"
    #include "header.h"
    
    int main()
    {
        ...
    }
    

    编译器肯定会抱怨 f() 被重新定义 . 这很明显:它的定义被包含两次!但是,以上 source1.cpp will compile without problems when header.h contains the proper include guards . 这是预期的 .

    尽管如此,即使存在包含保护并且编译器将不再使用错误消息打扰您,链接器仍将坚持在找到多个定义时合并从 source1.cppsource2.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段:

    每个程序应该只包含该程序中使用的每个非内联函数或变量的一个定义;无需诊断 . 该定义可以在程序中明确显示,可以在标准或用户定义的库中找到,或者(在适当的时候)隐式定义(见12.1,12.4和12.8) . 内联函数应在每个使用它的翻译单元中定义 .

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

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

    如果您希望将函数定义保存在多个翻译单元的头文件中(注意,如果您的 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段:

    类类型(第9条),枚举类型(7.2),带内部链接的内联函数(7.1.2),类模板(第14条),非静态函数模板(14.5.6)可以有多个定义),类模板的静态数据成员(14.5.1.3),类模板的成员函数(14.5.1.1),或模板特化,在程序中未指定某些模板参数(14.7,14.5.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段:

    未命名的命名空间或直接或间接声明的命名空间未命名的命名空间具有内部链接 . 所有其他名称空间都有外部链接 . 如果名称具有以下名称,则具有未在上面给出内部链接的命名空间范围的名称与封闭命名空间具有相同的链接: - 变量;或 - 一个功能;或 - 命名类(第9条),或在typedef声明中定义的未命名类,其中类具有用于链接目的的typedef名称(7.1.3);或 - 命名枚举(7.2),或在typedef声明中定义的未命名枚举,其中枚举具有用于链接目的的typedef名称(7.1.3);或 - 属于具有链接的枚举的枚举器;或 - 模板 .

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

  • -1

    首先,你应该100%确定你在“包含警卫”中没有重复 .

    使用此命令

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

    你将1)突出显示所有包含警卫,获得包含每个包含名称的计数器的唯一行,对结果进行排序,仅打印计数器并包含名称并删除那些非常独特的名称 .

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

相关问题