C++多态(多态实现原理 ,多态继承总结)

1. 什么是多态

通俗来说,就是指多种形态。指不同对象在完成某个行为时产生的不同状态。
eg:火车购票系统
普通人买票是全价买票,学生买票是半价,军人买票是优先买票。都是,买票这一行为,但是不同人在完成这动作时系统给予的反应不同。

2.如何构成多态

1.被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写———重写(也叫覆盖)条件:(三同:函数名/参数/返回值虚函数)
2.必须通过基类的指针或者引用调用虚函数

class person
{
public:
	virtual void BuyTicket(int t = 3)
	{
		cout << "普通全票" << endl;
	}
};
class student :public person
{
public:
	virtual void BuyTicket(int t = 2)
	{
		 cout << t << endl;
		cout << "学生半票" << endl;
	}
private:
	size_t IDCard;

};
int main()
{
	student st;
	person* ptr = &st;
	ptr->BuyTicket();
	person& x = st;
	x.BuyTicket();
	return 0;
}
//输出:
3
学生半票
3
学生半票

为什么此时输出的t值是3?
因为上述提到,虚函数要构成重写,派生类中要有一个跟基类除定义外完全相同的函数,即(返回类型,函数名,参数列表相同),所以此时子类函数中t的缺省值在继承基类后变成了基类虚函数的缺省值也就是3。
注意:
在重写基类虚函数时,派生类的虚函数在不加virtual关键字,也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但这种写法不是很规范,不支持。

//这样用也可以构成多态,但不规范,不建议使用
 void BuyTicket(int t = 2)
	{
		 cout << t << endl;
		cout << "学生半票" << endl;
	}

3.虚函数重写例外

1.协变(基类与派生类虚函数返回类型不同)
派生类重写基类虚函数时,与基类虚函数返回值不同,即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用(返回值的关系必须是父子关系)
eg:
在这里插入图片描述
2.析构函数的重写(基类与派生类析构函数的名字不同)
如果是基类的析构函数为虚函数,此时派生类析构函数只管定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类的名字不同,看似违背了上述提到的构成多态的三同,起始并不是,因为编译器对析构函数的名称做了特殊的处理,编译后析构函数的名称统一处理成destruct,处理过后就符合构成多态重写的条件了,因为此时函数名相同。
在这里插入图片描述
建议:基类的析构函数写成虚构

4.为什么建议基类析构函数加virtual

如果不加virtual构成多态,下列delete指针就会有问题。
在这里插入图片描述
总结:
如果子类涉及到资源的清理,然后又出现基类的指针根据切片原则,指向子类的对象时,若基类的析构函数不加virtual此时就是编译时决议,根据指针的类型调用其析构函数,(也可以说是一种隐藏)此时并不会调用子类的析构函数,就有可能出现内存泄露,但若是加上virtual,那么便是多态调用,运行时决议,根据指针指向的地址进行调用,此时就不会出现内存泄露,所以说建议在基类的析构函数前加上virtual。

5.override与final

两个关键字用于检查用户是否重写

final:修饰虚函数,表示该虚函数不能再被重写
eg

virtual void BuyTicket()final{}

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

class person
{
public:
	virtual void BuyTicket(){}
};
class student :public person
{
public:
	virtual void BuyTicket()override{}
};

6.重载、覆盖(重写)、隐藏(重定义)对比

重载:两个函数在同一作用域并且函数名相同,但是除返回值外,两个函数重载形参的(个数、类型、顺序)必须有一个不同
重写(覆盖):两个函数分别在基类和派生类的作用域,函数名/参数/返回值都必须相同(协变除外),同时两个函数必须都是虚函数
重定义(隐藏):两个函数分别位于基类和派生类的作用域,函数名相同,两个基类和派生类的同名函数不够成重写就是重定义。

7.抽象类

在虚函数的后面写上0,则这个函数为纯虚函数,包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。 派生类如果仅仅只是继承,不对虚函数进行重写,也不能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现了接口的继承。
有点类似于override但override是先对象已经被定义出来了,检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
但是纯虚函数如果不重写虚函数的话连对象都无法定义
eg:

