为什么不包括防止recursion包含和多个符号定义?

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

  1. 第一个问题:

    为什么不包括保护我的头文件不被相互recursion包含的保护 ? 我不断地收到有关显然存在的不存在的符号的错误,甚至每次我写下类似下面的语句错误,

    “啊”

    #ifndef A_H #define A_H #include "bh" ... #endif // A_H 

    “BH”

     #ifndef B_H #define B_H #include "ah" ... #endif // B_H 

    “的main.cpp”

     #include "ah" int main() { ... } 

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


  1. 第二个问题:

    为什么不包括防止多重定义的守卫? 例如,当我的项目包含两个包含相同头文件的文件时,有时候连接器会抱怨多次定义了一些符号。 例如:

    “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" ... 

    为什么发生这种情况? 我需要做些什么来解决我的问题?

第一个问题:

为什么不包括保护我的头文件不被相互recursion包含的保护

他们是

他们没有帮助的是相互包含的头文件中数据结构的定义之间的依赖关系 。 看看这意味着什么,让我们从一个基本的场景开始,看看为什么包括卫兵帮助互相包容。

假设你的相互包含的ahbh头文件有简单的内容,即问题文本代码段中的省略号被replace为空string。 在这种情况下,你的main.cpp会很高兴地编译。 而这只能归功于你的守卫!

如果您不确定,请尝试删除它们:

 //================================================ // ah #include "bh" //================================================ // bh #include "ah" //================================================ // main.cpp // // Good luck getting this to compile... #include "ah" int main() { ... } 

您会注意到编译器将在达到包含深度限制时报告失败。 此限制是特定于实现的。 根据C ++ 11标准的第16.2 / 6段:

#include预处理指令可能出现在由于另一个文件中的#include指令而被读取的源文件中, 直到实现定义的嵌套限制

那么发生了什么事

  1. 在parsingmain.cpp ,预处理器将会遇到指令#include "ah" 。 这个指令告诉预处理程序处理头文件ah ,把处理结果,用结果replacestring#include "ah" ;
  2. 在处理ah ,预处理程序会遇到#include "bh"指令,同样的机制也适用:预处理程序将处理头文件bh ,取其处理结果,并用#include指令replace结果;
  3. 在处理bh ,指令#include "ah"会告诉预处理器处理ah并用结果replace该指令;
  4. 预处理器将再次开始parsing,再次遇到#include "bh"指令,这将设置一个潜在的无限recursion过程。 当达到临界嵌套级别时,编译器将报告错误。

当包含警卫出现时 ,然而,在步骤4中将不会设置无限recursion。让我们来看看为什么:

  1. 和以前一样 )parsingmain.cpp ,预处理器将会遇到#include "ah"指令。 这告诉预处理器处理头文件ah ,取得处理结果,并用结果replacestring#include "ah" ;
  2. 在处理ah ,预处理器将会符合指令#ifndef A_H 。 由于macrosA_H尚未定义,它将继续处理以下文本。 随后的指令( #defines A_H )定义macrosA_H 。 然后,预处理程序将符合指令#include "bh" :预处理程序现在应该处理头文件bh ,取其处理结果,并用该结果replace#include指令;
  3. 处理bh ,预处理器将符合指令#ifndef B_H 。 由于macrosB_H尚未定义,它将继续处理以下文本。 随后的指令( #defines B_H )定义macrosB_H 。 然后,指令#include "ah"会告诉预处理器处理ah并用bh预处理的结果来replacebh#include指令。
  4. 编译器会再次开始预处理ah ,并再次遇到#ifndef A_H指令。 但是,在之前的预处理中,已经定义了macrosA_H 。 因此,这次编译器将跳过以下文本,直到find匹配的#endif指令,并且此处理的输出是空string(当然,假设没有任何后面的#endif指令)。 因此,预处理程序将用bhreplacebh#include "ah"指令,并追溯执行,直到它replacemain.cpp原来的#include指令。

因此, 包括卫兵防止相互包容 。 但是,它们无法帮助在相互包含的文件中定义类之间的依赖关系

 //================================================ // ah #ifndef A_H #define A_H #include "bh" struct A { }; #endif // A_H //================================================ // bh #ifndef B_H #define B_H #include "ah" struct B { A* pA; }; #endif // B_H //================================================ // main.cpp // // Good luck getting this to compile... #include "ah" int main() { ... } 

鉴于上面的头文件, main.cpp不会编译。

为什么发生这种情况?

为了看看发生了什么,只要再次执行步骤1-4就足够了。

很容易看出,前三步和第四步的大部分都不受这种变化的影响(只要仔细阅读就可以确信)。 但是,在步骤4结束时会发生一些不同:在用bhreplacebh#include "ah"指令后,预处理器将开始parsingbh的内容,特别是B的定义。 不幸的是, B的定义提到了A级,这正是由于包容性警卫而未曾遇到的!

声明一个以前没有声明过的types的成员variables当然是一个错误,编译器会礼貌地指出。

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

