整理:侯捷老师的《面向对象高级编程》(下)
理解对象模型,才能真正理解多态和动态绑定.
1 成员函数和成员变量在内存中的分布
下面程序在内存中的布局如下所示:
class A {
public:
virtual void vfunc1();
virtual void vfunc2();
void func1();
void func2();
private:
int m_data1;
int m_data2;
};
class B : public A {
public:
virtual void vfunc1();
void vfunc2();
private:
int m_data3;
};
class C : public B {
public:
virtual void vfunc1();
void vfunc2();
private:
int m_data1;
int m_data4;
};
代码很简单,继承也很清晰。
其在内存中的布局如下图所示:【这个图是精髓】
下面来解释一下这个图:
1 A内存结构:下图中用红框框起来。
对于A而言,其内存有两个数据成员:m_data1、m_data2,这是占类的大小的。
还有两个虚函数:virtual void vfunc1()和virtual void vfunc2()。那么虚函数在类中的内存分布是怎
样的呢?
首先,对于每个含有虚函数的类,系统都会自动生成虚函数表(简称虚表vtbl),虚表中存储A类中的每个虚函数的函数指针;然后,在A类实例化对象时,对象地址的前4个(32位机器)字节存储指向虚表的指针,简称虚指针(vptr)。
所以,对于A类而言,初始化对象时,内存中有两个数据成员和一个虚指针(虚指针指向虚表,虚表存储的是两个虚函数的地址)。
2 B的内存结构:下图用红框框起来
B类继承了A类,那么它就有A类的数据成员和成员函数,所以它的数据域有从A类继承的m_data1
和m_data2,还有自己的数据m_data3;它又实现A类中的虚函数vfunc1,自己还有一个func2函
数,这个函数不是虚函数,是非静态函数,不占内存。
所以,对于B类而言,初始化对象时,内存中有三个数据成员(两个父类的,一个自己的),一个
虚指针(虚指针指向虚表,虚表中存储的是自己重写的vfunc1函数的地址和继承父类A的虚函数
vfunc2的地址)。
3 C的内存结构:下图用红框框起来
C类的分析和B类的分析一样,只是还要多考虑A类
C类继承了B类,那么它就有B中的数据成员和成员函数,所以它的数据域有m_data1、m_data2(这两个是从A类继承来的)、m_data3(从b类继承来的)、m_data4(自己的);它又实现了B类的虚函数vfunc1,还有一个自己的不占内存的非静态函数func2。
所以,对于C类而言,初始化对象时,内存中有四个数据成员(A类的两个、B类的一个、自己的一个),一个虚指针(虚指针指向虚表,虚表中存储C类重写的vfunc1函数的地址和继承父类的虚函数vfunc2函数的地址)。
先看成员变量部分: 对于成员变量来说,每个子类对象重都包含父类的成分,值得注意的是, C 类的m_data1 字段和父类 A 类的字段 m_data1 相同,这两个字段共存于 C 类的对象中.
再看函数的部分,每个含有虚函数的对象都包含一个特殊的指针 vptr ,指向存储函数指针的虚表 vtbl .编译器根据 vtbl 表中存储的函数指针找到虚函数的具体实现.这种编译函数的方式被称为动态绑定
更为一般的多态的过程:
(1)编译器在发现基类中有虚函数时,会自动为每个含有虚函数的类生成一份虚表,该表是一个一维数组,虚表里保存了虚函数的入口地址;
(2)编译器会在每个对象的前四个字节中保存一个虚表指针,即vptr,指向对象所属类的虚表。在构造时,根据对象的类型去初始化虚指针vptr,从而让vptr指向正确的虚表,从而在调用虚函数时,能找到正确的函数;
(3)所谓的合适时机,在派生类定义对象时,程序运行会自动调用构造函数,在构造函数中创建虚表并对虚表初始化。在构造子类对象时,会先调用父类的构造函数,此时,编译器只“看到了”父类,并为父类对象初始化虚表指针,令它指向父类的虚表;当调用子类的构造函数时,为子类对象初始化虚表指针,令它指向子类的虚表;
(4)当派生类对基类的虚函数没有重写时,派生类的虚表指针指向的是基类的虚表;当派生类对基类的虚函数重写时,派生类的虚表指针指向的是自身的虚表;当派生类中有自己的虚函数时,在自己的虚表中将此虚函数地址添加在后面;
这样指向派生类的基类指针在运行时,就可以根据派生类对虚函数重写情况动态的进行调用,从而实现多态性。
2 静态绑定和动态绑定
- 对于一般的非虚成员函数来说,其在内存中的地址是固定的,编译时只需将函数调用编译成 call 命令即可,这被称为静态绑定.
- 对于虚成员函数,调用时根据虚表 vtbl 判断具体调用的实现函数,相当于先把函数调用翻译成 (*(p->vptr)[n])(p) ,这被称为动态绑定.
静态绑定和动态绑定编译出的汇编代码如下所示: