具有虚函数的类的大小。
一般的书上都说,虚函数是在运行时根据对象的实际类型“动态决定”函数入口。但什么是“动态决定”呢?实际上C++编译器在实现这个功能的时候,并非真的 等到虚函数被调用时才去判断这个对象是什么类型的。下面我用一个简单的图表来说明C++编译器到底干了些什么。假设有两个类
struct Base {
virtual void f();
virtual void g();
};
struct Derived : public Base {
virtual void f();
virtual void g();
};
Base 和 Derived 各有一个虚表,分别是 VTable_B 和 VTable_D ,那么编译器是怎么通过只用一个虚表指针来实现下面的“动态”调用呢?
Base *pB = new Derived();
pB->f();
先让我们看Base和Derived对象是怎么存储的,以及两个类的虚表结构
Base: VTable_B:
------------ -------------
| vptr | | f() 入口 |
+---------+ +----------+
| Base的 | | g() 入口 |
| 数据 | -------------
------------
Derived: VTable_D:
------------ --------------
| vptr | | f() 入口 |
+---------+ +----------+
| Base的 | | g() 入口 |
| 数据 | -------------
+---------+
| Derived的|
| 数据 |
------------
pB 指针既可以指向 Base 对象,也可以指向 Derived 对象,所以 pB 本身是不确定的。但是,任何对象本身却从被 new 出来开始就是确定的,所以 Base 对象在构造时,编译器会往 vptr 中填上 VTable_B 的地址,而 Derived 对象在构造时,编译器会往 vptr 中填上 VTable_D 的地址。
等到虚函数被调用的时候,也就是 pB->f() 这行语句被执行的时候,编译器并不需要知道 pB 到底是指向 Base 还是 Derived ,它只要直接用 vptr 就能找到正确的虚表和虚函数入口了,父类和子类的虚表结构是相似的,同一个虚函数入口在父表和子表的偏移量都是一样的。
通过上面这些介绍,我想你应该能理解,为什么在单一继承的条件下,不管有多少层继承,每个对象只需一个 vptr 就行了。
多重继承的条件下,一个 vptr 行不行呢?
不行。多重继承的时候,虚函数既有来自父类1的,也有来自父类2的,所以这些虚函数入口是不能放在同一个虚表当中的。假设 Derived 除了 Base外,还继承 Base2,并且 Base2 中有两个虚函数 x() 和 y (),那么 Derived 对象的存储结构也许是这样的(只是大概,和具体编译器相关)。
Derived: VTable_D:
------------ --------------
| vptr | | f() 入口 |
+---------+ +----------+
| Base的 | | g() 入口 |
| 数据 | -------------
+---------+
| vptr2 | VTable_D2:
+---------+ -------------
| Base2的 | | x() 入口 |
| 数据 | +-----------+
+---------+ | y() 入口 |
| Derived的| -------------
| 数据 |
------------