C++ #include 守卫

Posted

技术标签:

【中文标题】C++ #include 守卫【英文标题】:C++ #include guards 【发布时间】:2011-12-22 15:16:24 【问题描述】:

已解决

真正帮助我的是我可以#include .cpp 文件中的标头而不会导致重新定义的错误。


我是 C++ 新手,但我有一些 C# 和 Java 编程经验,所以我可能会遗漏一些 C++ 独有的基本知识。

问题是我真的不知道哪里出了问题,我会贴一些代码来尝试解释问题。

我有三个类,GameEvents、Physics 和 GameObject。我有他们每个人的标题。 GameEvents 有一个 Physics 和一个 GameObjects 列表。 Physics 有一个 GameObjects 列表。

我想要实现的是我希望 GameObject 能够访问或拥有一个物理对象。

如果我只是在 GameObject 中 #include "Physics.h" 我会得到 “错误C2111:'ClassXXX':'class'类型重新定义”我理解。 这就是我认为#include-guards 会有所帮助的地方,所以我在我的 Physics.h 中添加了一个包含保护,因为这是我想要包含两次的标题。

这就是它的样子

#ifndef PHYSICS_H
#define PHYSICS_H

#include "GameObject.h"
#include <list>


class Physics

private:
    double gravity;
    list<GameObject*> objects;
    list<GameObject*>::iterator i;
public:
    Physics(void);
    void ApplyPhysics(GameObject*);
    void UpdatePhysics(int);
    bool RectangleIntersect(SDL_Rect, SDL_Rect);
    Vector2X CheckCollisions(Vector2X, GameObject*);
;

#endif // PHYSICS_H

但如果我现在像这样在我的 GameObject.h 中#include "Physics.h":

#include "Texture2D.h"
#include "Vector2X.h"
#include <SDL.h>
#include "Physics.h"

class GameObject

private:
    SDL_Rect collisionBox;
public:
    Texture2D texture;
    Vector2X position;
    double gravityForce;
    int weight;
    bool isOnGround;
    GameObject(void);
    GameObject(Texture2D, Vector2X, int);
    void UpdateObject(int);
    void Draw(SDL_Surface*);
    void SetPosition(Vector2X);
    SDL_Rect GetCollisionBox();
;

我遇到了多个不明白为什么会出现的问题。 如果我不#include "Physics.h" 我的代码运行得很好。

我非常感谢任何帮助。

【问题讨论】:

***.com/questions/8010601/… 和许多其他人的副本。 【参考方案1】:

