我经常发现自己处于这样一种情况:我在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<<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;
}
9 回答
我曾经通过在类定义之后移动所有内联并将
#include
用于头文件中的内联之前的其他类来解决这类问题 . 这样就可以确保在解析内联之前设置所有定义内联 .这样做可以在两个(或多个)头文件中仍然有一堆内联 . 但是有必要包括警卫 .
像这样
......并在
B.h
做同样的事情以下是模板的解决方案:How to handle circular dependencies with templates
解决这个问题的线索是在提供定义(实现)之前声明这两个类 . 将声明和定义拆分为单独的文件是不可能的,但您可以将它们组织为单独的文件 .
思考这个问题的方法是“像编译器一样思考” .
想象一下,你正在编写一个编译器 . 你看到这样的代码 .
在编译 .cc 文件时(请记住 .cc 而不是 .h 是编译单位),您需要为对象
A
分配空间 . 那么,那么多少空间呢?足够存储B
!那么B
的大小是多少?足够存储A
!哎呀 .显然是一个必须打破的循环引用 .
你可以通过允许编译器保留尽可能多的空间来解决它,因为它知道前端 - 指针和引用,例如,总是32位或64位(取决于架构),所以如果你替换(任何一个)一个指针或参考,事情会很棒 . 假设我们替换
A
:现在事情变得更好了 . 有些 .
main()
仍然说:#include
,对于所有范围和目的(如果取出预处理器)只需将文件复制到 .cc 中 . 所以, .cc 看起来像:你可以看到为什么编译器无法解决这个问题 - 它不知道
B
是什么 - 它以前从未见过这个符号 .那么让我们告诉编译器
B
. 这被称为forward declaration,将在this answer中进一步讨论 .这很有效 . 这不是很好 . 但是在这一点上你应该了解循环引用问题以及我们对它做了什么,尽管修复很糟糕 .
这个修复不好的原因是因为
#include "A.h"
的下一个人必须在他们可以使用之前声明B
并且会得到一个可怕的#include
错误 . 那么让我们将声明转移到 A.h 本身 .而在 B.h ,此时,您可以直接
#include "A.h"
.HTH .
要记住的事情:
如果
class A
的对象为class B
作为成员,则无效 .前进声明是要走的路 .
申报顺序事项(这就是你要移出定义的原因) .
如果两个类都调用另一个类的函数,则必须移出定义 .
阅读常见问题:
How can I create two classes that both know about each other?
What special considerations are needed when forward declarations are used with member objects?
What special considerations are needed when forward declarations are used with inline functions?
维基百科上提供的简单示例为我工作 . (你可以在http://en.wikipedia.org/wiki/Circular_dependency#Example_of_circular_dependencies_in_C.2B.2B阅读完整的描述)
文件'''a.h''':
文件'''b.h''':
文件'''main.cpp''':
我曾写过一篇关于此的帖子:Resolving circular dependencies in c++
基本技术是使用接口来分离类 . 所以在你的情况下:
不幸的是,之前的所有答案都缺少一些细节 . 正确的解决方案有点麻烦,但这是正确完成它的唯一方法 . 它可以轻松扩展,处理更复杂的依赖项 .
以下是您如何做到这一点,准确保留所有细节和可用性:
解决方案与最初的预期完全相同
内联函数仍然是内联的
A
和B
的用户可以按任何顺序包括A.h和B.h.
创建两个文件,A_def.h,B_def.h . 这些只包含
A
和_37735的定义:然后,A.h和B.h将包含这个:
请注意,A_def.h和B_def.h是"private"标头,
A
和B
的用户不应使用它们 . 公共 Headers 是A.h和B.h.如果从头文件中删除方法定义,则可以避免编译错误并让类只包含方法声明和变量声明/定义 . 方法定义应放在.cpp文件中(就像最佳实践指南所说) .
以下解决方案的缺点是(假设您已将方法放在头文件中以内联它们),编译器不再内联这些方法,并尝试使用inline关键字产生链接器错误 .
尽管这是一个受到高度赞扬的答案的热门问题,但我对这个日期并不是一个合理的答案....
最佳实践:转发声明 Headers
如标准库的
<iosfwd>
Headers 所示,为其他人提供前向声明的正确方法是使用 forward declaration header . 例如:a.fwd.h:
啊:
b.fwd.h:
b.h:
A
和B
库的维护者应该负责保持它们的前向声明头与它们的头和实现文件同步,所以 - 例如 - 如果"B"的维护者出现并重写代码...b.fwd.h:
b.h:
...然后重新编译"A"的代码将由包含的
b.fwd.h
的更改触发,并且应该干净地完成 .可怜但常见的做法:在其他库中转发声明内容
说 - 而不是使用如上所述的前向声明标头 -
a.h
或a.cc
中的代码而不是前向声明class B;
本身:如果
a.h
或a.cc
之后确实包含b.h
:A的编译一旦到达
B
的冲突声明/定义就会以错误终止(即上述对B的更改破坏了A和任何其他滥用前向声明的客户端,而不是透明地工作) .否则
b.h
- 如果A只通过指针和/或引用存储/传递Bs,则可能)依赖于
#include
分析和更改文件时间戳的A
(及其进一步依赖的代码),从而导致链接时或运行时出错 . 如果B作为运行时加载的DLL分发,则"A"中的代码可能无法在运行时找到不同的错位符号,这可能会或可能不会处理得足以触发有序关闭或可接受的减少功能 .如果A的代码具有旧
B
的模板特化/ "traits",则它们将不会生效 .