伟大的 C++ 前向声明混乱

The great c++ forward declaration confusion

假设我有一个 Class A 和一个 Class B 及其对应的 header:

a.h

#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 的实现编译成 object-file,我首先包含 a.h,前向声明 A,然后包含 b.h,然后声明和定义 B。但是当 B已定义,它不知道 A 的大小,因此无法将 A 的 object 声明为 B 的成员。

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

常识表明 a.c 文件应始终如下所示:

#include "a.h"

[...]

我真的可以通过在 a.c 中的 a.h 之前包含 b.h 来解决问题吗?这是否违反了将实现文件的第一行包含在它的 header?

中的一些神圣约定

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

A.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

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

/* 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 的 class 定义出现在 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 的完整定义,以便您可以使用其成员。

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

a.h

#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 的声明,这样它就可以完成定义实现,并且它可以使用 #include "b.h" 只有当 A 的方法需要访问 B 的成员。