从继承的角度出发再探多态

我们先通过一段代码来理解继承的底层实现。

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

class CBase {
public:
CBase() {};
~CBase() {};
void SetNumber(int nNum) { nNumber = nNum; };

public:
int nNumber;
};

class CChild : CBase {
public:
void ShowNumber(int nNum) {
SetNumber(nNum);
nNumberChild = nNum + 1;
printf("%d\n", nNumber);
}

public:
int nNumberChild;
};

int main()
{
CChild cChild;
cChild.ShowNumber(23);
}

上面的代码中子类虽然没有写构造函数和析构函数,但是编译器还是自动生成了它们,子类构造函数、析构函数和父类的构造函数、析构函数调用顺序如下:

父类构造函数 -> 子类构造函数 -> 子类析构函数 -> 父类析构函数

我们关注的重点并不在这里,而是子类对象和父类对象的关系。

走进ShowNumber函数:

1
2
3
4
5
6
7
......
0099198D mov eax,dword ptr [ebp+8] ;参数nNum
00991990 push eax ;参数压栈
00991991 mov ecx,dword ptr [ebp-8] ;获取this指针
00991994 call 0099102D ;调用父类的SetNumber
}
......

在子类调用父类函数时,直接传递了子类的this指针,我们走进这个SetNumber :

1
2
3
4
5
......
0099192D mov eax,dword ptr [ebp-8] ;获取this指针
00991930 mov ecx,dword ptr [ebp+8] ;取出nNum的值
00991933 mov dword ptr [eax],ecx ;将nNum赋值到this指针的前4个字节也就是代码中的nNumber变量
......

执行结束回到ShowNumber继续执行

1
2
3
4
5
6
7
8
9
10
11
12
13
00991994  call        0099102D  ;调用父类的SetNumber

00991999 mov eax,dword ptr [ebp+8] ;取出参数nNum
0099199C add eax,1 ;临时nNum + 1
0099199F mov ecx,dword ptr [ebp-8] ;获取this指针
009919A2 mov dword ptr [ecx+4],eax ;赋值到nNumberChild

009919A5 mov eax,dword ptr [ebp-8] ;取出this指针
009919A8 mov ecx,dword ptr [eax] ;取出nNumber
009919AA push ecx ;压栈nNumber
009919AB push 998B30h ;压栈字符串
009919B0 call 00991050 ;调用printf
009919B5 add esp,8

由此我们看出父类的nNumber赋值到this的前4个字节,而子类的nNumberChild赋值到this的第四个字节开始的后面四个字节。

那么此时this的内存结构如下图:

1571140201271

由此,我们可以总结出,父类对象在子类对象开始处,那么将上例中的CChild的类修改为下面的样子,则他们的内存结构时完全一样的。

1
2
3
4
5
6
7
8
9
10
11
12
class CChild {
public:
void ShowNumber(int nNum) {
SetNumber(nNum);
nNumberChild = nNum + 1;
printf("%d\n", nNumber);
}

public:
CBase cBase;
int nNumberChild;
};

这种内存结构的优势是什么?

很明显,子类对象调用父类的函数,直接传递子类的对象地址就可以了,那么子类对象指针可以强制转换为父类对象指针来使用,反之则不行。

——————->分割线

再来聊聊多态,上代码:

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
class cBase {
public:
cBase() {};
virtual ~cBase() {};
virtual void Print() { printf("I am cBase\n"); };
};


class cChild0 : cBase{
public:
cChild0() {};
virtual ~cChild0() {};
virtual void Print() { printf("I am cChild0\n"); };
};

class cChild1 : cBase{
public:
cChild1() {};
virtual ~cChild1() {};
virtual void Print() { printf("I am cChild1\n"); };
};


void GoPrint(cBase* pBase)
{
pBase->Print();
}

void main()
{
cChild0 cCCHild0;
cChild1 cCCHild1;

GoPrint((cBase*)&cCCHild0);
GoPrint((cBase*)&cCCHild1);
}

先来看看输出:

1571141767552

是不是意料之中的结果?

来看看内部实现吧,先从cChild0的构造函数开始吧:

1
2
3
4
5
6
7
8
9
......
00D7183F pop ecx
00D71840 mov dword ptr [this],ecx
00D7184D mov ecx,dword ptr [this] ;以上为this指针操作
00D71850 call cBase::cBase (0D713EDh) ;调用父类构造函数
00D71855 mov eax,dword ptr [this] ;取出this指针
00D71858 mov dword ptr [eax],offset cChild0::`vftable' (0D78B54h) ;虚表赋值
00D7185E mov eax,dword ptr [this] ;返回this指针
......

首先调用了父类的构造函数,然后赋值虚表为本类(cChild0)的虚表。

走进cBase的构造函数:

1
2
3
4
5
6
7
......
00D717DF pop ecx
00D717E0 mov dword ptr [this],ecx
00D717ED mov eax,dword ptr [this] ;以上为this指针操作
00D717F0 mov dword ptr [eax],offset cBase::`vftable' (0D78B34h) ;初始化虚表
00D717F6 mov eax,dword ptr [this] ;返回this指针
......

在构造函数中只做一件事,就是赋值虚表为本类(cBase)的虚表。

总结下,在cChild0的构造函数中做了以下的事情:

调用父类构造函数 -> 在父类的构造函数中设置虚表为本类(cBase)的虚表 -> 设置虚表为本类的(cChild0)虚表

需要注意的是,在上文中设置两次虚表都是cChild0 this指针的前四个字节。

在cChild1中做了同样的事情,就不再次赘述了。

那么现在已经很清晰了,这两个子类对象在构造函数调用之后会将虚表都设成自己的虚表。

现在我们来看看GoPrint函数吧:

1
2
3
4
5
6
7
8
......
00D725E8 mov eax,dword ptr [pBase] ;取出参数,传递进来的对象
00D725EB mov edx,dword ptr [eax] ;取出虚表
00D725ED mov esi,esp
00D725EF mov ecx,dword ptr [pBase] ;设置this指针
00D725F2 mov eax,dword ptr [edx+4] ;根据虚表偏移取出虚函数
00D725F5 call eax ;调用虚函数
......

GoPrint函数就很清晰了,直接取出虚表根据偏移调用虚函数,也就理解了程序上面的输出。

现在我们在说说在《我们来聊聊C++多态吧,理解它,并找到它》中我们没有说到的内容,为什么在虚构函数中,要对多态表重新赋值。

在上例中,析构函数的执行顺序如下:

子类析构函数 -> 父类析构函数

那么问题出现了,假设在这两个析构函数中同时调用虚函数,如果在析构函数中没有对虚函数表重新赋值,那么在父类的析构函数中就会调用子类的析构函数,而这个时候子类也许有一些资源已经释放了,那么问题就已经很清晰了,内存泄漏!

1571159873270

Author: YuanBi
Link: https://www.basicbit.cn/2018/11/12/2018-11-12-从继承的角度再来聊聊多态吧/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.