C++ — 多态

多态的概念

通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。

多态定义的构成条件

  1. 基类中必须存在虚函数,派生类必须对基类中的虚函数进行重写
  2. 必须通过基类的指针或者引用来调用虚函数
    虚函数:被virtual修饰的类成员函数称为虚函数。
    虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。
  3. 派生类虚函数virtual可加可不加
  4. 派生类虚函数的访问权限可以与基类虚函数的访问权限不同,基类中的虚函数的访问权限必须是public
class A
{
public:
	virtual void Test()	//虚函数
	{
		cout << "test" << endl;
	}
	int _a;
};
class B : public A
{
public:
	virtual void Test()
	{
		cout << "test233" << endl;
	}
	int _b;
};
void test(A& a)
{
	a.Test();	//传递不同的对象,调用不同对象中的函数
}

虚函数重写的两个例外

  1. 协变(基类与派生类虚函数返回值类型不同)
    派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。
  2. 析构函数的重写(基类与派生类析构函数的名字不同)
    如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。所以,基类的析构函数建议都写成虚函数。

抽象类

在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,**只有重写纯虚函数,派生类才能实例化出对象。**纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

class B
{
public:
	virtual void Test() = 0	//纯虚函数
	{
		cout << "test" << endl;
	}
	int _b;
};
class C : public B
{
public:
	virtual void Test()
	{
		cout << "test233" << endl;
	}
	int _c;
};

B* b = new C;
b->Test();

接口继承和实现继承

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用基类函数,继承的是函数的实现。
虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。

多态的原理

虚函数表

一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到 虚函数表中,虚函数表也简称虚表。

class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
private:
	int _b = 1;
};sizeof(Base),我们会发现除了_b成员,还多一个__vfptr放在对象的前面(注意有些平台可能会
放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针。

虚基表的构建方式

基类虚表的构建方式
将虚函数的地址按照其在类中声明的先后次序放置在虚表中
派生类虚表的构建方式

  1. 派生类先将基类虚表中内容拷贝一份放在派生类的虚表中
  2. 如果派生类重写了基类的某个虚函数,用派生类自己的虚函数地址替换虚表的相同偏移量位置的基类虚函数
  3. 对于派生类新增加虚函数,按照声明的次序放在虚表的末尾

派生类与基类使用的不是同一张虚表,即使派生类没有对基类的任何虚函数进行重写
在这里插入图片描述
在这里插入图片描述
因此,在虚表中已经提前将地址写好,指向父类时,调用父类虚函数表中的父类虚函数,指向子类时,调用子类虚函数表中的子类虚函数。
注:

  1. 同类型对象共用一份虚表
  2. 虚表中指针的顺序为声明的顺序
  3. 子类具有新的虚函数,放在继承的虚表后面

打印虚表

class B
{
public:
	virtual void Test() = 0	//纯虚函数
	{
		cout << "test" << endl;
	}
	int _b;
};
class C1 : public B
{
public:
	virtual void Test()
	{
		cout << "test233" << endl;
	}
	int _c1;
};
class C2 : public B
{
public:
	virtual void Test()
	{
		cout << "test466" << endl;
	}
	int _c2;
};

typedef void(*VFPTR) ();
void PrintfTable(VFPTR Table[])
{
	// 依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数
	cout << " 虚表地址>" << Table << endl;
	for (int i = 0; Table[i] != nullptr; ++i)
	{
		printf(" 第%d个虚函数地址 :0X%x,->", i, Table[i]);
		VFPTR f = Table[i];
		f();
	}
	cout << endl;
}
int main()
{
	//创建两个派生类
	C1 c1;
	C2 c2;
	// 思路:取出c1、c2、对象的头4bytes,就是虚表的指针,虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr
	// 1.先取c的地址,强转成一个int*的指针
	// 2.再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针
	// 3.再强转成VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。
	// 4.虚表指针传递给PrintfTable进行打印虚表
	// 5.需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最后面没有放nullptr,导致越界,这是编译器的问题。我们只需要点目录栏的-生成-清理解决方案,再编译就好了。
	VFPTR* vTableb = (VFPTR*)(*(int*)&c1);
	PrintfTable(vTableb);
	VFPTR* vTabled = (VFPTR*)(*(int*)&c2);
	PrintfTable(vTabled);
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值