我经常发现自己处于一种情况,我在C++项目中面临多个编译/链接器错误,因为一些糟糕的设计决策(由其他人做出:))导致不同头文件中C++类之间的循环依赖(也可能发生)在同一个文件中).但幸运的是(?)这种情况经常不足以让我在下次再次发生问题时记住这个问题的解决方案.
因此,为了便于将来回忆,我将发布一个代表性问题和解决方案.更好的解决方案当然是受欢迎的.
A.h
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<
B.h
#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<
main.cpp
#include "B.h" #includeint main(int argc, char* argv[]) { A a(10); B b(3.14); a.Print(); a.SetB(&b); b.Print(); b.SetA(&a); return 0; }
小智.. 271
思考这个问题的方法是"像编译器一样思考".
想象一下,你正在编写一个编译器.你看到这样的代码.
// 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
错误.那么让我们将声明转移到Ah本身.// file: A.h class B; class A { B* _b; // or any of the other variants. };在Bh,在这一点上,你可以
#include "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.
1> 小智..:思考这个问题的方法是"像编译器一样思考".
想象一下,你正在编写一个编译器.你看到这样的代码.
// 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
错误.那么让我们将声明转移到Ah本身.// file: A.h class B; class A { B* _b; // or any of the other variants. };在Bh,在这一点上,你可以
#include "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.
但是你仍然不能在B上使用任何函数(如问题_b-> Printt())
"告诉编译器关于B"被称为B的前向声明.
我的天啊!完全错过了参考在占用空间方面已知的事实.最后,现在我可以正确设计!
这是我遇到的问题.如何在不完全重写头文件的情况下使用前向声明来实现这些功能?
@sydan:你做不到.[解决循环依赖*需要*类外定义](http://stackoverflow.com/q/7714345/103167).
2> Autodidact..:如果从头文件中删除方法定义并且让类仅包含方法声明和变量声明/定义,则可以避免编译错误.方法定义应放在.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" #includeusing namespace std; A::A(int val) :_val(val) { } void A::SetB(B *b) { _b = b; cout<<"Inside SetB()"< Print(); } void A::Print() { cout<<"Type:A val="<<_val< using namespace std; B::B(double val) :_val(val) { } void B::SetA(A *a) { _a = a; cout<<"Inside SetA()"< Print(); } void B::Print() { cout<<"Type:B val="<<_val<
如果你有模板方法怎么办?然后,除非手动实例化模板,否则无法将其移动到CPP文件中.
3> Tony Delroy..:我迟迟没回答这个问题,但是迄今为止没有一个合理的答案,尽管这是一个受到高度赞扬的答案的热门问题....
最佳实践:转发声明标头
如标准库的
标题所示,为其他人提供前向声明的正确方法是使用前向声明标头.例如:
a.fwd.h:
#pragma once class A;啊:
#pragma once #include "a.fwd.h" #include "b.fwd.h" class A { public: void f(B*); };b.fwd.h:
#pragma once class B;BH:
#pragma once #include "b.fwd.h" #include "a.fwd.h" class B { public: void f(A*); };
A
和B
库的维护者应该负责保持他们的前向声明标题与其标题和实现文件同步,所以 - 例如 - 如果"B"的维护者出现并重写代码...b.fwd.h:
templateclass Basic_B; typedef Basic_B B; BH:
templateclass Basic_B { ...class definition... }; typedef Basic_B B; ...然后重新编译"A"的代码将由包含的更改触发,
b.fwd.h
并且应该干净地完成.
可怜但常见的做法:在其他库中转发声明内容
说 - 而不是使用如上所述的前向声明标头 - 代码在
a.h
或a.cc
代替向前声明class B;
自己:
如果
a.h
或a.cc
之后包括b.h
:
A的编译一旦到达冲突的声明/定义就会以错误终止
B
(即上述对B的更改破坏了A和任何其他客户滥用前向声明,而不是透明地工作).否则(如果A最终没有包含
b.h
- 如果A仅通过指针和/或引用存储/传递Bs,则可能)
在更改为B之后,依赖于
#include
分析和更改的文件时间戳的构建工具将不会重建A
(及其进一步依赖的代码),从而导致链接时或运行时出错.如果B作为运行时加载的DLL分发,则"A"中的代码可能无法在运行时找到不同的错位符号,这可能会或可能不会很好地处理以触发有序关闭或可接受的减少的功能.
如果A的代码具有旧的模板特化/"特征"
B
,它们将不会生效.
@Farway对所有方面都是正确的.我没有打扰显示`main.cpp`,但很高兴您已经记录了评论中应包含的内容.干杯
4> dirkgently..:要记住的事情:
如果
class A
具有class B
作为成员的对象或反之亦然,则这将不起作用.前进声明是要走的路.
声明顺序事项(这就是你要移出定义的原因).
如果两个类都调用另一个类的函数,则必须将定义移出.
阅读常见问题:
如何创建两个彼此了解的类?
当前向声明与成员对象一起使用时需要哪些特殊注意事项?
当前向声明与内联函数一起使用时需要特别注意什么?
5> epatel..:我曾经通过在类定义之后移动所有内联并将
#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<......并在做同样的事情
B.h
请注意,您的标头保护使用保留标识符,保留任何具有双相邻下划线的标识符.
6> Eduard Wirch..:我曾写过一篇关于此的文章:解决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<Print(); } void Print() { cout<<"Type:B val="<<_val< #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; }
请注意,使用接口和"virtual"会影响运行时性能.