我经常发现自己处于这样一种情况:我在C ++项目中面临多个编译/链接器错误,因为一些糟糕的设计决策(由其他人做出:))导致不同头文件中C ++类之间的循环依赖< em>(也可能发生在同一个文件中)。但幸运的是(?)这种情况经常不足以让我在下次再次发生时记住这个问题的解决方案。
因此,为了便于将来回忆,我将发布一个代表性问题和解决方案。当然,更好的解决方案是受欢迎的。
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<<endl;
}
};
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<<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;
}
答案 0 :(得分:248)
考虑这个问题的方法是“像编译器一样思考”。
想象一下你正在编写一个编译器。你看到这样的代码。
// 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
。这称为forward declaration,将在this answer进一步讨论。
// 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 中,此时,您可以直接#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 :(得分:96)
如果从头文件中删除方法定义并且让类仅包含方法声明和变量声明/定义,则可以避免编译错误。方法定义应放在.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;
}
答案 2 :(得分:17)
要记住的事情:
class A
的对象class B
作为成员,则不起作用,反之亦然。 阅读常见问题:
答案 3 :(得分:15)
我迟迟没有回答这个问题,但是迄今为止没有一个合理的答案,尽管这是一个受到高度赞扬的答案的热门问题....
如标准库的<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
的更改触发,并且应该干净地完成。
Say - 而不是如上所述使用前向声明标头 - a.h
或a.cc
中的代码而不是前向声明class B;
本身:
a.h
或a.cc
之后确实包含b.h
:
B
的冲突声明/定义就会以错误终止(即上述对B的更改打破了A和任何其他客户滥用前向声明,而不是透明地工作)。 / LI>
b.h
- 如果A仅通过指针和/或引用存储/传递Bs,则可能)
#include
分析和更改文件时间戳的构建工具在更改为B后不会重建A
(及其进一步依赖的代码),从而导致链接时或运行时出错。如果B作为运行时加载的DLL分发,则“A”中的代码可能无法在运行时找到不同的错位符号,这可能会或可能不会处理得足以触发有序关闭或可接受的减少功能。如果A的代码具有旧版B
的模板特化/“特征”,则它们不会生效。
答案 4 :(得分:11)
我曾经通过在类定义之后移动所有 inlines 并在{em> inlines 之前的其他类中放置#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<<endl;
}
#endif /* __A_H__ */
...并在B.h
答案 5 :(得分:6)
我曾写过一篇关于此事的帖子:Resolving circular dependencies in 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;
}
答案 6 :(得分:3)
以下是模板的解决方案:How to handle circular dependencies with templates
解决此问题的线索是在提供定义(实现)之前声明这两个类。将声明和定义拆分为单独的文件是不可能的,但是您可以将它们组织为单独的文件。
答案 7 :(得分:2)
维基百科上的简单示例为我工作。 (您可以阅读http://en.wikipedia.org/wiki/Circular_dependency#Example_of_circular_dependencies_in_C.2B.2B)
的完整说明文件&#39;&#39; a.h&#39;&#39;&#39;:
#ifndef A_H
#define A_H
class B; //forward declaration
class A {
public:
B* b;
};
#endif //A_H
文件&#39;&#39; b.h&#39;&#39;&#39;:
#ifndef B_H
#define B_H
class A; //forward declaration
class B {
public:
A* a;
};
#endif //B_H
文件&#39;&#39;&#39;&#39;&#39;&#39;:
#include "a.h"
#include "b.h"
int main() {
A a;
B b;
a.b = &b;
b.a = &a;
}
答案 8 :(得分:0)
不幸的是,所有先前的答案都缺少一些细节。正确的解决方案有点麻烦,但这是正确执行此操作的唯一方法。而且可以轻松扩展,也可以处理更复杂的依赖项。
这是您可以做到的,完全保留所有细节和可用性的方法:
A
和B
的创建两个文件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。
答案 9 :(得分:0)
在某些情况下,可以在A类的头文件中定义 B类的方法或构造函数,以解决涉及定义的循环依赖关系。
这样,您可以避免将定义放入.cc
文件中,例如,如果您要实现仅标头的库。
// file: a.h
#include "b.h"
struct A {
A(const B& b) : _b(b) { }
B get() { return _b; }
B _b;
};
// note that the get method of class B is defined in a.h
A B::get() {
return A(*this);
}
// file: b.h
class A;
struct B {
// here the get method is only declared
A get();
};
// file: main.cc
#include "a.h"
int main(...) {
B b;
A a = b.get();
}
答案 10 :(得分:0)
不幸的是,我无法评论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
此指令应延迟当前文件的处理并完成所有待处理的包含。