C++多态

目录

一、多态的概念

二、多态的定义和实现

        2.1多态的构成条件

2.2虚函数

2.3虚函数的重写

2.4虚函数重写的两个例外

1.协变(基类与派生类的返回值可以不同)

2.析构函数的重写(基类与派生类析构函数的名字不同)

2.5 C++11 override和final

(1)final:修饰虚函数,表示该虚函数不能进行重写

(2)override:检查派生类是否重写了某个虚函数,如果没有重写则会编译报错

2.6重载、覆盖(重写)、隐藏(重定义)的对比

三、抽象类

3.1概念

3.2接口继承和实现继承

四、多态的原理

4.1虚函数表

4.2多态的原理

五、单继承和多继承关系中的虚函数表

5.1单继承中的虚函数表

5.2多继承中的虚函数表


一、多态的概念

        多态的概念:通俗来讲,就是多种形态,具体来说就是不同的对象去完成同一种行为的时候,会产生出不同的状态。

        举个栗子:比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人买票时是优先买票。
        再举个栗子: 最近为了争夺在线支付市场,支付宝年底经常会做诱人的扫红包-支付-给奖励金的活动。那么大家想想为什么有人扫的红包又大又新鲜8块、10块...,而有人扫的红包都是1毛,5毛....。其实这背后也是一个多态行为。支付宝首先会分析你的账户数据,比如你是新用户、比如
你没有经常支付宝支付等等,那么你需要被鼓励使用支付宝,那么就你扫码金额 =
random()%99;比如你经常使用支付宝支付或者支付宝账户中常年没钱,那么就不需要太鼓励你
去使用支付宝,那么就你扫码金额 = random()%1;总结一下:同样是扫码动作,不同的用户扫
得到的不一样的红包,这也是一种多态行为。ps:支付宝红包问题纯属瞎编,大家仅供娱乐。

二、多态的定义和实现

        2.1多态的构成条件

        多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了
Person。Person对象买票全价,Student对象买票半价。

那么在继承中要构成多态还有两个条件:
(1)必须通过父类的指针或者引用调用虚函数(我们在后面原理部分会讲讲这是为什么)
(2)被调用的函数必须是虚函数(虚函数存在于虚表中,才完成了多态的实现),且派生类必须对基类的虚函数进行重写

2.2虚函数

        虚函数:即被virtual修饰的类的成员函数

2.3虚函数的重写

        虚函数的重写(覆盖):派生类中有一个和基类完全相同的虚函数(即函数名、返回值类型、形参列表完全相同),那么称子类的虚函数重写了父类的虚函数。

        

        可以看出来,在用父类的指针/引用调用的时候,他们产生了不同的行为效果。所以我们说这就完成了一个虚函数的重写。

2.4虚函数重写的两个例外

        前面我们说虚函数重写需要满足这样的要求:

(1)父类的指针或引用去调用这个虚函数

(2)虚函数的函数名、形参列表、返回值要相同

但是凡事都有例外,虚函数的重写也是如此,他就有两个例外:

1.协变(基类与派生类的返回值可以不同)

        派生类重写基类虚函数的的时候,与基类虚函数的返回值不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用。

注意:这里父类和子类返回值类型必须是一对父子类(可以不是当前父子类)

注意:在父子关系中,只要父类声明了虚函数,子类可以不加virtual关键字也构成虚函数

2.析构函数的重写(基类与派生类析构函数的名字不同)

        如果基类的析构函数为虚函数,则此时派生类析构函数只要定义,无论是否加virtual关键字,
都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,
看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处
,编译后析构函数的名称统一处理成destructor。(这样处理是为了解决内存泄漏的问题)

        看这里就是析构函数形成了多态!但是为什么P2指向的Student对象调用了两个析构函数呢?因为Student是继承于父类Person的,我们必须要显式调用父类的析构函数才能完成对父类资源的释放,但是如果先掉父类再掉子类的析构函数,则会产生一些问题:子类的析构函数可能在函数体中用到了父类的成员,而此时父类已经销毁掉了。所以编译器为了防止我们乱调用析构函数,做了一个特殊处理:只需要写子类的析构函数,在运行的时候会自动在后面加上父类的析构函数。

2.5 C++11 override和final

        从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下可以因为疏忽,导致函数的名字字母顺序写反而无法构成重写等,而这种错误在编译阶段是不会报错的,只有当我们运行的时候,没有得到预期的结果才会知道,这会让我们费时费力的去寻找错误原因。因此:C++11中提供了两个关键字override和final,可以帮助用户检查是否进行了重写

(1)final:修饰虚函数,表示该虚函数不能进行重写

(2)override:检查派生类是否重写了某个虚函数,如果没有重写则会编译报错

2.6重载、覆盖(重写)、隐藏(重定义)的对比

三、抽象类

3.1概念

        在虚函数后面写上=0,则表示这个虚函数为纯虚函数。而包含纯虚函数的类叫做抽象类(也叫做接口类),抽象类不能实例化出对象。派生类继承后也不能直接实例化出对象,必须要把继承的到的每一个纯虚函数都完成重写,派生类才能实例化出对象。所以说纯虚函数规范了派生类必须要进行重写,更加体现了接口继承。

3.2接口继承和实现继承

        普通函数的继承是一种实现继承,也可以说是完全继承,父类的函数声明和定义全部被子类继承。而虚函数的继承是一种接口继承,也就是说子类继承父类的函数声明部分,但是定义由自己类来实现。但是这样会有一个坑,有可能子类在声明的时候,参数的缺省值和父类不同,大家都会以为是用子类的缺省值来实现,但正是因为他是接口继承,所以实际上还是用的父类的缺省值。

四、多态的原理

4.1虚函数表

        通过观察测试,我们会发现b对象并不是4字节,而是8个字节!除了_b成员,还多了一个_vfptr放在对象的前面(有些编译器会把这个放在对象的后面,这个并不影响),而对象中的这个指针就是虚函数表指针,他是一个函数指针数组指针,指向了虚函数数组的地址。

// 针对上面的代码我们做出以下改造
// 1.我们增加一个派生类Derive去继承Base
// 2.Derive中重写Func1
// 3.Base再增加一个虚函数Func2和一个普通函数Func3
class Base
{
public:
	virtual void Func1()
	{
		cout << "Base::Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "Base::Func2()" << endl;
	}
	void Func3()
	{
		cout << "Base::Func3()" << endl;
	}
private:
	int _b = 1;
};
class Derive : public Base
{
public:
	virtual void Func1()
	{
		cout << "Derive::Func1()" << endl;
	}
private:
	int _d = 2;
};
int main()
{
	Base b;
	Derive d;
	return 0;
}

通过观察和测试,我们发现了以下几点问题:
1. 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚
表指针也就是存在部分的另一部分是自己的成员。
2. 基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表
中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数
的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
3. 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函
数,所以不会放进虚表。
4. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。
5. 总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生
类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数地址 c.派生类自己
新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。

6. 这里还有一个童鞋们很容易混淆的问题:虚函数存在哪的?虚表存在哪的? 答:虚函数存在
虚表,虚表存在对象中。注意上面的回答的错的。但是很多童鞋都是这样深以为然的。注意
虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是
他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。那么虚表存在哪的
呢?实际我们去验证一下会发现vs下是存在代码段的

4.2多态的原理

        上面分析了这么多,原理到底是什么呢?

        其实不同对象(父类/子类)去调用虚函数的时候,就是首先通过第一个虚表指针,找到本类的虚函数数组,然后从这个数组中寻找到所需函数的地址,再通过这个地址去调用,从而实现了不同对象调用同一个函数实现了不同的效果。编译器也就是傻傻的跟着虚表去调用函数罢了

        

五、单继承和多继承关系中的虚函数表

5.1单继承中的虚函数表

namespace hmy
{
	class Base
	{
	public:
		virtual void func1() { cout << "Base::func1()" << endl; }
		virtual void func2() { cout << "Base::func2()" << endl; }
	private:
		int _a;
	};

	class Derive :public Base
	{
		virtual void func1() { cout << "Derive::func1()" << endl; }
		virtual void func3() { cout << "Derive::func3()" << endl; }
		virtual void func4() { cout << "Derive::func4()" << endl; }
	private:
		int _b;
	};

	//将void(*)()函数指针改名为Vfptr
	typedef void(* Vfptr)();
	//传入虚函数表的地址
	void PrintTable(Vfptr vTable[])
	{
		cout << "虚表地址" << vTable << endl;
		for (int i=0;vTable[i]!=nullptr;i++)
		{
			printf("第%d个虚函数地址是%p",i,vTable[i]);
			
			//调用该函数
			Vfptr f = vTable[i];
			f();
		}
	}
}

// 思路:取出b、d对象的头4bytes,就是虚表的指针,前面我们说了虚函数表本质是一个存虚函数
指针的指针数组,这个数组最后面放了一个nullptr
// 1.先取b的地址,强转成一个int*的指针
// 2.再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针
// 3.再强转成VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。
// 4.虚表指针传递给PrintVTable进行打印虚表
// 5.需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最
后面没有放nullptr,导致越界,这是编译器的问题。我们只需要点目录栏的-生成-清理解决方案,再编译就好了。

        可以看到,Derive在重写了func1()函数后,虚表指针的地址就变化了,而func2()没有重写,和父类Base中的地址一模一样。我们可以简单的理解成,子类会先拷贝父类地址,如果有重写,就会在虚表中进行地址的覆盖,如果没有重写,就会保留父亲的函数地址        

5.2多继承中的虚函数表

class Base1 {
public:
virtual void func1() {cout << "Base1::func1" << endl;}
virtual void func2() {cout << "Base1::func2" << endl;}
private:
int b1;
};
class Base2 {
public:
virtual void func1() {cout << "Base2::func1" << endl;}
virtual void func2() {cout << "Base2::func2" << endl;}
private:
int b2;
};
class Derive : public Base1, public Base2 {
public:
virtual void func1() {cout << "Derive::func1" << endl;}
virtual void func3() {cout << "Derive::func3" << endl;}
private:
int d1;
};
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
cout << " 虚表地址>" << vTable << endl;
for (int i = 0; vTable[i] != nullptr; ++i)
{
printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
VFPTR f = vTable[i];
f();
}
cout << endl;
}
int main()
{
Derive d;
VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
PrintVTable(vTableb1);
VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d+sizeof(Base1)));
PrintVTable(vTableb2);
return 0;
}

观察下图可以看出:多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值