预处理器是一个程序,它接受您的程序,进行一些更改(例如包含文件 (#include)、宏扩展 (#define) 以及基本上以 # 开头的所有内容)并给出“干净”的结果到编译器。

预处理器在看到#include时会这样工作:

当你写作时:

#include "some_file"

some_file 的内容几乎可以直接复制粘贴到包含它的文件中。现在,如果你有:

a.h:
class A  int a; ;

还有:

b.h:
#include "a.h"
class B  int b; ;

还有:

main.cpp:
#include "a.h"
#include "b.h"

你得到:

main.cpp:
class A  int a; ;  // From #include "a.h"
class A  int a; ;  // From #include "b.h"
class B  int b; ;  // From #include "b.h"

现在您可以看到A 是如何重新定义的。

当你写守卫时,它们变成这样:

a.h:
#ifndef A_H
#define A_H
class A  int a; ;
#endif

b.h:
#ifndef B_H
#define B_H
#include "a.h"
class B  int b; ;
#endif

现在让我们看看 main 中的 #includes 是如何扩展的(这与之前的情况完全一样:复制粘贴)

main.cpp:
// From #include "a.h"
#ifndef A_H
#define A_H
class A  int a; ;
#endif
// From #include "b.h"
#ifndef B_H
#define B_H
#ifndef A_H          // From
#define A_H          // #include "a.h"
class A  int a; ;  // inside
#endif               // "b.h"
class B  int b; ;
#endif

现在让我们跟随预处理器,看看从中产生了哪些“真实”代码。我会一行一行的去:

// From #include "a.h"

评论。忽视!继续:

#ifndef A_H

A_H 是否已定义?不!然后继续:

#define A_H

好的,现在A_H 已定义。继续:

class A  int a; ;

这不是预处理器的东西,所以不要管它。继续:

#endif

之前的if到此结束。继续:

// From #include "b.h"

评论。忽视!继续:

#ifndef B_H

B_H 是否已定义?不!然后继续:

#define B_H

好的,现在B_H 已定义。继续:

#ifndef A_H          // From

A_H 是否已定义?是的!然后忽略直到对应的#endif

#define A_H          // #include "a.h"

忽略

class A  int a; ;  // inside

忽略

#endif               // "b.h"

之前的if到此结束。继续:

class B  int b; ;

这不是预处理器的东西,所以不要管它。继续:

#endif

之前的if到此结束。

也就是说,在预处理器处理完文件之后,编译器看到的是这样的:

main.cpp
class A  int a; ;
class B  int b; ;

所以你可以看到,任何可以在同一个文件中两次获得#included 的东西,无论是直接还是间接都需要被保护。由于.h 文件总是很可能被包含两次,因此最好保护所有 .h 文件。

附:请注意,您还有通知 #includes。想象一下预处理器将 Physics.h 的代码复制粘贴到 GameObject.h 中,看到有一个 #include "GameObject.h",这意味着将 GameObject.h 复制到自身中。当你复制时,你又会得到#include "Pysics.h",你会永远陷入循环中。编译器会阻止这种情况,但这意味着您的 #includes 已经完成了一半。

在说如何解决这个问题之前,你应该知道另一件事。

如果你有:

#include "b.h"

class A

    B b;
;

然后编译器需要知道关于b 的所有信息,最重要的是,它有哪些变量等,以便它知道应该在A 中放置多少字节来代替b

但是,如果您有:

class A

    B *b;
;

那么编译器实际上不需要知道任何关于B 的信息(因为指针,无论类型如何都具有相同的大小)。关于B,它唯一需要知道的就是它的存在!

所以你做了一些叫做“前向声明”的事情:

class B;  // This line just says B exists

class A

    B *b;
;

这与您在头文件中执行的许多其他操作非常相似,例如:

int function(int x);  // This is forward declaration

class A

public:
    void do_something(); // This is forward declaration

【讨论】:

谢谢,这对我帮助很大,我对正在发生的事情有了更好的了解。我已在我的 GameObject.h 中成功转发声明了 Physics *physics。但如果我理解正确,我无法在我的 GameObject 中创建物理,我必须将现有指针传递给我的 GameObject.cpp? 我似乎无法理解如何访问我的 GameObject.cpp 中的物理指针如果我尝试物理->CheckCollisions() 我得到“不允许指向不完整类类型的指针”并且没有成员找到了。 @Orujimaru 我在GameObject 类中没有看到Physics 对象或指针,但我在Physics 类中看到了很多GameObject *s。这建议在Physics.h 中向前声明GameObject,而不是相反。无论哪种方式,当您想使用该类(即在.cpp 文件中)时,您需要包含每个标头本身。由于您不包含 .cpp 文件,因此不存在循环包含的风险! @hans,不不,通过#define定义的符号包含在同一行中。例如#define SOME_SYMBOL (some_expression) 意味着预处理器将用(some_expression) 替换它从该点看到的SOME_SYMBOL。如果你有#define SOME_SYMBOL,前面什么都没有,它会在它看到它的时候用任何东西替换它。所以#define A_H 的意思是“定义一个名为A_H 的符号,它什么都不替换”。对我们来说重要的部分是“定义一个名为 A_H 的符号”,因为稍后我们想要一个关于是否定义符号的 if (#ifndef)。 所以,class A int a#define A_H 无关,它确实是 #ifndef...#endif' right? Also #define A_H` 的一部分,A_H 没有被分配,只是为了正式定义 A_H对吧?【参考方案2】:

您在此处有循环引用:Physics.h 包括 GameObject.h,其中包括 Physics.h。您的课程Physics 使用GameObject*(指针)类型,因此您不需要在Physics.h 中包含GameObject.h,而只需使用前向声明 - 而不是

#include "GameObject.h" 

class GameObject;   

此外,在每个头文件中放置守卫。

【讨论】:

【参考方案3】:

问题是你的GameObject.h 没有警卫,所以当你在Physics.h 中的#include "GameObject.h" 时,当GameObject.h 包含Physics.h 时,它会被包含在内。

【讨论】:

【参考方案4】:

在您的所有 *.h*.hh 头文件中添加包含保护(除非您有特定的理由不这样做)。

要了解发生了什么,请尝试获取源代码的预处理形式。对于 GCC,它类似于g++ -Wall -C -E yourcode.cc &gt; yourcode.i(我不知道 Microsoft 编译器是如何做到这一点的)。也可以询问包含哪些文件,GCC为g++ -Wall -H -c yourcode.cc

【讨论】:

【参考方案5】:

首先你也需要在游戏对象上包含守卫,但这不是这里真正的问题

如果其他的东西首先包括physics.h,physics.h 包括gameobject.h,你会得到这样的结果:

class GameObject 
...
;

#include physics.h

class Physics 
...
;

并且由于包含保护,#include Physics.h 被丢弃,并且您最终会在声明 Physics 之前声明 GameObject。

但如果你想让 GameObject 有一个指向物理的指针,那就是个问题了,因为对于 htat,物理必须首先声明。

要解决循环,您可以改为前向声明一个类,但前提是您只是在以下声明中将其用作指针或引用,即:

#ifndef PHYSICS_H
#define PHYSICS_H

//  no need for this now #include "GameObject.h"

#include <list>

class GameObject;

class Physics

private:
    list<GameObject*> objects;
    list<GameObject*>::iterator i;
public:
    void ApplyPhysics(GameObject*);
    Vector2X CheckCollisions(Vector2X, GameObject*);
;

#endif // PHYSICS_H

【讨论】:

我已经为所有标题添加了保护,但是当我用 Class GameObject 替换 #include "GameObject.h" 时; Visual Studio 为 Physics.cpp 和 Physics.h 中的每一行代码返回错误。如果我尝试在任何标头中转发声明任何其他类,也会发生同样的情况。【参考方案6】:

ALL 头文件中使用包含保护。由于您使用的是 Visual Studio,因此您可以使用 #pragma once 作为所有标头中的第一个预处理器定义。

但我建议使用经典方法:

#ifndef CLASS_NAME_H_
#define CLASS_NAME_H_

// Header code here

#endif //CLASS_NAME_H_

第二次阅读forward declaration 并应用它。

【讨论】:

【参考方案7】:

标头保护的目标是避免多次包含同一个文件。 但是目前在 C++ 中使用的 header 保护可以改进。目前的守卫是:

#ifndef AAA_H
#define AAA_H

class AAA
 /* ... */ ;

#endif

我的新后卫建议是:

#ifndef AAA_H
#define AAA_H

class AAA
 /* ... */ ;

#else
class AAA;  // Forward declaration
#endif

这解决了当 AAA 类需要 BBB 类声明而 BBB 类需要 AAA 类声明时出现的烦人问题,通常是因为从一个类到另一个类的交叉指针:

// File AAA.h
#ifndef AAA_H
#define AAA_H

#include "BBB.h"

class AAA
 
  BBB *bbb;
/* ... */ 
;

#else
class AAA;  // Forward declaration
#endif

//+++++++++++++++++++++++++++++++++++++++

// File BBB.h
#ifndef BBB_H
#define BBB_H

#include "AAA.h"

class BBB
 
  AAA *aaa;
/* ... */ 
;

#else
class BBB;  // Forward declaration
#endif

我希望将其包含在自动从模板生成代码的 IDE 中。

【讨论】:

【参考方案8】:

" #pragma once " ::: 与标头保护具有相同的目的,并且具有更短且不易出错的额外好处。

许多编译器使用#pragma 指令支持更简单的替代形式的头文件保护: “#pragma once” // 你的代码在这里

但是,#pragma once 不是 C++ 语言的官方部分,并非所有编译器都支持它(尽管大多数现代编译器都支持)。

出于兼容性目的,人们建议坚持使用传统的头部保护。它们的工作量并不多,而且保证所有兼容的编译器都支持它们。

【讨论】:

以上是关于C++ #include 守卫的主要内容,如果未能解决你的问题,请参考以下文章

4557: [JLoi2016]侦察守卫|树形DP

4557: [JLoi2016]侦察守卫|树形DP

#include 守卫不起作用 - 或者我不明白

JXOI2018 守卫

bzoj 3029 守卫者的挑战——概率期望dp+状态数思考

bzoj 3029: 守卫者的挑战概率dp