首页
论坛
专栏
课程

[系统底层] [其他内容] [原创]我们聊聊继承吧,从继承的角度出发再来聊聊多态

2019-10-15 21:55 1068

[系统底层] [其他内容] [原创]我们聊聊继承吧,从继承的角度出发再来聊聊多态

2019-10-15 21:55
1068

我们先通过一段代码来理解继承的底层实现。
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函数:

......
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 :

......
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继续执行

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的内存结构如下图:


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

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

public:
    CBase cBase;
	int nNumberChild;
};

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

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

------------------->分割线

再来聊聊多态,上代码:

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);
}

先来看看输出:


是不是意料之中的结果?

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

......
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的构造函数:

......
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函数吧:

......
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++多态吧,理解它,并找到它》中我们没有说到的内容,为什么在虚构函数中,要对多态表重新赋值。


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

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


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


上文中的代码,在析构函数中使用虚函数,这是为什么?我们留在下一篇文章中吧,明天见





[公告]安全测试和项目外包请将项目需求发到看雪企服平台:https://qifu.kanxue.com

最后于 2019-10-16 01:25 被Hasic编辑 ,原因:
最新回复 (2)
killpy 2 2019-10-16 16:29
2
0
全文重点--》 需要注意的是,在上文中设置两次虚表都是cChild0 this指针的前四个字节
Hasic 2019-10-16 21:32
3
0
killpy 全文重点--》 需要注意的是,在上文中设置两次虚表都是cChild0 this指针的前四个字节
谢谢您的指点
游客
登录 | 注册 方可回帖
返回