动态绑定,多态(带你从本质了解多态)

在上一章节中,我们讲述了虚函数和虚函数表,我们知道了不管在类中有多少个虚函数,都不会使类的大小扩大,在this指针中,只会多出一个虚函数表的地址,是this指针的第一个内容,在虚函数表中,函数是根据虚函数定义的顺序排列的,在这一章节中,我们将通过深入解析虚函数表,从而从本质上理解多态。

一.深入探索虚函数表

我们知道在虚函数表中存储的是该函数的地址,那么我们该如何验证?
我们可以通过函数指针的方式来调用存储在虚函数表中的函数:

#include "stdafx.h"

class Base{
public:
	int a;
	int b;
	Base(){
		a = 1;
		b = 2;
	}
	void Function_1(){
		printf("Base:Function_1...\n");
	}
	virtual void Function_2(){
		printf("Base:Function_2...\n");
	}
	virtual void Function_3(){
		printf("Base:Function_3...\n");
	}
};

int main(int argc, char* argv[])
{
	typedef void (*Function)(void);
	Base b;
	int* p;
	p = (int*)&b;
	int* function;
	function = (int*)(*p);
	Function pFn;
	for(int i=0;i<2;i++){
		pFn = (Function)*(function+i);
		pFn();
	}
	return 0;
}

这里我们通过函数指针来调用虚函数表中的地址,发现虚函数表中存的确实是虚函数的地址,且顺序是按照定义虚函数的顺序排列的。

1.单继承无函数覆盖下的虚函数表

我们知道在虚函数表中存的只有虚函数的地址,所以我们在写代码的时候不再写构造函数和析构函数,我们只写虚函数

#include "stdafx.h"

class Base{
public:
	int a;
	int b;
	virtual void Function_1(){
		printf("Base:Function_1...\n");
	}
	virtual void Function_2(){
		printf("Base:Function_2...\n");
	}
};

class Sub1:public Base{
public:
	int c;
		virtual void Sub_1(){
		printf("Sub1:Sub_1...\n");
	}
	virtual void Sub_2(){
		printf("Sub2:Sub_2...\n");
	}
};

int main(int argc, char* argv[])
{
	typedef void (*Function)(void);
	Sub1 b;
	int* p;
	p = (int*)&b;
	int* function;
	function = (int*)(*p);
	Function pFn;
	for(int i=0;i<4;i++){
		pFn = (Function)*(function+i);
		pFn();
	}
	return 0;
}

我们看看程序输出窗口:
单继承无函数覆盖
我们观察代码就能知道,我们通过函数指针调用函数的时候,是根据虚函数表的顺序调用的,所以我们得出结论:
当继承父类的时候,父类的虚函数会在派生类虚函数的上面,且它们的顺序都为定义虚函数的顺序。

2.单继承有函数覆盖下的函数虚表
#include "stdafx.h"

class Base{
public:
	int a;
	int b;
	virtual void Function_1(){
		printf("Base:Function_1...\n");
	}
	virtual void Function_2(){
		printf("Base:Function_2...\n");
	}
};

class Sub1:public Base{
public:
	int c;
	virtual void Function_1(){
		printf("Sub1:Function_1...\n");
	}
	virtual void Sub_1(){
		printf("Sub1:Sub_1...\n");
	}
	virtual void Sub_2(){
		printf("Sub2:Sub_2...\n");
	}
};

int main(int argc, char* argv[])
{
	typedef void (*Function)(void);
	Sub1 b;
	int* p;
	p = (int*)&b;
	int* function;
	function = (int*)(*p);
	Function pFn;
	for(int i=0;i<5;i++){
		pFn = (Function)*(function+i);
		pFn();
	}
	return 0;
}

这里注意一个细节,在通过函数指针调用函数的时候,我写了一个for循坏,并且次数为5,但是在程序运行的时候,它弹出来一个窗口告诉我该地址不可访问,则说明虚函数表里的函数肯定比五个少。
我们来看看程序输出窗口:
单继承有函数覆盖
这里打印出的函数顺序,实际上就是虚函数表里的函数顺序。
这时候,我们应该记得在课堂上,“铁男“说过一句话:”覆盖的是哪个,就在那个表里“。这里我解释一下:子类Function_1覆盖的是父类的虚函数,那么这个函数就出现在”父类的虚函数表“里(注意这里我们只是形象地称为父类的虚函数表,实际上这里只有一张虚函表哦),但是函数的具体功能是我们后定义的那个函数的功能。

3.多继承无函数覆盖下的虚函数表

看完了单继承,我们来看看多继承的虚函数表:

#include "stdafx.h"

class Base1{
public:
	int a;
	int b;
	virtual void Base1_1(){
		printf("Base1:Function_1...\n");
	}
	virtual void Base1_2(){
		printf("Base1:Function_2...\n");
	}
};

class Base2{
public:
	int c;
	int d;
	virtual void Base2_1(){
		printf("Base2:Function_1...\n");
	}
	virtual void Base2_2(){
		printf("Base2:Function_2...\n");
	}
};


class Sub1:public Base1,Base2{
public:
	int e;
	virtual void Sub_1(){
		printf("Sub1:Sub_1...\n");
	}
	virtual void Sub_2(){
		printf("Sub1:Sub_2...\n");
	}
};

