我经常发现自己处于这样一种情况:由于一些糟糕的设计决策(由其他人做出:),我在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;
}
思考这个问题的方法是“像编译器一样思考”。
假设您正在编写一个编译器。你会看到这样的代码。
// 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.