这里的多态指的是动态多态,不包括函数重装和模版的静态多态内容。多态所代表的特性,才是面向对象的真正表现。具体行为其实就是用基类的指针或者引用调用虚函数,得到的结果跟指针指向对象的具体类型有关,而跟指针的静态类型无关。比如说,基类指针指向派生类A的对象,那么调用的其实是派生类A里面实现的虚函数,如果是派生类B的对象,那么则是B类里面实现的虚函数。
我们先定义一个接口。也就是一个包含纯虚函数的类,这种类不能用于构造对象。其实,接口才是我们最关心的。
1 2 3 4 5 6 7
| #define interface struct interface IAnimal { virtual void Eat() = 0; virtual void Drink() = 0; virtual void Sleep() = 0; };
|
然后定义2个类继承这个接口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
| class CMonkey : public IAnimal { public: CMonkey() { m_nSex = 0; }
virtual void Eat() { cout << "Monkey Eat" << endl; }
virtual void Drink() { cout << "Monkey Drink" << endl; }
virtual void Sleep() { cout << "Monkey Sleep" << endl; }
enum SEX { MALE = 0, FEMALE = 1 };
int GetSex() const { return m_nSex; } void SetSex(int nSex) { m_nSex = nSex; }
private: int m_nSex; };
class CMonster : public IAnimal { public: CMonster() { m_nEvil = NO; }
virtual void Eat() { cout << "Monster Eat" << endl; }
virtual void Drink() { cout << "Monster Drink" << endl; }
virtual void Sleep() { cout << "Monster Sleep" << endl; }
enum EVIL { NO = 0, YES = 1 };
int GetEvil() const { return m_nEvil; } void SetEvil(int nEvil) { m_nEvil = nEvil; }
private: int m_nEvil; };
|
再定义CPeople多重继承上面2个类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| class CPeople : public CMonkey, public CMonster { public: virtual void Eat() { cout << "People Eat" << endl; }
virtual void Drink() { cout << "People Drink" << endl; }
virtual void Sleep() { cout << "People Sleep" << endl; }
virtual void Name() { cout << "People Name" << endl; }
const char* GetName() const { return m_szName; } void SetName(char* pszName) { strcpy(m_szName, pszName); }
private: char m_szName[12]; };
|
我们先来测试多态是如何表现的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| CMonkey monkey; monkey.SetSex(CMonkey::MALE);
CMonster monster; monster.SetEvil(CMonster::YES);
CPeople people; people.SetName("Jack");
IAnimal* pAnimal[3]; pAnimal[0] = monkey; pAnimal[1] = monster; pAnimal[2] = (CMonkey*)people; for (int i = 0; i < 3; ++i) { pAnimal[i]->Eat(); pAnimal[i]->Drink(); pAnimal[i]->Sleep(); } cout << endl;
|
这段代码的输出如下:
从代码的输出可以看到,pAnimal[i]是根据所指向的数据类型确定该调用什么函数的。这就是多态的意思。相同的接口,不同的表现。
下面我们来推测这几个对象的内存布局。首先是monkey。
假设,对象monkey的内存布局,如图:
也就是,monkey保护2个成员,一个虚函数表指针和一个int成员。虚函数表指针指向一个表,前面3项是函数地址,第四项是NULL,第五项根据有些书籍的说法是type_info对象的地址。
我用下面的代码来验证这个内存布局。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| typedef void (*FunType) (); FunType p; int nSex; int* vtbl = NULL; cout << "sizeof(monkey): " << sizeof(monkey) << endl; vtbl = (int*)(*(int*)monkey); cout << "vtbl address:" << (int)vtbl << endl; p = (FunType)vtbl[0]; p(); p = (FunType)vtbl[1]; p(); p = (FunType)vtbl[2]; p(); cout << "vtbl[3]:" << vtbl[3] << endl; cout << "vtbl[4]:" << vtbl[4] << endl; nSex = *((int*)monkey + 1); cout << "nSex: " << nSex << endl << endl;
|
输出如同:
从输出可以看到,monkey大小为8字节,刚好是一个指针和一个int的大小。而且,monkey的地址转换为int*之后,再取值就可以得到虚函数表的地址。用vtbl[0]到vtbl[2]调用刚好是调用接口里面定义的三个虚函数。vtbl[3]为0,vtbl[4]为一个地址。为什么,这里推断vtbl里面含有5项了,而不是4项了。下面再说明。
monster的内存布局类似,就不说明了。
最关键的是people的内存布局。推测如下图所示:
用下面的代码,来验证这个假设。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| cout << "sizeof(people): " << sizeof(people) << endl; vtbl = (int*)(*(int*)people); cout << "vtbl address:" << (int)vtbl << endl; p = (FunType)vtbl[0]; p(); p = (FunType)vtbl[1]; p(); p = (FunType)vtbl[2]; p(); p = (FunType)vtbl[3]; p(); cout << "vtbl[4]:" << vtbl[4] << endl; cout << "vtbl[5]:" << vtbl[5] << endl;
nSex = *((int*)people + 1); cout << "nSex: " << nSex << endl << endl;
vtbl = (int*)(*((int*)people + 2)); cout << "vtbl address:" << (int)vtbl << endl; p = (FunType)vtbl[0]; p(); p = (FunType)vtbl[1]; p(); p = (FunType)vtbl[2]; p(); cout << "vtbl[3]:" << vtbl[3] << endl; cout << "vtbl[4]:" << vtbl[4] << endl; nEvil = *((int*)people + 3); cout << "nEvil: " << nEvil << endl << endl;
char* pszName = (char*)((int*)people + 4); cout << pszName << endl;
|
输出如同:
people大小为28字节,符合假设。第一个成员为继承自CMonkey的虚函数表指针,符合假设。虚函数表的前三项,是在CPeople类中实现的接口里面虚函数地址,接下来CPeolple里面定义的虚函数Name的地址,最后是NULL和type_info obj的地址。people的第三个成员为继承自CMonster的虚函数指针,指向虚函数表的第七项。从第七项开始,可以看作继承自CMonster的虚函数表。
从输出可以看出,CPeople里面新定义的虚函数,放到第一个虚函数指针所指向的虚表里面了。并且,由于一个类只有只有一张虚表,所以不同的虚函数指针所指向的其实是虚表的不同部分。这些表可以看作是独立的,也可以看作是地址上面连续的。
前面说过,为什么猜测虚函数表里面还有一个NULL项了,然后才跟随一个type_info obj的地址?由于,在上面的输出中,2个虚函数指针相差是24。所以,我猜测第一个虚表里面含有6项。经过代码测试,第五项也是0。
至于为什么猜测内存布局里面先是虚函数表的指针,再是数据成员,这个是我通过代码测试得来的。环境是vs2013。