应使用 class X; 前置声明而非 #include "X.h" 时,仅当代码中只涉及 X*/X&、函数参数/返回值,且不访问成员、不调用构造/析构、不计算 sizeof(X);否则必须 #include 完整定义。

什么时候该用 class X; 而不是 #include "X.h"
当你只需要指针或引用类型(X*、X&)、函数参数/返回值中出现 X,但不访问其成员、不调用构造/析构、不计算 sizeof(X) 时,前置声明就足够了。比如在头文件里声明一个接受 std::shared_ptr<config></config> 的函数,Config 就可以只前置声明。
常见错误是:在头文件里写了 class Widget;,却紧接着定义 std::vector<widget> widgets;</widget> —— 这会编译失败,因为 std::vector 需要完整类型;同理,Widget w; 或 sizeof(Widget) 也必须看到完整定义。
- 能用前置声明的场景:
void load(const Config& c);、std::unique_ptr<logger> logger_;</logger>、void set_handler(std::function<void> cb);</void> - 必须
#include的场景:Config config_;、std::vector<widget> list;</widget>、auto w = Widget{};
friend 声明和前置声明容易冲突
如果类 A 声明 friend class B;,而 B 只被前置声明过(class B;),GCC/Clang 会报错:friend declaration declares a non-template class。这不是语法错误,而是因为 friend 关系要求编译器知道 B 是个类(而非 typedef 或 enum),但仅靠前置声明不足以支撑友元语义检查。
解决办法只有两个:要么把 B 的完整定义 #include 进来(通常放头文件顶部),要么把 friend 声明挪到 B 定义之后(比如放到 .cpp 文件里,用 class B; + friend 组合)。
立即学习“C++免费学习笔记(深入)”;
- 错误写法:
class B;→class A { friend class B; }; - 可行写法1:
#include "B.h"在A.h开头,再写friend class B; - 可行写法2:在
A.cpp中前置声明B,并在类外补全friend声明(需确保A的定义已可见)
模板类不能靠简单前置声明来使用
写 template <typename t> class Container;</typename> 并不能让你在别处安全使用 Container<int></int> —— 模板的实例化需要完整定义。你可能会看到编译器报错:invalid use of incomplete type 'class Container<int>'</int>。
除非你只声明指针类型(Container<int>*</int>),否则必须 #include 模板类的完整定义(通常是头文件,含所有实现)。C++20 的 export 模板已弃用,所以没有绕过这一限制的通用方案。
- 允许:
Container<int>* ptr;</int>、void process(Container<string>&);</string>(参数是引用,但函数体在 .cpp 里才展开) - 不允许:
Container<double> c;</double>、c.size();、std::is_same_v<container>, Container<float>></float></container>
循环依赖下前置声明救不了所有情况
两个头文件互相 #include,光靠前置声明往往不够。比如 A.h 前置声明 class B; 并声明 std::shared_ptr<b> b_;</b>,而 B.h 也前置声明 class A; 并声明 A* a_; —— 看似合理,但一旦某个头文件里出现内联函数(比如 A::get_b() { return b_.get(); }),就会因 B 类型不完整而失败。
根本解法是把相互依赖的逻辑下沉到实现文件:头文件只保留最小接口(用指针/引用 + 前置声明),所有涉及对方成员访问、构造、拷贝的代码移到 .cpp 中,并在那里 #include 对方头文件。
- 头文件
A.h:只写class B;和std::shared_ptr<b> b_;</b>,不写任何访问b_->xxx()的内联函数 - 实现文件
A.cpp:#include "B.h",再实现void A::do_something_with_b() { b_->run(); } - 避免在头文件里写
= default析构函数,如果它依赖B的完整定义(编译器可能隐式生成)
前置声明真正起效的前提,是你严格控制头文件里的“信息暴露边界”——不是加了 class X; 就万事大吉,而是要同步检查:这里有没有隐式触发完整类型需求的操作?稍不注意,sizeof、模板实例化、内联函数、甚至 PIMPL 的析构声明,都会让前置声明瞬间失效。


















