【C++】多态

这篇具有很好参考价值的文章主要介绍了【C++】多态。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

多态的概念

多态,即多种形态,具体指完成某个行为,不同的对象完成时,产生不同的状态。

比如买票,成人买票全价,儿童买票半价,军人买票优先,不同的对象调用同一个买票相关的函数,产生的行为是不同的。

构成多态的两个条件:

1. 必须通过基类的指针或者引用调用虚函数
2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行 重写
虚函数:即被 virtual 修饰的类成员函数称为虚函数
虚函数的重写 ( 覆盖 ) 派生类中有一个跟基类完全相同的虚函数 ( 即派生类虚函数与基类虚函数的 返回值类型、函数名字、参数列表完全相同 ) ,称子类的虚函数重写了基类的虚函数
【C++】多态,c++,c++,开发语言
【C++】多态,c++,c++,开发语言
【C++】多态,c++,c++,开发语言
按照我们继承的相关知识,Func传参是用父类对象的引用接收的,实参传递父类对象正常接收,实参传子类对象进行切片处理,当父子类满足多态条件时,会分别调用父子类的函数;当不满足父子类多态条件时(上述两个条件缺一不可),那么只看当前接收的类型,由于当前接收类型为父类,所以输出结构均为:买票-全价。
一句话: 多态调用看的是指向的对象,普通对象只看当前类型

虚函数的重写

在重写基类虚函数时,派生类的虚函数在不加 virtual 关键字时,虽然也可以构成重写 ( 因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性), 但是该种写法不是很规范,不建议这样使用。
1. 协变 ( 基类与派生类虚函数返回值类型不同 )
派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。
2. 析构函数的重写 ( 基类与派生类析构函数的名字不同 )
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加 virtual 关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor(同名保证符合多态重写条件)
为什么一定要处理成重写呢?
因为如果子类成员变量中涉及空间申请,那么就需要在析构函数中释放,如果不重写,父子类析构函数名均为destructor,调用哪个析构函数取决于动态申请时涉及的那个指针类型,如果类型为父类对象的指针,那么不管new时new的是子类对象还是父类对象,都会被看作父类指针,仅仅调用父类析构,这样就会造成内存泄漏。

c++11 override&final

1. final :修饰虚函数,表示该虚函数不能再被重写
class Car
{
public:
 virtual void Drive() final {}
};
class Benz :public Car
{
public:
 virtual void Drive() {cout << "Benz-舒适" << endl;}
};
final也可修饰类使其成为最终类,这样这个类就不会被继承。
2. override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错
class Car{
public:
 virtual void Drive(){}
};
class Benz :public Car {
public:
 virtual void Drive() override {cout << "Benz-舒适" << endl;}
};
重定义(隐藏)与重写(覆盖)都要求两个函数分别在基类和派生类的作用域,再加上三同(函数名&返回值&参数) (重写中协变除外) 区别就在于,重写要求两个函数必须是虚函数。
也就是说,两个基类和派生类的同名函数,不构成重写就是重定义(重写是重定义的子集)。

抽象类

在虚函数的后面写上 =0 ,则这个函数为纯虚函数。 包含纯虚函数的类叫做抽象类(也叫接口 类),抽象类不能实例化出对象 。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
class Car
{
public:
	virtual void Drive() = 0;
};
class Benz :public Car
{
public:
	virtual void Drive()
	{
		cout << "Benz-舒适" << endl;
	}
};
class BMW :public Car
{
public:
	virtual void Drive()
	{
		cout << "BMW-操控" << endl;
	}
};
void Test()
{
	Car* pBenz = new Benz;
	pBenz->Drive();
	Car* pBMW = new BMW;
	pBMW->Drive();
}

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

多态基本原理