class A
{
public:
	virtual void Test() = 0;
};
class B :public A//可以定义对象
{
public:
	virtual void Test()
	{
		cout << "B:Test()" << endl;
	}
};
class C :public A//不能定义对象
{

};

上述的A与C都不能定义变量,但是B却可以,而此时A和C都是属于抽象类,抽象类是不能实例化出对象的。
多态继承和接口继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类函数继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要将函数定义成虚函数

8. 多态实现的原理

class Base
{
public:
	virtual void func1()
	{
		cout << "Base:Func1()" << endl;
	}

	virtual void func2()
	{
		cout << "Base:Func2()" << endl;
	}
	
private:
	int _b = 1;

};
class Derive:public Base
{
public:
	virtual void func1()
	{
		cout << "Derive:Func1()" << endl;
	}
	virtual void func2()
	{
		cout << "Derive:Func2()" << endl;
	}
};

Base a;cout<<sizeof(a)<<endl;发现编译器输出的是大小是8通过vs下的监视窗口发现
在这里插入图片描述
除了成员_b,还多一个_vfptr放在对象的前面,(有些平台可能会放在对象的后面,与平台有关),这个指针我们称为虚函数表指针(v代表virtual,f代表function)(顾名思义,对象中的虚函数都会虚函数表指针指向的虚函数表中),一个含有虚函数的类中都至少有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表

编译时决议:普通函数只会进符号表,都是编译时决议,即在编译阶段就决定了调用函数类型
虚函数要放在符号表,试运行时决议
可以说一个是动态的一个是静态的。

个人理解:虚函数表本质上是一个函数指针数组,同一个类型共用一个虚表,这个数组里面存储了原本存储了基类虚函数的地址,其早在编译阶段就生成了,但是每个类对象里面都有一个虚函数指针,在类对象的构造函数的初始化列表中对虚表的初始化就是使得对象的虚表指针指向虚表,而虚函数的重写在**语法层概念:**派生类对继承基类的虚函数进行了重写,原理层而言,拷贝父类的虚表进行了修改(也就是将虚函数的定义改变了),覆盖原本虚表中含有的函数指针,换成派生类自己虚函数指针,

Derive q;
	Base* ptr1 = &q;
	ptr1->func1();
	Base a;
	ptr1 = &a;
	ptr1->func1();

所以如上所示:指针压根就不关心自己是什么类型,如果是普通函数的话,就是编译时决议,其指针类型决定其调用的函数是什么,如果是多态调用,那就是编译时决议,编译器根据指针先根据指针找到对象中的虚函数指针进而找到虚函数表,再从虚函数表中找到函数的地址进行调用。

多态完成的基础是虚函数表完成了覆盖

9.验证只要是虚函数就会存储在虚表

eg

class Base
{
public:
	virtual void func1()
	{
		cout << "Base:Func1()" << endl;
	}

	virtual void func2()
	{
		cout << "Base:Func2()" << endl;
	}
	
private:
	int _b = 1;

};
class Derive:public Base
{
public:
	virtual void func1()
	{
		cout << "Derive:Func1()" << endl;
	}
	virtual void func2()
	{
		cout << "Derive:Func2()" << endl;
	}
	void func3()
	{
		cout << "Deirve:func3()" << endl;
	}
	virtual void func4()
	{
		cout << "Derive:Func4()" << endl;
	}
};

此时的Derive是继承Base但是func4器其虽然是虚函数,但是在Base中却没有这个虚函数,我采用固定方法是直接直接去内存值打印并调用。(因为我用的是vs编译器,知道在vs下面虚函数表的结尾是空指针,但在g++就不仅一定是这样的)

