C++ 多态详解

文章目录

  • 1. 多态的概念
  • 2. 多态的定义及实现
    • 2.1 多态的构成条件
    • 2.2 虚函数
    • 2.3 虚函数的重写
      • 2.3.1 虚函数重写的两个例外
    • 2.4 C++11 override 和 final
    • 2.5 重载、覆盖(重写)、隐藏(重定义)的对比
  • 3. 多态的原理
    • 3.1 虚函数表
    • 3.2多态的原理
  • 4. 单继承和多继承关系的虚函数表
    • 4.1 单继承中的虚函数表
    • 4.2 多继承中的虚函数表

1. 多态的概念

多态是面向对象编程中的一个重要概念,通俗来说,多态就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态
举个例子:比如我们在12306买票,对于买票这个行为,成人买成人票就是全价,学生买学生票就是半价。
在这里插入图片描述

2. 多态的定义及实现

2.1 多态的构成条件

在继承中要构成多态还有两个条件

  1. 必须通过基类的指针或者引用调用虚函数
  2. 被调用的函数必须是虚函数且派生类必须对基类的虚函数进行重写
    在这里插入图片描述
    多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如上面图片中的例子:Student继承了Person。Person对象买票全价,Student对象买票半价。

代码如下:

class Person
{
public:virtual void buy_ticket(){cout << "全价" << endl;}
};
class Student : public Person
{public:virtual void buy_ticket(){cout << "半价" << endl;}};
void func(Person& p)
{p.buy_ticket();
}
void func(Person* p)
{p->buy_ticket();
}
int main()
{Person p;Student s;//基类的引用func(p);func(s);//基类的指针func(&p);func(&s);return 0;
}

2.2 虚函数

virtual修饰的类成员函数称为虚函数。

class Person
{
public://buy_ticket()就是虚函数,被virtual修饰的类成员函数virtual void buy_ticket(){cout << "全价" << endl;}
};

2.3 虚函数的重写

虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。
重写虚函数的目的是为了在派生类中提供一个特定于派生类的实现,从而实现特定的行为。重写后,当通过基类指针或引用调用虚函数时,如果指向或引用的是派生类对象,将会调用派生类中的虚函数,如果指向或引用的是基类对象,将会调用基类中的虚函数,这样就可以做到不同的派生类的对象在调用同一个函数时,能表现出不同的行为。
在这里插入图片描述

2.3.1 虚函数重写的两个例外

  1. 协变(基类与派生类虚函数返回值类型不同)
    派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。
    在这里插入图片描述
  2. 析构函数的重写(基类与派生类析构函数的名字不同)
    如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。
    这里我们可以间接验证一下:
    在这里插入图片描述
    因此在涉及到资源管理的时候,基类的析构函数最好加上virtual关键字修饰,否则可能在某些情况下,造成无法正确调用析构函数而造成内存泄漏。
    比如下面这种情况:
    在这里插入图片描述

2.4 C++11 override 和 final

  1. final:修饰虚函数,表示该虚函数不能再被重写
    在这里插入图片描述
  2. final:修饰类,表示该类不能被继承
    在这里插入图片描述
  3. override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错
    在这里插入图片描述

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

在这里插入图片描述

3. 多态的原理

3.1 虚函数表

我们先来看一道题:

#include<iostream>
using namespace std;
class Base
{
public:virtual void Func1(){cout << "Func1()" << endl;}
private:int _b = 1;
};int main()
{cout << sizeof(Base) << endl;return 0;
}

运行结果:(32位平台下是8,64位平台下位16。)
在这里插入图片描述
我们发现,Base类只有一个整型变量,就算考虑内存对齐,结果应该是4呀,这里为啥会输出8呢?
下面我们打开监视窗口来看下Base类对象的模型:
在这里插入图片描述
通过观察测试我们发现b对象是8bytes,除了_b成员,还多一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。

针对上面的代码我们做出以下改造
1.我们增加一个派生类Derive去继承Base
2.Derive中重写Func1
3.Base再增加一个虚函数Func2和一个普通函数Func3

#include<iostream>
using namespace std;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. 虚函数存在哪的?虚表存在哪的?
    答:虚函数存在虚表,虚表存在对象中。注意上面的回答的错的。注意虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。那么虚表存在哪的呢?
    在这里插入图片描述
    上面分析了这个半天了那么多态的原理到底是什么?下面我们来具体分析一下:

3.2多态的原理

现在我们再来看下之前写的买票的代码,Func函数传Person调用的Person::BuyTicket,传Student调用的是Student::BuyTicket:

#include<iostream>
using namespace std;
class Person 
{
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person 
{
public:virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
void Func(Person& p)
{p.BuyTicket();
}
int main()
{Person p;Func(p);Student s;Func(s);return 0;
}

在这里插入图片描述
上面我们分析出,当p指向谁就去谁的虚函数表中去取对应虚函数的地址,因此就实现出了不同对象完成同一行为时,展现出不同的形态。
而我们要达到多态,有两个条件,一个是虚函数覆盖,一个是对象的指针或引用调用虚函数。这是为什么呢?我们来反思一下:
在这里插入图片描述
我们分析出,赋值不会拷贝虚函数指针,因此要实现出不同对象完成同一行为时,展现出不同的形态,有两个条件,一个是虚函数覆盖,一个是对象的指针或引用调用虚函数。
下面我们在来分析一下多态调用和普通函数调用有什么区别呢?
在这里插入图片描述
通过上面的汇编代码分析,看出满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象的中取找的。不满足多态的函数调用时编译链接时确认好的

4. 单继承和多继承关系的虚函数表

在单继承和多继承关系中,下面我们去关注的是派生类对象的虚表模型,因为基类
的虚表模型前面我们已经看过了,没什么需要特别研究的。

4.1 单继承中的虚函数表

分析如下代码:

class Base 
{ 
public :virtual void func1() { cout<<"Base::func1" <<endl;}virtual void func2() {cout<<"Base::func2" <<endl;}
private :int a;
};
class Derive :public Base 
{ 
public :virtual void func1() {cout<<"Derive::func1" <<endl;}virtual void func3() {cout<<"Derive::func3" <<endl;}virtual void func4() {cout<<"Derive::func4" <<endl;}
private :int b;
};int main()
{Base b;Derive d;return 0;
}

分析如下:
在这里插入图片描述
我们通过监视窗口中我们发现看不见func3和func4。这里是编译器的监视窗口故意隐藏了这两个函数,也可以认为是他的一个小bug。而且通过内存窗口只能看清虚函数的个数,那么我们如何查看d的虚表呢?下面我们使用代码打印出虚表中的函数。

class Base
{
public:virtual void func1() { cout << "Base::func1" << endl; }virtual void func2() { cout << "Base::func2" << endl; }
private:int a;
};
class Derive :public Base
{
public:virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }virtual void func4() { cout << "Derive::func4" << endl; }
private:int b;
};
typedef void (*VFPTR)();
void PrintVFTable(VFPTR* vftptr)
{for (int i = 0; vftptr[i] != 0; ++i){printf("第%d个虚函数的地址:%p------>", i + 1, vftptr[i]);vftptr[i]();}printf("\n");
}
int main()
{Base b;Derive d;
//思路:取出b、d对象的头4bytes,就是虚表的指针,前面我们说了虚函数表本质是一个存虚函数指针的指针数组,
// 这个数组最后面放了一个nullptr
// 1.先取b的地址,强转成一个int*的指针
// 2.再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针
// 3.再强转成VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。
// 4.虚表指针传递给PrintVTable进行打印虚表VFPTR* vftptr = (VFPTR*)(*(int*)&b);PrintVFTable(vftptr);vftptr = (VFPTR*)(*(int*)&d);PrintVFTable(vftptr);return 0;
}

运行结果如下:
在这里插入图片描述
因此我们可以推断出派生类的虚函数表模型:
在这里插入图片描述

4.2 多继承中的虚函数表

分析如下代码:

#include <iostream>
using namespace std;class Base1 
{
public:virtual void func1() { cout << "Base1::func1" << endl; }virtual void func2() { cout << "Base1::func2" << endl; }
private:int b1 = 1;
};
class Base2 {
public:virtual void func1() { cout << "Base2::func1" << endl; }virtual void func2() { cout << "Base2::func2" << endl; }
private:int b2 = 2;
};
class Derive : public Base1, public Base2 
{
public:virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }
private:int d1 = 0;
};
typedef void(*VFPTR) ();
void PrintVFTable(VFPTR* vftptr)
{for (int i = 0; vftptr[i] != 0; ++i){printf("第%d个虚函数的地址:%p------>", i + 1, vftptr[i]);vftptr[i]();}printf("\n");
}
int main()
{Derive d;VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);PrintVFTable(vTableb1);VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));PrintVFTable(vTableb2);return 0;
}

通过监视创窗口看下d对象的模型:
在这里插入图片描述
通过监视窗口可以看出,d对象有两个虚函数指针和从两个基类继承下来的成员以及自己的成员,但是从监视窗口无法看出两个虚函数表具体放了哪几个虚函数,因此下面我们打印一下两个虚函数表:
观察下图可以看出:多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中!!!
在这里插入图片描述

从上图中可以看出,两个虚函数表中的func1是同一个函数,但是两张表中的地址不一样这是为啥呢?
在这里插入图片描述
下面从汇编代码看下,这个函数是如何调用的:
在这里插入图片描述
从上表中看出,两个表中func1的地址不同,是因为第二个虚函数表中的func1在调用的时候要修正this指针。

至此,本片文章就结束了,若本篇内容对您有所帮助,请三连点赞,关注,收藏支持下。

