我经常发现自己处于这样一种情况:由于一些糟糕的设计决策(由其他人做出:),我在c++项目中面临多个编译/链接器错误,这导致了不同头文件中c++类之间的循环依赖(也可能发生在同一个文件中)。但幸运的是,这种情况发生的次数并不多,所以当下次再次发生这种情况时,我还能记住解决这个问题的方法。

因此,为了便于以后回忆,我将发布一个有代表性的问题和解决方案。更好的解决方案当然是受欢迎的。


A.h B类; A类 { int _val; B * _b; 公众: (int val) : _val (val) { } SetB(B * B) { _b = b; _b - >打印();//编译错误:C2027:使用未定义的类型“B” } 无效的Print () { cout < <“类型:val = " < < _val < < endl; } };


B.h # include“A.h” B类 { 双_val; * _a; 公众: B(双val) : _val (val) { } SetA(A * A) { _a = a; _a - >打印(); } 无效的Print () { cout < <“B型:val = " < < _val < < endl; } };


main.cpp # include“B.h” # include < iostream > Int main(Int argc, char* argv[]) { 一个(10); B B (3.14); a.Print (); a.SetB(乙); b.Print (); b.SetA(和); 返回0; }


当前回答

如果从头文件中删除方法定义,并让类只包含方法声明和变量声明/定义,就可以避免编译错误。方法定义应该放在.cpp文件中(就像最佳实践指南所说的那样)。

以下解决方案的缺点是(假设您已经将方法放在头文件中以内联它们)编译器不再内联这些方法,并且尝试使用内联关键字会产生链接器错误。

//A.h
#ifndef A_H
#define A_H
class B;
class A
{
    int _val;
    B* _b;
public:

    A(int val);
    void SetB(B *b);
    void Print();
};
#endif

//B.h
#ifndef B_H
#define B_H
class A;
class B
{
    double _val;
    A* _a;
public:

    B(double val);
    void SetA(A *a);
    void Print();
};
#endif

//A.cpp
#include "A.h"
#include "B.h"

#include <iostream>

using namespace std;

A::A(int val)
:_val(val)
{
}

void A::SetB(B *b)
{
    _b = b;
    cout<<"Inside SetB()"<<endl;
    _b->Print();
}

void A::Print()
{
    cout<<"Type:A val="<<_val<<endl;
}

//B.cpp
#include "B.h"
#include "A.h"
#include <iostream>

using namespace std;

B::B(double val)
:_val(val)
{
}

void B::SetA(A *a)
{
    _a = a;
    cout<<"Inside SetA()"<<endl;
    _a->Print();
}

void B::Print()
{
    cout<<"Type:B val="<<_val<<endl;
}

//main.cpp
#include "A.h"
#include "B.h"

int main(int argc, char* argv[])
{
    A a(10);
    B b(3.14);
    a.Print();
    a.SetB(&b);
    b.Print();
    b.SetA(&a);
    return 0;
}

其他回答

我曾经解决过这类问题,方法是将所有内联移到类定义之后,并将其他类的#include放在头文件中的内联之前。这样可以确保在解析内联之前设置所有定义+内联。

这样做可以使两个(或多个)头文件中仍然有大量内联。但有必要配备保安。

像这样

// File: A.h
#ifndef __A_H__
#define __A_H__
class B;
class A
{
    int _val;
    B *_b;
public:
    A(int val);
    void SetB(B *b);
    void Print();
};

// Including class B for inline usage here 
#include "B.h"

inline A::A(int val) : _val(val)
{
}

inline void A::SetB(B *b)
{
    _b = b;
    _b->Print();
}

inline void A::Print()
{
    cout<<"Type:A val="<<_val<<endl;
}

#endif /* __A_H__ */

...在B.h中做同样的事情

维基百科上的简单例子对我很有用。 (你可以在http://en.wikipedia.org/wiki/Circular_dependency#Example_of_circular_dependencies_in_C.2B.2B上阅读完整的描述)

文件“a.h”:

#ifndef A_H
#define A_H

class B;    //forward declaration

class A {
public:
    B* b;
};
#endif //A_H

文件“b.h”:

#ifndef B_H
#define B_H

class A;    //forward declaration

class B {
public:
    A* a;
};
#endif //B_H

文件“main.cpp”:

#include "a.h"
#include "b.h"

int main() {
    A a;
    B b;
    a.b = &b;
    b.a = &a;
}

首先,我们需要一些定义。

定义

宣言

extern int n;
int f();
template<typename T> int g(T);
struct A;
template<typename T> struct B;

定义

int n;
int f() { return 42; }
template<typename T> int g(T) { return 42; }
struct A { int f(); };
template<typename T> struct B { int g(T*); };

不同之处在于重复定义会导致违反One definition Rule (ODR)。编译器会给出一个类似"error: redefinition of '…'"的错误。

注意,“向前声明”只是一种声明。声明可以重复,因为它们没有定义任何东西,因此不会导致ODR。

注意,默认参数只能给出一次,可能是在声明期间,但如果有多个声明,则只能给出其中一个。因此,有人可能会说这是一个定义,因为它可能不会被重复(在某种意义上它是:它定义了默认参数)。但是,由于它没有定义函数或模板,我们无论如何都将其称为声明。下面将忽略默认参数。

函数定义

(Member) function definitions generate code. Having multiple of those (in different Translation Units (TU's), otherwise you'd get an ODR violation already during compile time) normally leads to a linker error; except when the linker resolves the collision which it does for inline functions and templated functions. Both might or might not be inlined; if they are not 100% of the time inlined then a normal function (instantiation) needs to exist; that might cause the collision that I am talking about.

非内联、非模板(成员)函数只需要存在于单个TU中,因此应该在单个.cpp中定义。

然而,内联和/或模板(成员)函数定义在头文件中,可能被多个TU包含,因此需要链接器进行特殊处理。然而,它们也被认为是生成代码的。

类定义

类定义可能生成代码,也可能不生成代码。如果有,那是针对链接器将解决冲突的函数。

当然,在类内部定义的任何成员函数都是“内联”定义。如果在类声明期间定义了这样一个函数,那么可以简单地将它移到类声明之外。

相反的,

struct A {
  int f() const { return 42; }
};

do

struct A {
  inline int f() const;
}; // struct declaration ends here.

int A::f() const { return 42; }

因此,我们最感兴趣的是代码生成(函数实例化),它们不能被移到类声明之外,并且需要一些其他定义才能被实例化。

事实证明,这通常涉及智能指针和默认析构函数。假设结构B不能定义,只能声明,结构A如下所示:

struct B;
struct A { std::unique_ptr<B> ptr; };

那么A的实例化而B的定义不可见(一些编译器可能不介意稍后在同一TU中定义B)将导致错误,因为A的默认构造函数和析构函数都会生成unique_ptr<B>的析构函数,这需要B的定义[例如:' sizeof '对不完整类型' B '的无效应用]。不过,还是有办法解决这个问题:不要使用生成的默认构造函数/析构函数。

例如,

struct B;
struct A {
  A();
  ~A();
  std::unique_ptr<B> ptr;
};

将编译并只有两个未定义的符号A::A()和A::~A(),您仍然可以像以前一样在A的定义之外内联编译(前提是您在这样做之前定义了B)。

三个部分,三个文件?

因此,我们可以区分结构/类定义的三个部分,分别放在不同的文件中。

(向前)声明: A.fwd.h 类定义: A.h 内联和模板成员函数定义: A.inl.h

当然还有带有非内联和非模板成员函数定义的A.cpp;但这些与循环头依赖关系无关。

忽略默认参数,声明将不需要任何其他声明或定义。

类定义可能需要声明某些其他类,也可能需要定义其他类。

内联/模板成员函数可能需要其他定义。

因此,我们可以创建以下示例来展示所有可能性:

struct C;
struct B
{
  B();
  ~B();
  std::unique_ptr<C> ptr;  // Need declaration of C.
};

struct A
{
  B b;    // Needs definition of B.
  C f();  // Needs declaration of C.
};

inline A g()  // Needs definition of A.
{
  return {};
}

struct D
{
  A a = g();  // Needs definition of A.
  C c();      // Needs declaration of C.
};

B: B (), B:: ~ B (), C:: f()和C D:: C()定义一些. cpp。

但是,我们把它们也内联起来;在这一点上,我们需要定义C,因为这四个都需要(B::B和B::~B,因为unique_ptr,见上文)。在这个TU中这样做,突然就没有必要把B::B()和B::~B()放在B的定义之外(至少在我使用的编译器中)。尽管如此,我们保持B不变。

然后我们得到:

// C.fwd.h:
struct C;

// B.h:
struct B
{
  inline B();
  inline ~B();
  std::unique_ptr<C> ptr;
};

// A.h:
struct A
{
  B b;
  inline C f();
};

// D.h:
inline A g()
{
  return {};
}
struct D
{
  A a = g();
  inline C c();
};

// C.h:
struct C {};

// B.inl.h:
B::B() {}
B::~B() {}

// A.inl.h:
C A::f()
{
  D d;
  return d.c();
}

// D.inl.h:
C D::c()
{
  return {};
}

换句话说,A的定义是这样的

// A.fwd.h:
struct A;
// A.h:
#include "B.h"      // Already includes C.fwd.h, but well...
#include "C.fwd.h"  // We need C to be declared too.
struct A
{
  B b;
  inline C f();
};
// A.inl.h:
#include "A.h"
#include "C.h"
#include "D.inl.h"
C A::f()
{
  D d;
  return d.c();
}

请注意,理论上我们可以创建多个.inl.h头文件:每个函数一个头文件,如果不这样做,就会导致问题。

禁止模式

注意,所有#include都位于所有文件的顶部。

(理论上).fwd.h头文件不包括其他头文件。因此,可以随意包含它们,而不会导致循环依赖。

.h定义头文件可能包含.inl.h头文件,但如果这导致循环头文件依赖,那么总是可以通过将使用内联函数的函数从.inl.h移到当前类的.inl.h来避免这种情况;在智能指针的情况下,可能还需要将析构函数和/或构造函数移动到.inl.h。

因此,唯一剩下的问题是.h定义头的循环包含,即A.h包含B.h, B.h包含A.h。在这种情况下,必须通过用指针替换类成员来解耦循环。

最后,纯.inl.h文件的循环是不可能的。如果有必要,你可能应该把它们移动到一个文件中,在这种情况下,编译器可能无法解决问题;但显然,当它们相互使用时,你不能让所有函数都内联,所以你不妨手动决定哪些可以非内联。

思考这个问题的方法是“像编译器一样思考”。

假设您正在编写一个编译器。你会看到这样的代码。

// file: A.h
class A {
  B _b;
};

// file: B.h
class B {
  A _a;
};

// file main.cc
#include "A.h"
#include "B.h"
int main(...) {
  A a;
}

当你编译。cc文件时(记住。cc而不是。h是编译的单位),你需要为对象a分配空间,那么,那么,有多少空间呢?足够储存B了!那么B的大小是多少呢?足够储存A!哦。

显然你必须打破一个循环引用。

你可以通过允许编译器保留尽可能多的空间来打破它——例如,指针和引用将始终是32或64位(取决于体系结构),所以如果你用指针或引用替换(任何一个),事情就会很好。我们在A中替换:

// file: A.h
class A {
  // both these are fine, so are various const versions of the same.
  B& _b_ref;
  B* _b_ptr;
};

现在情况好多了。有点。Main()仍然说:

// file: main.cc
#include "A.h"  // <-- Houston, we have a problem

#include,对于所有的范围和目的(如果你取出预处理器)只是复制文件到.cc。所以实际上,。cc看起来像:

// file: partially_pre_processed_main.cc
class A {
  B& _b_ref;
  B* _b_ptr;
};
#include "B.h"
int main (...) {
  A a;
}

你可以看到为什么编译器不能处理这个——它不知道B是什么——它以前甚至从来没有见过这个符号。

因此,让我们告诉编译器关于b的信息。这被称为前向声明,并在本回答中进一步讨论。

// main.cc
class B;
#include "A.h"
#include "B.h"
int main (...) {
  A a;
}

这个作品。这并不好。但是在这一点上,您应该已经理解了循环引用问题,以及我们如何“修复”它,尽管修复是糟糕的。

这个修复很糟糕的原因是,下一个要#include“A.h”的人在使用它之前必须声明B,并且会得到一个可怕的#include错误。让我们把声明移到A.h本身。

// file: A.h
class B;
class A {
  B* _b; // or any of the other variants.
};

在B.h中,此时你可以直接包含“A.h”。

// file: B.h
#include "A.h"
class B {
  // note that this is cool because the compiler knows by this time
  // how much space A will need.
  A _a; 
}

HTH.

需要记住的事情:

如果类A有类B的对象作为成员,这将不起作用,反之亦然。 向前申报是一种方式。 声明的顺序很重要(这就是为什么要移出定义)。 如果两个类都调用另一个类的函数,则必须将定义移出。

阅读常见问题:

如何创建两个相互了解的类? 对成员对象使用前向声明时需要特别注意什么? 前向声明与内联函数一起使用时需要特别注意什么?