【问题标题】:The great c++ forward declaration confusion伟大的 C++ 前向声明混乱
【发布时间】:2018-02-07 02:01:41
【问题描述】:

假设我有一个 A 类和一个 B 类以及它们对应的标题:

啊。

#ifndef CLASS_A
#define CLASS_A

/* forward declare A */
class A;
/* includes */
#include "b.h"
/* define class A */
class A {
public:
    A() : p_b(nullptr) {}
    B *p_b;
};
#endif

b.h

#ifndef CLASS_B
#define CLASS_B

/* forward declare B */
class B;
/* includes */
#include "a.h"
/* define class B */
class B {
public:
    B() : m_a() {}
    A m_a;
};
#endif

这不起作用:

为了将 A 的实现编译成一个目标文件,我首先包含 ah,它转发声明 A,然后包含 bh,然后声明和定义 B。但是当 B 被定义时,它不知道 A 的大小,因此不能将 A 的对象声明为 B 的成员。

然而,A 不需要知道 B 的大小,因为它只有一个指向 B 的指针,并且可以在定义 B 之前完全定义。因此,在将 B 用作成员之前,可以完全知道 B 的大小,并且完整的声明应该没问题。

常识告诉 a.c 文件应始终如下所示:

#include "a.h"

[...]

我可以通过在 a.c 中包含 a.h 之前的 b.h 来实际解决问题吗?这是否违反了将实现文件的第一行包含在其标头中的某些神圣约定?

【问题讨论】:

  • 你有没有想过在定义A之后包含b.h?
  • 那么 A 会失败,因为 B 在我创建指向它的指针时不会定义类型,我认为。
  • 通常,您在需要它们的文件中提出声明,而不是让标头本身包含使用第一个标头的不同标头。
  • 您想使用前向声明来消除 a.h 和 b.h 之间的循环依赖。删除 a.h 中 b.h 的包含,并将 A 的前向声明替换为 B 的前向声明。在其自己的标头中前向声明一个类是没有意义的(除非您有一个复杂的标头)。
  • 非常好的阅读,如果不是完全重复的话:Resolve build errors due to circular dependency amongst classes

标签: c++ member forward-declaration


【解决方案1】:

您以向后的方式使用前向声明。代码应该看起来更像这样:

啊。

#ifndef CLASS_A
#define CLASS_A

/* forward declare B */
class B;

/* define class A */
class A {
public:
    A() : p_b(nullptr) {}
    B *p_b;
};

#endif

b.h

#ifndef CLASS_B
#define CLASS_B

#include "a.h"

/* define class B */
class B {
public:
    B() : m_a() {}
    A m_a;
};

#endif

a.h 不需要知道B 实际上是什么,因为A 包含B* 指针而不是B 对象。所以a.h 根本不应该使用#include "b.h",它应该向前声明B

b.h 确实需要知道A 实际上是什么,因为B 包含一个A 对象而不是A* 指针。所以b.h应该使用#include "a.h",它在定义A之前已经前向声明了B,然后b.h完成定义B

a.c 然后可以使用#include "a.h" 来引入A 的声明,这样就可以完成定义实现了,并且只有当A 的方法需要访问@ 的成员时才可以使用#include "b.h" 987654347@。

【讨论】:

  • 所以我的错误在于假设我应该总是在任何包含之前在它的头文件中转发声明一个类,因为最终的循环依赖关系可能想知道一个符号是指一种类型还是其他东西。因此,我必须习惯于总是转发声明我使用的类名,而不是我提供摆脱这些错误,只要我只使用转发类的指针。
  • @salbeira 如果我没看错你说的话,那么是的。头文件不应该转发声明它也定义的类(除非头文件定义了多个相互依赖的类)。前向声明任何你可以摆脱的不需要包含其他头文件的东西,让实现文件包含它需要的任何头文件。这减少了头文件相互依赖,从而减少了头文件更改时需要重新编译的代码量。而且它更好地优化了编译器使用预编译头文件的能力。
【解决方案2】:

让我们看看编译器在预处理器完成它之后看到了什么:

/* forward declare A */
class A;
/* includes */
/* forward declare B */
class B;
/* includes */
/* define class B */
class B {
public:
    B() : m_a() {}
    A m_a;
};
/* define class A */
class A {
public:
    A() : p_b(nullptr) {}
    B *p_b;
};

如您所见,B 的类定义在 A 完全定义之前,因此您的程序格式不正确。

在这里,您只需要一个前向声明(B),它应该在 A.h 中 A 的定义之前:

#ifndef CLASS_A
#define CLASS_A

// Forward declare B so that B* p_b is legal
class B;

// Note that B.h is *not* included here

class A {
public:
    A() : p_b(nullptr) {}
    B *p_b;
};
#endif

在这里,您通过前向声明 B 而不是包含 B 的完整定义来打破循环包含循环。大概在 A.cpp 中你会 #include B 的完整定义,以便你可以使用它的成员。

【讨论】:

    【解决方案3】:

    因为每个类都依赖于另一个类,所以两个类都应该定义在同一个头文件中(并且在同一个命名空间中)。如果由于某种原因它们必须位于不同的头文件中,这将起作用。

    啊.h

    #ifndef CLASS_A
    #define CLASS_A
    
    class B;
    
    class A {
    public:
        A() : p_b(nullptr) {}
        B *p_b;
    };
    #endif
    

    B.h

    #ifndef CLASS_B
    #define CLASS_B
    
    #include "a.h"
    
    class B {
    public:
        B() : m_a() {}
        A m_a;
    };
    #endif
    

    【讨论】:

    • 呵呵,很高兴你假设这是一个家庭作业,但它绝对不是 :-) 相关的类实际上是应用程序的非常不同的子系统的一部分,其中一个定义了一个节点一个层次图和另一个代表一个想要插入菜单的小部件,但这对于问题来说将是太多的信息。
    • @salbeira 我希望没有冒犯。无意。我删除了 H 字。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2010-12-29
    • 2021-12-25
    • 1970-01-01
    • 2023-03-16
    • 1970-01-01
    • 2012-03-14
    相关资源
    最近更新 更多