我经常发现自己处于这样一种情况:由于一些糟糕的设计决策(由其他人做出:),我在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; }


当前回答

我回答这个问题晚了,但到目前为止还没有一个合理的答案,尽管这是一个受欢迎的问题,得到了高度好评的答案....

最佳实践:向前声明标头

正如标准库的<iosfwd>标头所示,为其他人提供前向声明的正确方法是有一个前向声明标头。例如:

a.fwd.h:

#pragma once
class A;

a.h:

#pragma once
#include "a.fwd.h"
#include "b.fwd.h"

class A
{
  public:
    void f(B*);
};

b.fwd.h:

#pragma once
class B;

b.h:

#pragma once
#include "b.fwd.h"
#include "a.fwd.h"

class B
{
  public:
    void f(A*);
};

A库和B库的维护者应该各自负责保持它们的前向声明头与它们的头和实现文件同步,因此-例如-如果“B”的维护者出现并将代码重写为…

b.fwd.h:

template <typename T> class Basic_B;
typedef Basic_B<char> B;

b.h:

template <typename T>
class Basic_B
{
    ...class definition...
};
typedef Basic_B<char> B;

...然后,“A”的代码重新编译将由包含的b.fwd.h的更改触发,并应该干净地完成。


糟糕但常见的做法:在其他lib中向前声明东西

比如,不是像上面解释的那样使用前向声明头,而是在a.h或a.cc中使用前向声明类B;本身:

if a.h or a.cc did include b.h later: compilation of A will terminate with an error once it gets to the conflicting declaration/definition of B (i.e. the above change to B broke A and any other clients abusing forward declarations, instead of working transparently). otherwise (if A didn't eventually include b.h - possible if A just stores/passes around Bs by pointer and/or reference) build tools relying on #include analysis and changed file timestamps won't rebuild A (and its further-dependent code) after the change to B, causing errors at link time or run time. If B is distributed as a runtime loaded DLL, code in "A" may fail to find the differently-mangled symbols at runtime, which may or may not be handled well enough to trigger orderly shutdown or acceptably reduced functionality.

如果A的代码有旧B的模板专门化/“特征”,它们就不会生效。

其他回答

不幸的是,之前所有的答案都遗漏了一些细节。正确的解有点麻烦,但这是唯一正确的方法。而且它易于扩展,也能处理更复杂的依赖关系。

以下是如何做到这一点,准确地保留所有细节和可用性:

解决方案与最初的计划完全相同 内联函数仍然是内联的 A和B的用户可以任意顺序使用A.h和B.h

创建两个文件,A_def.h, B_def.h。这些将只包含A和B的定义:

// A_def.h
#ifndef A_DEF_H
#define A_DEF_H

class B;
class A
{
    int _val;
    B *_b;

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

// B_def.h
#ifndef B_DEF_H
#define B_DEF_H

class A;
class B
{
    double _val;
    A* _a;

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

然后,A.h和B.h会包含这个

// A.h
#ifndef A_H
#define A_H

#include "A_def.h"
#include "B_def.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

// B.h
#ifndef B_H
#define B_H

#include "A_def.h"
#include "B_def.h"

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

inline void B::SetA(A *a)
{
    _a = a;
    _a->Print();
}

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

#endif

注意,A_def.h和B_def.h是“私有”头文件,A和B的用户不应该使用它们。公共头文件是A.h和B.h。

下面是模板的解决方案:如何处理模板的循环依赖关系

解决这个问题的线索是在提供定义(实现)之前声明两个类。不能将声明和定义分离到单独的文件中,但是可以将它们作为单独的文件进行结构。

我回答这个问题晚了,但到目前为止还没有一个合理的答案,尽管这是一个受欢迎的问题,得到了高度好评的答案....

最佳实践:向前声明标头

正如标准库的<iosfwd>标头所示,为其他人提供前向声明的正确方法是有一个前向声明标头。例如:

a.fwd.h:

#pragma once
class A;

a.h:

#pragma once
#include "a.fwd.h"
#include "b.fwd.h"

class A
{
  public:
    void f(B*);
};

b.fwd.h:

#pragma once
class B;

b.h:

#pragma once
#include "b.fwd.h"
#include "a.fwd.h"

class B
{
  public:
    void f(A*);
};

A库和B库的维护者应该各自负责保持它们的前向声明头与它们的头和实现文件同步,因此-例如-如果“B”的维护者出现并将代码重写为…

b.fwd.h:

template <typename T> class Basic_B;
typedef Basic_B<char> B;

b.h:

template <typename T>
class Basic_B
{
    ...class definition...
};
typedef Basic_B<char> B;

...然后,“A”的代码重新编译将由包含的b.fwd.h的更改触发,并应该干净地完成。


糟糕但常见的做法:在其他lib中向前声明东西

比如,不是像上面解释的那样使用前向声明头,而是在a.h或a.cc中使用前向声明类B;本身:

if a.h or a.cc did include b.h later: compilation of A will terminate with an error once it gets to the conflicting declaration/definition of B (i.e. the above change to B broke A and any other clients abusing forward declarations, instead of working transparently). otherwise (if A didn't eventually include b.h - possible if A just stores/passes around Bs by pointer and/or reference) build tools relying on #include analysis and changed file timestamps won't rebuild A (and its further-dependent code) after the change to B, causing errors at link time or run time. If B is distributed as a runtime loaded DLL, code in "A" may fail to find the differently-mangled symbols at runtime, which may or may not be handled well enough to trigger orderly shutdown or acceptably reduced functionality.

如果A的代码有旧B的模板专门化/“特征”,它们就不会生效。

维基百科上的简单例子对我很有用。 (你可以在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;
}

我曾经写过一篇关于这个问题的文章:在c++中解决循环依赖

基本技术是使用接口来解耦类。在你的例子中:

//Printer.h
class Printer {
public:
    virtual Print() = 0;
}

//A.h
#include "Printer.h"
class A: public Printer
{
    int _val;
    Printer *_b;
public:

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

    void SetB(Printer *b)
    {
        _b = b;
        _b->Print();
    }

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

//B.h
#include "Printer.h"
class B: public Printer
{
    double _val;
    Printer* _a;
public:

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

    void SetA(Printer *a)
    {
        _a = a;
        _a->Print();
    }

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

//main.cpp
#include <iostream>
#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;
}