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

2023-12-13

两个常见问题包括警卫:

  1. 第一个问题:

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

    "a.h"

    #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
    

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

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


第一个问题:

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

They are.

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

假设你的相互包含a.h and 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()
{
    ...
}

您会注意到,当达到包含深度限制时,编译器将报告失败。此限制是特定于实现的。根据 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.h与预处理的结果a.h;
  4. 编译器将开始预处理a.h再次,并遇见#ifndef A_H再次指令。然而,在之前的预处理过程中,宏A_H已被定义。因此,编译器这次会跳过下面的文本,直到匹配到#endif找到指令,并且此处理的输出是空字符串(假设后面没有任何内容)#endif当然是指令)。因此,预处理器将取代#include "a.h"指令中b.h与空字符串,并将追溯执行,直到它替换原来的#include指令中main.cpp.

Thus, 包括警卫确实可以防止相互包容。然而,他们无能为力类的定义之间的依赖关系在相互包含的文件中:

//================================================
// 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.h对于空字符串,预处理器将开始解析b.h并且,特别是,的定义B。不幸的是,定义B提到类A,以前从未遇到过because包含守卫!

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

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

你需要前向声明.

事实上,定义班级的A不需要定义类B,因为一个pointer to A被声明为成员变量,而不是类型的对象A。由于指针具有固定大小,因此编译器不需要知道指针的确切布局A也不计算其大小以正确定义类B。因此,这足以前向声明 class A in 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

Your main.cpp现在肯定会编译。几点说明:

  1. 不仅通过替换来打破相互包含#include带有前向声明的指令b.h足以有效地表达依赖关系B on A:只要可能/实用,使用前向声明也被认为是一种良好的编程习惯,因为它有助于避免不必要的包含,从而减少总体编译时间。但消除相互包含后,main.cpp必须修改为#include both a.h and b.h(如果确实需要后者),因为b.h不再是间接的#included 通过a.h;
  2. 虽然类的前向声明A足以让编译器声明指向该类的指针(或在可接受不完整类型的任何其他上下文中使用它),取消引用指向该类的指针A(例如调用成员函数)或计算其大小illegal对不完整类型的操作:如果需要,请提供完整的定义A需要可供编译器使用,这意味着必须包含定义它的头文件。这就是为什么类定义及其成员函数的实现通常分为头文件和该类的实现文件(类模板是此规则的一个例外):实现文件,这些文件永远不会#included 由项目中的其他文件,可以安全地#include使定义可见的所有必要的标题。另一方面,头文件不会#include其他头文件unless他们确实需要这样做(例如,定义基类可见),并将尽可能/实用地使用前向声明。

第二个问题:

为什么不包括警卫阻止多重定义?

They are.

他们无法保护您免受多重定义的侵害在单独的翻译单元中。这也解释在本次问答在 StackOverflow 上。

太看到了,尝试删除包含防护并编译以下修改版本source1.cpp (or source2.cpp,重要的是):

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

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

int main()
{
    ...
}

编译器肯定会在这里抱怨f()被重新定义。这是显而易见的:它的定义被包含了两次!然而,上述source1.cpp 编译时不会出现问题header.h包含适当的包含防护。这是预期的。

尽管如此,即使存在包含保护并且编译器将不再用错误消息来打扰您,linker将坚持这样一个事实:在合并从编译中获得的目标代码时会发现多个定义source1.cpp and source2.cpp,并将拒绝生成您的可执行文件。

为什么会发生这种情况?

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

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

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

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

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

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

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

If您想将函数定义保存在头文件中#included by multiple翻译单元(注意,如果您的标题是#included 只是通过one翻译单元),您需要使用inline关键词。

否则,您只需要保留宣言你的功能在header.h,将其定义(主体)放入one分离.cpp仅文件(这是经典方法)。

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