你需要转发声明

实际上,为了定义类B ,不需要类A定义,因为指向 A指针被声明为成员variables,而不是typesA的对象。 由于指针具有固定的大小,因此编译器不需要知道A的确切布局,也不需要计算其大小,以正确定义B类。 因此,在bh 转发A是足够的,并使编译器知道它的存在:

 //================================================ // bh #ifndef B_H #define B_H // Forward declaration of A: no need to #include "ah" struct A; struct B { A* pA; }; #endif // B_H 

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

  1. 不仅通过用bh的前向声明replace#include指令来打破相互包含,足以有效地expressionBA的依赖关系:只要可能/实际使用前向声明也被认为是一种很好的编程实践 ,因为它有助于避免不必要的包含,从而缩短整体编译时间。 但是,在消除相互包含之后, main.cpp将不得不被修改为#include ahbh (如果后者是所有需要的),因为bh不再是间接的#include d通过ah ;
  2. 虽然类A的前向声明足以让编译器声明指向这个类的指针(或者在任何其他可以接受不完全types的上下文中使用它),解除指向A指针(例如调用成员函数)或者计算它的大小是不完整的types的非法操作:如果需要的话, A的完整定义需要可用于编译器,这意味着定义它的头文件必须包括在内。 这就是为什么类定义和成员函数的实现通常被分割成一个头文件和一个该类的实现文件(类模板是这个规则的一个例外):实现文件,这些文件永远不会被其他文件包含该项目,可以安全#include所有必要的标题,使定义可见。 另一方面,头文件不会包含其他头文件, 除非他们真的需要这样做(例如,为了使基类的定义可见),并且在可能/实际的时候使用前向声明。

第二个问题:

为什么不包括防止多重定义的守卫?

他们是

他们不能保护你的是在不同的翻译单位中有多个定义。 这也是在StackOverflow的问答中解释的。

看到这一点,尝试删除包括守卫和编译以下,修改版本的source1.cpp (或source2.cpp ,为什么重要):

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

编译器肯定会在这里抱怨f()被重新定义。 这是显而易见的:其定义包括两次! 但是, header.h包含适当的包含防护时 ,上面的source1.cpp 将会编译时没有问题 。 这是预料之中的。

尽pipe如此,即使包含守护程序存在,编译器也不会再用错误消息打扰你, 链接器会坚持这样一个事实,即在合并从source1.cppsource2.cpp编译得到的目标代码时会发现多个定义,并会拒绝生成您的可执行文件。

为什么发生这种情况?

基本上,您的项目中的每个.cpp文件(在这个上下文中的技术术语是翻译单元 )是分别独立编译的。 parsing一个.cpp文件时,预处理器将处理所有的#include指令,并展开它所遇到的所有macros调用,这个纯文本处理的输出将被input到编译器中,以便将其转换成目标代码。 一旦编译器完成了为一个翻译单元生成目标代码的工作,它将继续下一个翻译单元,并且在处理先前的翻译单元时遇到的所有macros定义将被遗忘。

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

因此,尽pipe包括警卫在内,可以帮助您防止在一个翻译单元中同一标题的recursion相互包含和冗余包含,但它们无法检测相同的定义是否包含在不同的翻译单元中。

然而,当合并从项目的所有.cpp文件编译生成的目标代码时,链接程序看到相同的符号被定义了多次,因为这违反了One Definition Rule 。 根据C ++ 11标准的第3.2 / 3段:

每个程序应该包含每个非内联函数或程序中使用的variables的一个定义; 不需要诊断。 该定义可以在程序中显式出现,可以在标准库或用户定义的库中find,或者(在适当时)隐式定义(见12.1,12.4和12.8)。 内联函数应在每个使用它的翻译单元中定义

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

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

如果你想把你的函数定义保存在一个包含多个翻译单元的#include d的头文件中(注意,如果你的头文件只包含一个翻译单元,就不会出现问题),你需要使用inline关键字。

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

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

可以有多个类types(第9章),枚举types(7.2), 带外部链接的内联函数 (7.1.2),类模板(第14章),非静态函数模板(14.5.6) ,一个类模板的静态数据成员(14.5.1.3),一个类模板的成员函数(14.5.1.1),或者某个模板参数没有被指定的模板特化(14.7,14.5.5)定义出现在不同的翻译单元中,并且提供的定义满足下列要求[…]

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

使用static关键字而不是inline关键字也可以通过给你的函数内部链接来阻止链接器错误,从而使得每个翻译单元都拥有该函数(及其本地静态variables)的私有副本 。 但是,这最终会导致更大的可执行文件,并且通常使用inline应该是首选。

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

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

– 一个variables; 要么

一个function ; 要么

– 一个已命名的类(第9章),或者是一个在typedef声明中定义的未命名的类,其中该类具有用于链接目的的typedef名称(7.1.3); 要么

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

– 一个枚举,属于枚举与链接; 要么

– 一个模板。

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