typedef void(*V_FUnc)();
void PrintVFtable(V_FUnc* a)
{
	printf("_vfptr:%p\n", a);
	for (size_t i = 0; a[i] != nullptr; ++i)
	{
		printf("[%d]:%p->", i, a[i]);
		V_FUnc f = a[i];
		f();
	}
}
int main()
{

	Derive q;

	PrintVFtable((V_FUnc*)(*((int*)&q)));
	//
	return 0;
}

在这里插入图片描述
其实也可以再写一个类,使直接继承Derive(但此时Derive就不能继承Base),然后通过定义新的派生类对象,观察新派生类对象的监视窗口即可。

10.虚函数表指针存储的位置

在这里插入图片描述
通过函数可以发现虚函数指针存储在常量区、代码段。

11.区分动态绑定与静态绑定

1.静态绑定又称为前期绑定(早绑定)在程序编译期间确定了程序的行为,也称为静态多态比如:函数重载
2.动态绑定又称为后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数也称为动态多态
在这里插入图片描述
如图所示:第一个函数调用时静态调用,在反汇编中是直接call函数地址,但是第二个函数是函数的多态调用,可以明显看到,是通过寄存器操作,找到虚函数表,再在虚函数表中找到对应的函数。

12.多继承中的虚函数表

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;

};
int main()
{
	Derive d;
	V_FUnc* vTableb1 = (V_FUnc*)(*(int*)&d);
	PrintVFtable(vTableb1);
	V_FUnc* vTableb2 = (V_FUnc*)(*(int*)((char*)&d + sizeof(Base1)));
	PrintVFtable(vTableb2);

	return 0;
}
 

在这里插入图片描述
根据结果可以看出,多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中

在这里插入图片描述
但是可以发现一个很奇怪的东西,虽然调用的都是派生类中的第一个函数输出Derive::func1(),但是可以看到输出的函数地址不一样。
在这里插入图片描述
为什么Base1的func1()只需要jump一次,就可以直接调用函数了,但是Base2中的func1()需要jump很多次?

本质上还是因为他们储存的位置不同,因为无论是Base1还是Base2调用函数,此时调用的都是Derive中的func1,所以传给this指针的是需要传递Derive的地址,因为Base1的地址和Derive的地址是相同的,但Base2的地址发生切片,需要修正,所以中途需要修正指针,利用寄存器将此时的this指针往前移八位,为什么是八位?
因为在Base2前面还有Base1的虚函数指针和一个int类型的变量,所以刚好是8位。

13.继承与多态常见面试问题

1.inline函数可以是虚函数吗?
可以,但是编译器会自动忽略inline的属性,因为内联函数是没有地址的,但是多态调用会将函数的地址放在虚函数表中
2.静态成员可以是虚函数吗?
不可以,因为静态成员没有this指针,使用类型::成员函数的调用方法无法访问虚函数表,所以静态成员无法放进虚函数表
3.构造函数可以是虚函数吗?
不能,虚表指针是在初始化列表中初始化的,而调用虚函数需要通过虚函数表找到函数的地址,但是现在连虚函数指针都没有初始化,所以构造函数不能是虚函数
4.对象访问普通函数快还是访问虚函数快?
如果是普通对象,是一样快的,但如果是指针对象或者是引用对象,则是普通函数更快,因为构成多态,要到虚表中去查找,等于走了两步
5.虚表是在什么阶段生成的,存在哪?
虚函数表是在编译阶段生成的,初始化是在构造函数的初始化列表,使得对象的虚表指针指向虚表,一般情况下是存在代码段(常量区)

14.为什么对象不能构成多态

Base b = d;
Base*ptr = &d;
ptr = &b;
ptr->func1();

对象切片的时候,子类只会拷贝成员给父类对象,不会拷贝虚表指针,如果拷贝虚表指针的话,上述情况就混乱了,此时到底是调用父类的函数还是子类的函数,而如果无法拷贝虚表指针的话就无法实现多态,所以对象无法实现多态,这是从为什么c++中对象不支持多态调用。现在c++的规则就是多态调用只能是指针或者引用。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

每天少点debug

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

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

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

打赏作者

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

抵扣说明:

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

余额充值