多态的内存结构

这里的多态指的是动态多态,不包括函数重装和模版的静态多态内容。多态所代表的特性,才是面向对象的真正表现。具体行为其实就是用基类的指针或者引用调用虚函数,得到的结果跟指针指向对象的具体类型有关,而跟指针的静态类型无关。比如说,基类指针指向派生类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;//0:male,1:female
};

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;//0:no,1:yes
};

再定义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
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。