我经常发现自己处于这样一种情况:由于一些糟糕的设计决策(由其他人做出:),我在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;
}
维基百科上的简单例子对我很有用。
(你可以在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;
}
思考这个问题的方法是“像编译器一样思考”。
假设您正在编写一个编译器。你会看到这样的代码。
// 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.
不幸的是,我不能评论geza的答案。
他不仅仅是说“把声明放到一个单独的头文件中”。他说,你必须将类定义头文件和内联函数定义分离到不同的头文件中,以允许“延迟依赖”。
但是他的插图不是很好。因为这两个类(A和B)只需要彼此的不完整类型(指针字段/参数)。
为了更好地理解它,想象类A有一个类型为B而不是B*的字段。此外,类A和类B想定义一个内联函数,参数类型为另一种:
这段简单的代码行不通:
// A.h
#pragme once
#include "B.h"
class A{
B b;
inline void Do(B b);
}
inline void A::Do(B b){
//do something with B
}
// B.h
#pragme once
class A;
class B{
A* b;
inline void Do(A a);
}
#include "A.h"
inline void B::Do(A a){
//do something with A
}
//main.cpp
#include "A.h"
#include "B.h"
这将导致以下代码:
//main.cpp
//#include "A.h"
class A;
class B{
A* b;
inline void Do(A a);
}
inline void B::Do(A a){
//do something with A
}
class A{
B b;
inline void Do(B b);
}
inline void A::Do(B b){
//do something with B
}
//#include "B.h"
这段代码不能编译,因为B::Do需要后面定义的a的完整类型。
为了确保它能编译源代码,应该是这样的:
//main.cpp
class A;
class B{
A* b;
inline void Do(A a);
}
class A{
B b;
inline void Do(B b);
}
inline void B::Do(A a){
//do something with A
}
inline void A::Do(B b){
//do something with B
}
对于需要定义内联函数的每个类,使用这两个头文件是完全可能的。
唯一的问题是循环类不能只包含“公共标头”。
为了解决这个问题,我想建议一个预处理器扩展:#pragma process_pending_includes
这个指令应该延迟当前文件的处理,并完成所有挂起的include。