【C++】多态,c++,c++,开发语言
首先我们观察一下Person对象的大小,在64位机器下算出的大小是16(32位机器下是8),为什么这里Person里只有一个整型加一个虚函数得到的结果是16呢,那么这里的虚函数不是函数吗,按照以往处理函数的方式,在计算大小时,函数是不计入的,也就是说结果应该是4,ok让我们继续往下看
【C++】多态,c++,c++,开发语言
通过调试我们发现,ps这个父类对象中不仅包含_a变量,还包含一个名为 _vfptr的指针,那么二者相加再加上内存对齐规则就明白为什么大小为16了。
对象中的这个指针我们叫做 虚函数表指针(v代表virtual,f代表function)
一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。
【C++】多态,c++,c++,开发语言
这里也就大致知道了为什么父类指针或引用接收父类对象或者子类对象都可以区分开的原因了:即使子类进行切片后得到的结构跟父类相同,但是 二者的虚函数表不同,因此运行时根据虚函数表找到的函数指针也就不同,因此呈现出多态。
总结一下派生类的虚表生成: a. 先将基类中的虚表内容拷贝一份到派生类虚表中 b. 如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c. 派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后
【C++】多态,c++,c++,开发语言
图中func1未重写因此父子类对应的虚表中func1的地址相同;其他虚函数重写了,因此进行了覆盖,地址发生变化。
【C++】多态,c++,c++,开发语言
派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后,这条总结虽然在内存中可以看到确实在最后存在一个地址,但是否是新增的func3的地址,我们需要验证:
首先我们捋一下验证思路:要想验证这个内存中看到的地址是否是func3的地址,首先需要得到虚表的地址(因为func3作为虚函数,指向它的函数指针一定会存在虚表中,且根据总结一定是最后一个地址),虚表的地址是通过vfptr这个指针得到的,而vfptr又是ps对象的指针变量,因此我们可以&ps得到一个指向ps对象的指针(相当与person*),但是我们只需要该指针指向vfptr这部分,由于vfptr是一个指针,其所占大小在32位机器下是4字节,64位机器下是8字节,因此我们需要从ps对象中截取出所需要的字节,为了使得该方法适用于32或者64位机器,我们可以将&ps强转成一个int**的二级指针,因为对一个int**类型的指针进行解引用得到的依然是一个指针,既然是指针那么就满足其在32位机器下是4字节,64位机器下是8字节,强转之后进行解引用就得到了vfptr这个函数指针对应的地址,那么接下来就只需写一个打印函数,通过将这个函数指针的地址传递给打印函数(由于这里拿到的vfptr的地址是int*,所以传参时需要强转成函数指针),就可以打印出指向的虚表内容了,再将打印内容跟内存作比较就可以得出结论。
【C++】多态,c++,c++,开发语言
通过验证我们发现结论是正确的。         
那么虚表存在哪里呢?
ok它存在 代码段(常量区)中,这里就不进行验证了,验证方法就是通过创建各种区域(栈、堆、常量区、静态区)的变量,通过将其地址输出打印,对比确定它的实际位置。

动态绑定与静态绑定

1. 静态绑定又称为前期绑定 ( 早绑定 ) 在程序编译期间确定了程序的行为 也称为静态多态
比如:函数重载
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;
};

【C++】多态,c++,c++,开发语言

我们通过监视窗口得到Derive对象d中含有两张虚表,一个存Base1对象b1中的虚函数,一个存Base2对象b2中的虚函数,如果重写则进行相应的覆盖。

但是d中的func3会存在哪里呢,根据上面的总结,子类新增的虚函数存在虚表的结尾,那么是存在哪个虚表的结尾呢,我们依然通过上面的方法检验一下

【C++】多态,c++,c++,开发语言

【C++】多态,c++,c++,开发语言

打印出来我们发现func3写在了第一个虚表之后,同时我们还从打印结果发现虚表中调用的func1也就是下标位0的位置的虚函数它显示的地址不同,可是我们重写了func1就意味着应该调到了同一个func1函数,那为什么这里的地址不同呢?

【C++】多态,c++,c++,开发语言

我们利用指针切片得到指向两个虚表的指针,然后用指针直接调用func1函数调用的是同一个函数,我们转到反汇编看一下,首先调用第一个func1,我们发现反汇编中的call马上要执行func1函数,我们将eax寄存器的值添加到监视窗口发现它的值是第一个虚表中的func1的地址,按f11进去发现是一个jmp指令,它要跳转到func1的真实地址,调用func1.

【C++】多态,c++,c++,开发语言

【C++】多态,c++,c++,开发语言

我们继续观察第二个指针

【C++】多态,c++,c++,开发语言

我们发现此时的eax存的地址改成了第二张虚表中的func1的地址,显然这个地址跟第一张虚表的地址不同,我们按f11跳转进去

【C++】多态,c++,c++,开发语言

一直跳转,我们又会找到一个func1的地址

【C++】多态,c++,c++,开发语言

而这个地址跟第一张虚表的func1的地址是相同的,继续jmp就跟第一个指针寻找func1的过程相同了。

所以两个指针最终找到的func1函数是同一个!

反观整个过程,第二个指针无非多了一个跳转的过程,这个过程执行了一句(sub   ecx,8),这个ecx存的是this指针,这条语句意思是让this指针-8,执行这个操作后就跟第一个指针的寻址过程重合了。

仔细观察两个指针的指向位置不难发现,中间差距了一个Base1的大小,Base1中存了一个vfptr指针和一个int变量,因此其大小为8,因此让这里的this指针即第二个指针-8就是为了让这个指针指向第一个指针的位置,然后开始寻找func1的地址。

为什么我们第二个指针要先找到第一个指针指向的位置,再找func1呢?

