循环引用根源在于头文件互相#include,应改用前置声明class b;替代,并将需完整定义的代码移至.cpp文件中;指针、引用、智能指针声明可留在头文件,但构造、析构、模板实例化等必须在.cpp中包含对应头文件。

头文件之间直接互相 #include 是循环引用的根源,不是语法错误,而是编译器在解析类定义时看不到完整类型——比如报 error: field 'xxx' has incomplete type 或 error: 'ClassName' does not name a type,基本就能断定是这个问题。
用前置声明替代头文件包含
当一个类只以指针、引用或友元形式出现在另一个类中,不需要知道其内存布局,只需告诉编译器“这个类存在”即可。
-
class B;写在A.h顶部,代替#include "B.h" - 成员变量只能是
B*、B&或std::unique_ptr<b></b>等不依赖sizeof(B)的类型 - 不能写
B b;、std::vector<b></b>、class A : public B {}—— 这些都要求完整定义 - 函数参数/返回值若为
B值类型(非指针/引用),也必须改掉
把需要完整类型的代码移到 .cpp 文件
前置声明只解决“声明”,不解决“使用”。只要涉及构造、析构、拷贝、sizeof、模板实例化(如 std::vector<b></b>),就必须看到 B 的完整定义。
-
A.h中删掉#include "B.h",只留class B; - 把
A::foo(B* b)的实现(哪怕只有一行调用)从头文件挪到A.cpp -
A.cpp开头加上#include "B.h"—— 此时编译单元可见完整定义 - 同理处理
B.h对A的依赖,避免双向补全
注意智能指针和模板的隐含依赖
std::unique_ptr<b></b> 和 std::shared_ptr<b></b> 在头文件里声明成员是安全的(它们只依赖前向声明),但一旦调用 std::make_unique<b>()</b> 或 std::make_shared<b>()</b>,就必须在该处包含 B.h。
-
std::vector<b></b>、std::array<b></b>、std::function<void></void>等模板,几乎都要求B是完整类型 -
std::weak_ptr<b></b>同样只依赖前向声明,但lock()返回的std::shared_ptr<b></b>不会触发额外依赖 - 如果用了 PIMPL,
std::unique_ptr<impl></impl>成员可以藏在 .cpp 里,头文件完全不暴露Impl定义
最容易被忽略的是:你以为加了 class B; 就万事大吉,结果在头文件里写了 list.push_back(std::make_unique<b>())</b> —— 这行代码必须进 .cpp,否则编译器仍会报错。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