创作不易,白嫖不好,各位的支持和认可,就是我创作的最大动力,我们下篇文章见!

如果本篇博客有任何错误,请批评指教,不胜感激 !!!
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/661700.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

OBSERVER(观察者)-- 对象行为模式

意图&#xff1a; 定义对象间地一种一对多地依赖关系&#xff0c;当一个对象地状态发生改变时&#xff0c;所有对于依赖于它的对象都得到通知并被自动更新。 别名&#xff1a; 依赖(Dependents), 发布-订阅(Publish-Subsribe) 动机&#xff1a; 将一个系统分割成一系列相互协…

【webrtc】MessageHandler 9: 基于线程的消息处理:执行Port销毁自己

Port::Port 构造的时候,就触发了一个异步操作,但是这个操作是要在 thread 里执行的,因此要通过post 消息 MSG_DESTROY_IF_DEAD 到thread跑:port的创建并米有要求在thread中 但是port的析构却在thread里 这是为啥呢?

C++--const成员及const取地址操作符重载

前言 今天我们来了解一下const成员的基本使用,以及const取地址重载的运用 来开始今天的学习 const成员 1.基本定义, 将const修饰的“成员函数”称之为const成员函数&#xff0c;const修饰类成员函数&#xff0c;实际修饰该成员函数 隐含的*this指针&#xff0c;表明在该成员函…

DSP开发实战教程-国产DSP替代进口TI DSP的使用技巧

1.替换CCS安装路径下的Flash.out文件 找到各自CCS的安装路径&#xff1a; D:\ti\ccs1230\ccs\ccs_base\c2000\flashAlgorithms 复制进芯电子国产DSP官网提供的配置文件 下载链接&#xff1a;https://mp.csdn.net/mp_download/manage/download/UpDetailed 2.替换原有文件 3.…

Object类的公共方法面试问题及回答

1. 什么是 Object 类&#xff1f; 答&#xff1a; Object 类是 Java 中所有类的超类。每个类都使用 Object 作为树的根&#xff0c;所有对象&#xff08;包括数组&#xff09;都实现这个类的方法。 2. Object 类中有哪些重要的方法&#xff1f; 答&#xff1a; equals(Obje…

GPT3 终极指南(二)

原文&#xff1a;zh.annas-archive.org/md5/6de8906c86a2711a5a84c839bec7e073 译者&#xff1a;飞龙 协议&#xff1a;CC BY-NC-SA 4.0 第五章&#xff1a;GPT-3 作为企业创新的下一步 当一个新的创新或技术转变发生时&#xff0c;大公司通常是最后一个采纳的。它们的等级结构…

【学习vue 3.x】(五)VueRouter路由与Vuex状态管理

文章目录 章节介绍本章学习目标 路由的基本搭建与嵌套路由模式vue路由的搭建嵌套路由模式 动态路由模式与编程式路由模式动态路由模式编程式路由 命名路由与命名视图与路由元信息命名路由命名视图路由元信息 路由传递参数的多种方式及应用场景路由传参 详解route对象与router对…

【数学视野】Softmax 函数和 Gibbs 分布之间的数学桥梁

Softmax 函数和 Gibbs 分布之间的数学桥梁 文章目录 一、说明二、Softmax 函数的定义和组件三、从逻辑到多类的泛化3.1 连接到 Gibbs 分布3.2 统计力学背景3.3 切换到 Softmax 功能 四、优化和计算稳定性4.1 Softmax 的梯度4.2 链接到信息论 五、总结 一、说明 softmax 函数是各…

瘦身秘籍:如何使用 PyInstaller 打造超小型 Python 可执行文件

你是否曾经尝试过将你的 Python 程序打包成一个可执行文件&#xff0c;却发现生成的文件大得惊人&#xff1f;别担心&#xff0c;本文将教你如何使用 PyInstaller 尽可能减小生成的 onefile 大小&#xff0c;让你的程序轻盈如风&#xff01; 1. 使用虚拟环境 首先&#xff0c…

MyBatis(XML映射器操作)

文章目录 XML映射器操作&#xff08;XxxMapper.xml&#xff09;文件目录1.基本介绍1.优点2.常用顶级元素 2.环境配置1.在原来的父模块下创建一个子模块2.删除没用的两个文件夹3.创建基本目录4.父模块的pom.xml5.jdbc.properties6.mybatis-config.xml7.测试使用MonsterMapperTes…

【JS篇之】异常

前言&#xff1a;在代码编写过程中&#xff0c;最常遇到的就是程序异常。其实异常并非坏事&#xff0c;它可以让开发人员及时发现、定位到错误&#xff0c;提醒我们做正确的事情&#xff0c;甚至在某些时候&#xff0c;我们还会手动抛出异常。 1.异常的分类 在JS中&#xff0…

考研英语作文---写作要义

应用文结构 例题 信件 ------第一段写法 第一段 第二段 第三段 第一段 第三段 第二段 强调句方式开头方式 练习