因为我们要找的func1函数是属于Derive对象的,调用时需要传this指针,通过this指针进行调用,这里的this指针需要传递正确,需要指向对象,第一个指针指向的位置恰好就是Derive对象,因此不需要修改,而第二个指针作为this指针指向是错误的,他并没有指向Derive对象,因此需要先修正,再进行实际调用。文章来源地址https://www.toymoban.com/news/detail-820762.html

到了这里,关于【C++】多态的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【C++笔记】C++多态

    多态的概念: 在编程语言和类型论中,多态(英语:polymorphism)指为不同数据类型的实体提供统一的接口。多态类型(英语:polymorphic type)可以将自身所支持的操作套用到其它类型的值上。 计算机程序运行时,相同的消息可能会送给多个不同的类别之对象,而系统可依据对

    2024年02月08日
    浏览(16)
  • 16- C++多态-4 (C++)

    思考:在之前实现的英雄模型中,假如实现某个接口可以传入一个英雄,在该接口中可以对英雄的力量、敏捷和智力进行加强,请问该接口的参数该如何设计? 以上解决办法利用了C++中的多态,接下来让我们来了解一下 C++中的多态 。 多态:一个函数有多种形态。 多态的分

    2024年02月15日
    浏览(15)
  • [C++]:万字超详细讲解多态以及多态的实现原理(面试的必考的c++考点)

    文章目录 前言 一、多态的定义及实现 1.多态的构成条件 2.c++11的override和final 3.重载,重写,重定义的比较 4.抽象类 5.多态的原理 6.多继承中的虚函数表 7.动态绑定和静态绑定 总结 多态的概念: 多态的概念:通俗来说,就是多种形态, 具体点就是去完成某个行为,当不同的

    2023年04月22日
    浏览(30)
  • C++修炼之路之多态---多态的原理(虚函数表)

    目录 一:多态的原理  1.虚函数表  2.原理分析 3.对于虚表存在哪里的探讨 4.对于是不是所有的虚函数都要存进虚函数表的探讨 二:多继承中的虚函数表 三:常见的问答题  接下来的日子会顺顺利利,万事胜意,生活明朗-----------林辞忧  接上篇的多态的介绍后,接下来介绍

    2024年04月26日
    浏览(17)
  • C++中的多态你真的了解吗?多态原理全面具体讲解

    目录 1. 多态的概念 2. 多态的定义及实现 2.1 多态的构成条件 2.2 虚函数 2.3 虚函数的重写 2.4 C++11 override 和 final 2.5 重载、覆盖(重写)、隐藏(重定义)的对比 3. 抽象类 3.1 概念 4. 多态的原理 4.1 虚函数表 4.2多态的原理 4.3 动态绑定与静态绑定 5. 单继承和多继承关系中的虚函数表

    2024年02月04日
    浏览(17)
  • 【C++】一文带你吃透C++多态

    🍎 博客主页:🌙@披星戴月的贾维斯 🍎 欢迎关注:👍点赞🍃收藏🔥留言 🍇系列专栏:🌙 C/C++专栏 🌙那些看似波澜不惊的日复一日,一定会在某一天让你看见坚持的意义!-- 算法导论🌙 🍉一起加油,去追寻、去成为更好的自己! @TOC 提示:以下是本篇文章正文内容,

    2024年02月08日
    浏览(18)
  • C++多态

    多态是C++面向对象三大特性之一 静态多态:函数重载和运算符重载属于静态多态,复用函数名 动态多态:派生类和虚函数实现运行时多态 静态多态的函数地址早绑定—编译阶段确定函数地址 动态多态的函数地址晚绑定—运行阶段确定函数地址 有继承关系 子类重写父类的虚

    2024年02月22日
    浏览(9)
  • 【C++】多态及原理

    1.多态的概念 多态,顾名思义就是多种状态, 具体点就是去完成某种行为,但是通过不同的对象去完成某种行为都会产生不同的状态,这就是多态 比如买票这个行为。当 普通人 买票时,是全价买票; 学生 买票时,是半价买票; 军人 买票时是优 先买票。 这就是不同的对象

    2024年02月16日
    浏览(17)
  • 【C++进阶之路】多态篇

     多态,顾名思义,就是 一件事物具备不同的形态 ,是继承之后, 面向对象的第三大特性 ,可以这样说: 有了继承才有了类的多态,而类的多态是为了更好的实现继承。  多态的列车即将起航,不知你准备好了吗?   继承与多态相辅相成 。 举个例子:  我们都是人(

    2024年02月15日
    浏览(19)
  • C++,多态练习

    一、定义基类Animals,以及多个派生类,基类中至少包含虚函数perform() 二、用函数模型实现不同数据类型的交换功能 

    2024年02月11日
    浏览(13)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包