如果想将某个类用作基类,为什么该类必须定义而并非声明
对小学生来说,可以把它理解成:
继承就像“盖房子”,建新房子之前,必须知道老房子的完整结构,而不能只知道一个名字。
下面我们一步一步讲。
一、先认识两个概念:声明 vs 定义
在 C++ 中:
1. 声明(Declaration)
声明只是告诉编译器:
“有这么一个东西存在。”
例如:
class Animal;这句话叫:
类的前向声明(forward declaration)
意思:
“以后会有一个叫 Animal 的类。”
但是编译器不知道:
Animal 有几个成员变量?
Animal 有哪些函数?
Animal 占多少内存?
它只知道:
有这么一个名字。
2. 定义(Definition)
定义就是完整写出类:
class Animal { public: int age; void eat() { cout << "吃东西"; } };现在编译器知道:
Animal:
有一个 int 类型的 age
有一个 eat 函数
大小是多少
如何创建对象
二、什么叫“作为基类”?
例如:
class Dog : public Animal { };意思:
Dog 继承 Animal。
可以理解:
Animal | | DogDog 是 Animal 的孩子。
三、为什么基类必须定义?
因为创建 Dog 的时候:
Dog 身体里面包含 Animal 的部分。
例如:
class Animal { public: int age; }; class Dog : public Animal { };实际上:
Dog对象 +----------------+ | Animal部分 | | | | age | +----------------+ | Dog自己的部分 | +----------------+Dog 天生带有 Animal。
所以编译器必须知道:
Animal 到底有多大?
四、如果只有声明,会发生什么?
例如:
class Animal; // 只有声明 class Dog : public Animal { };编译器看到:
Dog继承Animal它会问:
“Animal是什么样?”
可是它只知道:
Animal这个名字存在不知道:
Animal里面有什么所以无法计算:
Dog对象占多少空间例如:
Dog dog;编译器需要分配:
多少字节?但是:
Animal??? 大小未知所以错误。
五、用盖房子的故事理解
假设:
爸爸 Animal 要给孩子 Dog 留房间。
爸爸房间:
+----------------+ | 爸爸的房间 | | 10平方米 | +----------------+孩子:
Dog = 爸爸房间 + 自己房间那么建 Dog 房子之前:
必须知道爸爸房间多大。
如果只听说:
“有一个爸爸。”
但是不知道:
爸爸房间多大
有几个房间
怎么建?
所以:
继承必须知道基类完整结构。
六、为什么普通指针可以只声明?
这里容易混淆。
例如:
class Animal; Animal* p;可以。
为什么?
因为:
指针大小固定。
比如:
64位系统:
指针 = 8字节编译器只需要知道:
“这是一个地址。”
不需要知道 Animal 有多大。
但是:
class Dog : public Animal { };不行。
因为:
Dog包含Animal。
必须知道:
Animal大小七、一个经典对比
情况1:指向对象
可以:
class Student; Student* p;因为:
只需要保存地址。
情况2:继承
不可以:
class Student; class GoodStudent : public Student { };因为:
GoodStudent需要包含Student。
八、还有一个重要原因:虚函数表
如果基类有虚函数:
class Animal { public: virtual void speak(); };那么对象里面还有:
虚函数表指针 vptr例如:
Animal对象 +-------------+ | vptr | +-------------+ | 成员变量 | +-------------+派生类:
Dog对象 +-------------+ | Animal部分 | | vptr | +-------------+ | Dog部分 | +-------------+如果不知道 Animal 定义:
连:
有没有虚函数
有没有vptr
内存布局
都不知道。
所以更加不能继承。
九、总结成一句小学生能记住的话
为什么基类必须定义?
因为:
派生类像是在基类身体上继续添加东西,编译器必须先知道基类完整长什么样,才能计算派生类的大小和布局。
口诀:
声明: 知道名字,不知道身体 定义: 知道身体,知道大小 指针: 只需要知道名字 继承: 必须知道身体十、和前面“虚析构函数”的知识联系起来
以后学习:
class Base { public: virtual ~Base() { } }; class Derived : public Base { };这里:
Derived继承Base。
编译器必须知道:
Base完整定义。
因为:
Derived对象里面包含Base部分。
所以:
继承关系比指针关系要求更多的信息。
这也是为什么:
class Base; Base* p; // 可以 class Derived : public Base { }; // 不可以这是 C++ 面向对象中非常核心的规则。