由于类之间的循环依赖性,解决构建错误

我经常发现自己处于C ++项目中由于一些糟糕的设计决定(由其他人制作)导致多个编译/链接器错误的情况,这导致了不同头文件中的C ++类之间的循环依赖关系(也可能发生在同一个文件中)。 但幸运的是(?)对于我下次再次发生这种问题时,这种情况通常不会发生。

因此,为了在未来轻松召回,我将发布代表性问题和解决方案。 更好的解决方案当然是受欢迎的。


  • Ah

    class B;
    class A
    {
        int _val;
        B *_b;
    public:
    
        A(int val)
            :_val(val)
        {
        }
    
        void SetB(B *b)
        {
            _b = b;
            _b->Print(); // COMPILER ERROR: C2027: use of undefined type 'B'
        }
    
        void Print()
        {
            cout<<"Type:A val="<<_val<<endl;
        }
    };
    

  • Bh

    #include "A.h"
    class B
    {
        double _val;
        A* _a;
    public:
    
        B(double val)
            :_val(val)
        {
        }
    
        void SetA(A *a)
        {
            _a = a;
            _a->Print();
        }
    
        void Print()
        {
            cout<<"Type:B val="<<_val<<endl;
        }
    };
    

  • main.cpp

    #include "B.h"
    #include <iostream>
    
    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;
    }
    

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

    想象一下,你正在编写一个编译器。 你看到这样的代码。

    // 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 "Ah"的下一个人必须声明B才能使用它,并且会得到一个可怕的#include错误。 所以让我们把声明移到Ah本身。

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

    而在Bh ,在这一点上,你可以直接#include "Ah"

    // 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。


    如果您从头文件中删除方法定义并让这些类仅包含方法声明和变量声明/定义,则可避免编译错误。 方法定义应放置在.cpp文件中(就像最佳实践准则所述)。

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

    //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;
    }
    

    要记住的事情:

  • 如果class A具有class B的对象作为成员,或者相反,这将不起作用。
  • 前向声明是要走的路。
  • 声明的顺序很重要(这就是为什么你要移除定义)。
  • 如果两个类都调用另一个类的函数,则必须将定义移出。
  • 阅读常见问题:

  • 我如何创建两个互相了解的类?
  • 使用成员对象的前向声明时需要特别注意什么?
  • 内联函数使用前向声明时需要特别注意什么?
  • 链接地址: http://www.djcxy.com/p/85703.html

    上一篇: Resolve build errors due to circular dependency amongst classes

    下一篇: numpy's QR appears faster than scipy's