int main(int argc, char* argv[])
{
	typedef void (*Function)(void);
	Sub1 b;
	int* p;
	p = (int*)&b;
	int* function;
	function = (int*)(*p);
	Function pFn;
	for(int i=0;i<6;i++){
		pFn = (Function)*(function+i);
		pFn();
	}
	return 0;
}

根据我们上面的讲解,应该在虚函数表中有6个函数地址,但是程序在运行的时候照样提醒我:该地址不允许访问,说明在虚函数表中,不足6个函数。
我们看看程序输出窗口:
程序输出窗口1

那么到底哪里出了问题?父类Base2中的虚函数去哪了?
我们先来看一下Sub的大小:

#include "stdafx.h"

class Base1{
public:
	int a;
	int b;
	virtual void Base1_1(){
		printf("Base1:Function_1...\n");
	}
	virtual void Base1_2(){
		printf("Base1:Function_2...\n");
	}
};

class Base2{
public:
	int c;
	int d;
	virtual void Base2_1(){
		printf("Base2:Function_1...\n");
	}
	virtual void Base2_2(){
		printf("Base2:Function_2...\n");
	}
};


class Sub1:public Base1,Base2{
public:
	int e;
	virtual void Sub_1(){
		printf("Sub1:Sub_1...\n");
	}
	virtual void Sub_2(){
		printf("Sub1:Sub_2...\n");
	}
};

int main(int argc, char* argv[])
{
	printf("%d",sizeof(Sub1));
	
	return 0;
}

我们可以看到程序输出窗口输出了28,我们来看看Sub的成员:继承了Base1的a和b,继承了Base2的c和d,自己的成员e,还有一张虚表,应该一共是24,可是它为什么输出了28?
其实通过课堂上老师的讲解我们已经知道:多重继承函数时会有多张虚表,而Base2的虚表就存在于this指针的第二个成员,他是Base2的虚表。

二.前期绑定和后期绑定

我们知道当程序调用函数的时候,有两种调用方式,一种是直接调用函数的地址,这种地址在程序编译的时候就已经写死了,另一种是通过一个地址,间接调用函数。
这里介绍一个名词:绑定,将函数与地址链接在一起的过程,叫做绑定。
直接调用函数的方式,在编译时就已将函数与地址绑定,我们称为(前期)编译期绑定
间接调用函数的方式,在运行的时候才进行绑定,我们称这种方式为(运行期)动态绑定或者晚绑定
注意:
只有virtual函数是动态绑定

三.多态

了解了前面的过程,多态的概念这里一句话就明白了:动态绑定还有另一个名字:多态。
这里给出多态的书面定义:
C++中的多态分为静态多态和动态多态。静态多态是函数重载,在编译阶段就饿能够确定调用哪个函数。动态多态是由继承产生的,指同一个属性或行为在基类和各派生类中具有不同的语义,不同的对象根据所接受的消息做出不同的响应,这种现象称为多态。
多态的实现需要满足三个条件:
(1)基类中声明虚函数
(2)派生类重写基类的虚函数
(3)将基类指针指向派生类对象,通过基类指针访问虚函数
我们来看看多态的具体实现,看看多态到底是什么:

#include "stdafx.h"

class Base{
public:
	int x;
	Base(){
		x=100;
	}
	virtual void Base_1(){
		printf("Base:Function_1...\n");
	}
	virtual void Base_2(){
		printf("Base:Function_2...\n");
	}
};

class Sub:public Base{
public:
	int e;
	Sub(){
		x=200;
	}
	virtual void Base_1(){
		printf("Sub1:Sub_1...\n");
	}
};

void Test(Base* p){
	int n = p->x;
	printf("%d\n",n);
	p->Base_1();
	p->Base_2();
}

int main(int argc, char* argv[])
{
	Base b;
	Base* p = &b;
	Test(p);
	return 0;
}

首先我们定义了一个基类对象,并且通过基类指针去访问函数,我们来看看程序输出框:
多态实现
我们定义了基类对象,并且通过基类指针去访问函数,当然是没有任何问题的。
接下来我们看看多态的实现:
创建一个基类,再创建一个派生类,将基类函数覆盖,通过基类指针访问派生类:

#include "stdafx.h"

class Base{
public:
	int x;
	Base(){
		x=100;
	}
	virtual void Base_1(){
		printf("Base:Function_1...\n");
	}
	virtual void Base_2(){
		printf("Base:Function_2...\n");
	}
};

class Sub:public Base{
public:
	int e;
	Sub(){
		x=200;
	}
	virtual void Base_1(){
		printf("Sub1:Sub_1...\n");
	}
};

void Test(Base* p){
	int n = p->x;
	printf("%d\n",n);
	p->Base_1();
	p->Base_2();
}

int main(int argc, char* argv[])
{
	Sub b;
	Base* p = &b;
	Test(p);
	return 0;
}

我们来看看程序输出窗口:
多态实现
我们可以看到,基类中属性的值也被改变,并且基类中函数也被覆盖,这就是我们所说的多态,同一个属性或行为在基类和各派生类中具有不同的语义,不同的对象根据所接受的消息做出不同的响应。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Shad0w-2023

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值