类声明(如class widget;)仅为前置声明,不提供类型完整信息,无法计算大小或实例化;完整定义(如class widget { ... };)才确立内存布局与成员访问规则,是类型“落地成实体”的标志。

类的声明只是让编译器“认出这个名字”
比如 class Widget; 这一行,编译器只记住“存在一个叫 Widget 的类”,但完全不知道它有多大、有哪些成员、能不能实例化。此时你不能做这些事:sizeof(Widget)、Widget w;、Widget* p = new Widget;——都会报错,提示 incomplete type。
这种声明常见于头文件前置声明(forward declaration),用来打破头文件循环包含,或减少编译依赖。但它本身不生成任何符号,也不触发任何构造/析构逻辑。
类的完整定义才真正“落地成实体”
class Widget { public: int x; void foo(); }; 这才是定义:编译器现在能算出 sizeof(Widget)(至少是 4),能为 Widget w; 分配栈内存,也能检查成员访问是否合法。
注意:类定义中的成员函数如果在类体内写实现(如 void foo() { return; }),那这个函数既是声明也是内联定义;如果只写声明(void foo();),则必须在类外提供定义(void Widget::foo() { ... }),否则链接时会报 undefined reference。
- 类定义本身不分配对象内存,但它是对象内存布局的依据
- 静态数据成员(如
static int count;)在类内只是声明,必须在类外单独定义(int Widget::count = 0;) - const static 整型成员可在类内初始化(
static const int N = 42;),这属于声明+常量折叠,不算 ODR 意义上的定义
为什么 class A {}; 算定义,而 class A; 不算?
因为 class A {}; 提供了完整的类型信息:编译器能确定它是空类(sizeof(A) == 1)、能生成默认构造/析构、能作为基类或模板实参使用。而 class A; 连大小都不知道,连 std::is_class_v<a></a> 都求值为 false(在定义前)。
容易踩的坑:
– 在头文件里只写 class B;,却在同一个头里用 std::vector<b></b> 或 B member;:非法,std::vector 要求完整类型
– 把静态成员的定义漏写在 .cpp 里,只留类内声明:链接失败
– 误以为 class C { static const int v = 1; }; 就完成了定义:对整型 const static 成员,这确实够用;但换成 double 或非 const 就必须类外定义
类模板的声明和定义更不能混
模板不是类,它是一套生成类的“配方”。template<typename t> class List;</typename> 是声明,但几乎没人这么写——因为模板通常要立刻定义,否则无法实例化。真正常见的是:在头文件中同时完成声明与定义(即模板定义必须可见于所有使用点)。
如果你把模板定义放到 .cpp 里,其他文件包含头文件后尝试 List<int> l;</int>,编译器会静默通过(因有声明),但链接时找不到 List<int></int> 的任何成员定义——因为模板没被实例化出来。这是比普通类更隐蔽的错误。
所以实际工程中:
– 普通类:头文件放完整定义,或拆成声明(.h)+ 实现(.cpp)
– 类模板:头文件必须包含完整定义(或显式实例化导出,但极少用)
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