a 可以有多个定义类类型(第 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关键字还可以通过提供您的函数来抑制链接器错误内部联系,从而使每个翻译单元拥有一个私有的copy该函数(及其局部静态变量)的。然而,这最终会导致更大的可执行文件,并且使用inline一般情况下应该是首选。

另一种方法可以达到与static关键字是放置函数f() in an 未命名的命名空间。根据 C++11 标准第 3.5/4 段:

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

- 一个变量;或者

一个函数; or

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

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

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

— 一个模板。

出于与上述相同的原因,inline应该优先考虑关键字。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

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

  • 如何在 C++ 中的文件末尾添加数据?

    我已按照网上的说明进行操作 此代码应该将输入添加到文件 数据库 的末尾 但当我检查时 数据会覆盖现有数据 请帮忙 这是我的代码 int main string name string address string handphone cou
  • 用 C++ 进行服装建模 [关闭]

    Closed 这个问题正在寻求书籍 工具 软件库等的推荐 不满足堆栈溢出指南 help closed questions 目前不接受答案 我正在编写一些软件 最终会绘制一个人体框架 可以配置各种参数 并且计划是在假人身上放置某种衣服 我研究
  • 使用Physics.Raycast 和Physics2D.Raycast 检测对象上的点击

    我的场景中有一个空的游戏对象 带有 2D 组件盒碰撞器 我将脚本附加到该游戏对象 void OnMouseDown Debug Log clic 但是当我点击我的游戏对象时 没有任何效果 你有什么想法 如何检测我的盒子碰撞器上的点击 使用光
  • 将内置类型转换为向量

    我的 TcpClient 类接受vector
  • 互斥体实现可以互换(独立于线程实现)

    所有互斥体实现最终都会调用相同的基本系统 硬件调用吗 这意味着它们可以互换吗 具体来说 如果我使用 gnu parallel算法 使用openmp 并且我想让他们称之为线程安全的类我可以使用boost mutex用于锁定 或者我必须编写自己
  • XamlReader.Load 在后台线程中。是否可以?

    WPF 应用程序具有从单独的文件加载用户控件的操作 使用XamlReader Load method StreamReader mysr new StreamReader pathToFile DependencyObject rootOb
  • 读取文件特定行号的有效方法。 (奖励:Python 手册印刷错误)

    我有一个 100 GB 的文本文件 它是来自数据库的 BCP 转储 当我尝试导入它时BULK INSERT 我在第 219506324 行上收到一个神秘错误 在解决此问题之前 我想看看这一行 但可惜的是我最喜欢的方法 import line
  • 在 C# 中循环遍历文件文件夹的最简单方法是什么?

    我尝试编写一个程序 使用包含相关文件路径的配置文件来导航本地文件系统 我的问题是 在 C 中执行文件 I O 这将是从桌面应用程序到服务器并返回 和文件系统导航时使用的最佳实践是什么 我知道如何谷歌 并且找到了几种解决方案 但我想知道各种功
  • 使用 C 语言使用 strftime() 获取缩写时区

    我看过this https stackoverflow com questions 34408909 how to get abbreviated timezone and this https stackoverflow com ques
  • 关于在 Windows 上使用 WiFi Direct Api?

    我目前正在开发一个应用程序 我需要在其中创建链接 阅读 无线网络连接 在桌面应用程序 在 Windows 10 上 和平板电脑 Android 但无关紧要 之间 工作流程 按钮 gt 如果需要提升权限 gt 创建类似托管网络的 WiFi 网
  • 如何在 Linq 中获得左外连接?

    我的数据库中有两个表 如下所示 顾客 C ID city 1 Dhaka 2 New york 3 London 个人信息 P ID C ID Field value 1 1 First Name Nasir 2 1 Last Name U
  • 将 Excel 导入到 Datagridview

    我使用此代码打开 Excel 文件并将其保存在 DataGridView 中 string name Items string constr Provider Microsoft Jet OLEDB 4 0 Data Source Dial
  • 未定义的行为或误报

    我 基本上 在野外遇到过以下情况 x x 5 显然 它可以在早期版本的 gcc 下编译干净 在 gcc 4 5 1 下生成警告 据我所知 警告是由 Wsequence point 生成的 所以我的问题是 这是否违反了标准中关于在序列点之间操
  • 如何将自定义 JSON 文件添加到 IConfiguration 中?

    我正在使用 asp net Autofac 我正在尝试加载自定义 JSON 配置文件 并基于该文件创建 实例化 IConfiguration 实例 或者至少将我的文件包含到默认情况下构建的 IConfiguration asp net 中
  • 使用 Moq 使用内部构造函数模拟类型

    我正在尝试模拟 Microsoft Sync Framework 中的一个类 它只有一个内部构造函数 当我尝试以下操作时 var fullEnumerationContextMock new Mock
  • (de)从 CSV 序列化为对象(或者最好是类型对象的列表)

    我是一名 C 程序员 试图学习 C 似乎有一些内置的对象序列化 但我在这里有点不知所措 我被要求将测试数据从 CSV 文件加载到对象集合中 CSV 比 xml 更受青睐 因为它更简单且更易于人类阅读 我们正在创建测试数据来运行单元测试 该集
  • 为什么在setsid()之前fork()

    Why fork before setsid 守护进程 基本上 如果我想将一个进程与其控制终端分离并使其成为进程组领导者 我使用setsid 之前没有分叉就这样做是行不通的 Why 首先 setsid 将使您的进程成为进程组的领导者 但它也
  • 编译时“strlen()”有效吗?

    有时需要将字符串的长度与常量进行比较 例如 if line length gt 2 Do something 但我试图避免在代码中使用 魔法 常量 通常我使用这样的代码 if line length gt strlen Do somethi
  • 使用 GhostScript.NET 打印 PDF DPI 打印问题

    我在用GhostScript NET http ghostscriptnet codeplex com打印 PDF 当我以 96DPI 打印时 PDF 打印效果很好 但有点模糊 如果我尝试以 600DPI 打印文档 打印的页面会被极大地放大
  • 检查Windows控制台中是否按下了键[重复]

    这个问题在这里已经有答案了 可能的重复 C 控制台键盘事件 https stackoverflow com questions 2067893 c console keyboard events 我希望 Windows 控制台程序在按下某个

随机推荐