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

news2024/9/20 20:21:54

文章目录

  • 一、多态概念
  • 二、多态的定义即实现
      • 2.1 多态的构成条件
      • 2.2 虚函数
      • 2.3虚函数的重写
          • 1.虚函数中析构函数的重写
          • 2.重载、重写(覆盖)、重定义(隐藏)的区别
      • 2.4 C++11 override 和 final
  • 三、抽象类
      • 3.1抽象类概念
      • 3.2 接口继承和实现继承
  • 四、多态的原理
      • 4.1虚函数表
        • 4.2虚表内存的存储位置
      • 4.2多态的原理
      • 4.3 动态绑定和静态绑定
  • 五、单继承和多继承关系的虚函数表
      • 5.1多继承中的虚函数表。


一、多态概念

多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。不同的人去做这件事结果不一样。

二、多态的定义即实现

2.1 多态的构成条件

在继承中构成多态的两个必须前提有
1. 必须是父类的指针去调用虚函数
2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

在这里插入图片描述

2.2 虚函数

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

class Person {
public:
 virtual void Function() { cout << "山羊" << endl;}
};

2.3虚函数的重写

继承关系父子的两个虚函数,需要符合三同(子类虚函数与父类虚函数的返回值,参数列表,函数名字完全相同
三同例外:协变->返回值可以不同,但是返回值必须是父子类关系的指针或者引用。
补充:virtual只能修饰成员函数。

class Person {
public:
 virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
 virtual void BuyTicket() { cout << "买票-半价" << endl; }

 //子类不加virtual也构成虚函数重写
 //void BuyTicket() { cout << "买票-半价" << endl; }


};
void Func(Person& p)
{ p.BuyTicket(); }
int main()
{
Person ps;
Student st;
Func(ps);
Func(st);
 return 0;
}

注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用。

1.虚函数中析构函数的重写

我们来观察以下代码:

class Person {
public:
 ~Person() {cout << "~Person()" << endl;}
};
class Student : public Person {
public:
 ~Student() { cout << "~Student()" << endl; }
};

int main()
{
	 Person* p1 = new Person;
	 Person* p2 = new Student;
	 delete p1;
	 delete p2;
}

在这里插入图片描述可以看出即使p2指针指向Student子类对象但是还是会调用~Person的析构函数。
这是因为delete是根据类型去调用的析构函数,p2指针是Person类型的指针。
但是我们期望这里实现的是指向哪个对象,就去调那个对象的析构函数,这里也可以去实现多态,已完成我们想要的功能。

但是需要完成多态又需要完成“三同”的前提,不用担心,编译器在这里对析构函数名字做出了特殊处理,就是为了构成多态。
现在只剩唯一一个条件,那就是这两个函数要都是虚函数。

class Person {
public:
virtual ~Person() {cout << "~Person()" << endl;}
};
class Student : public Person {
public:
virtual ~Student() { cout << "~Student()" << endl; }
};

int main()
{
	 Person* p1 = new Person;
	 Person* p2 = new Student;
	 delete p1;
	 delete p2;
}

在这里插入图片描述
现在就完成了析构函数的重写。
继承和多态中的析构函数一定是要先析构子类对象再析构父类对象。

例题强化记忆:
以下程序的输出结果是什么?

  class A
   {
   public:
       virtual void func(int val = 1){ std::cout<<"A->"<< val <<std::endl;}
       virtual void test(){ func();}
   };
   
   class B : public A
   {
   public:
       void func(int val=0){ std::cout<<"B->"<< val <<std::endl; }
   };
   
   int main(int argc ,char* argv[])
   {
       B*p = new B;
       p->test();
       return 0;
   }
// A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确

解析:
1.B类型指针p在调用A类中test时调用的是A* this,这里是因为test原本是A类对象的成员函数,被B类对象继承下来,一样还是A类对象的函数。
2.A* this -> func()[所以这里构成父类指针调用虚函数,这里是多态调用,],注意这里A和B的func我们认为参数是相同的[三同我们看的是形参的类型]
3.又因为p指针指向的是B类对象所以这里调用的是B类对象的func().
所以按一般来说我们都会选D选项B->0,
但是这道题选B选项B->1.
为什么呢?
因为虚函数的重写继承的是父类对象函数的声明,
重写的是函数的实现。

2.重载、重写(覆盖)、重定义(隐藏)的区别

在这里插入图片描述

2.4 C++11 override 和 final

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

class Car
{
public:
 virtual void Drive() final {}
};
class Benz :public Car
{
public:
 virtual void Drive() {cout << "Benz-舒适" << endl;}
};

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

class Car{
public:
 virtual void Drive(){}
};
class Benz :public Car {
public:
 virtual void Drive() override {cout << "Benz-舒适" << endl;}
};

三、抽象类

3.1抽象类概念

在虚函数的后面写上 =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();
}

3.2 接口继承和实现继承

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

四、多态的原理

4.1虚函数表

// 这里常考一道笔试题:sizeof(Base)是多少?
class Base
{
public:
 virtual void Func1()
 {
 cout << "Func1()" << endl;
 }
private:
 int _b = 1;
};
int main()
{
	cout << sizeof(Base) << endl;
}

在这里插入图片描述
通过观察测试我们发现b对象是8bytes,除了_b成员,还多一个__vfptr(是一个虚函数指针数组)放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function在计算类的大小时,只要这个类有虚函数我们就要考虑,这个类有虚函数表的问题。
在这里插入图片描述
虚函数编译以后在内存中储存在代码段中。只是单独把地址放到了虚函数表中。

下面Base类中有两个虚函数, 又一个派生类继承了Func1的虚函数,我们可以观测虚函数表中怎么存放虚函数的地址

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

private:
	int _b = 1;
};
class Derive : public Base
{
public:
	virtual void Func1()
	{
		cout << "Derive::Func1" << endl;
	}
};
int main()
{
	//cout << sizeof(Base) << endl;
	Base b;
	Derive d;
}

在这里插入图片描述
我们通过调试可以发现以下几点:
1.虚表指针实际上储存的是,虚函数的地址

2.派生类d中只有一个由父类继承下来的虚表指针(父类和子类的虚表不是同一个,可以理解为子类的虚表是拷贝过来的),不过我们对虚函数Func1做出重写操作时,d类中的Func1地址就会改变,所以d的虚表是继承了b的 Func2 地址和重写 Func1 地址。所以这也就是为什么函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。
3.虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr

总结以下派生类中的虚表生成:
a.先将基类中的虚表内容拷贝一份到派生类虚表中。

b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数。

** c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。**

补充:同一个类的虚函数表是一样的,共用一张虚表

4.2虚表内存的存储位置

堆 栈 静态区 代码段
虚函数存储在哪里?
虚函数和普通函数一样,都是存储在代码段,同时把虚函数地址存了一份到虚函数表。
虚函数表存储在哪?
我们想要获取虚函数表的地址该怎么操作呢?(虚表在vs环境下放在对象的头四个字节上)
我们可以用以下操作

//Base是一个包含虚函数的类
Base b1;
printf("虚表地址:%p", *((int*)&b1));

*((int*)&b1)该怎么分析呢:&b1本来是Base整个对象的地址,由int强转之后变成了只指向Base对象开头四个字节的地址,然后再解引用就取到了Base整个对象前四个字节的数据,也就是去到了虚函数表的数据。

虚函数表存储在代码段(常量区)
且补充:所有的虚函数一定是存在虚函数表的。

4.2多态的原理

还记得这里Func函数传Person调用的Person::BuyTicket传Student调用的是Student::BuyTicket
在这里插入图片描述

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 Mike;
	Func(Mike);
	Student Johnson;
	Func(Johnson);
 return 0;
}

1.观察下图的红色箭头我们看到,p是指向mike对象时,p->BuyTicket在mike的虚表中找到虚函数是Person::BuyTicket。
2.观察下图的蓝色箭头我们看到,p是指向johnson对象时,p->BuyTicket在johson的虚表中找到虚函数是Student::BuyTicket。(此时是父类对象指针调用子类对象,会发生切片因为上面代码类中只有虚函数,也就是把虚函数表切出来,但是此时,虚函数表已经是子类对象拷贝过来的虚函数表了)
3.这样就实现出了不同对象去完成同一行为时,展现出不同的形态。

在这里插入图片描述

思考:
为什么Func不能用父类对象传参只能用父类对象指针或引用传参
指针和引用传参:
Person* p = johnson指向子类对象时,是把子类对象中父类那一部分切割出来。
对象传参:
Person p = johnson 切割出子类对象父类中那一部分,成员拷贝给父类,但是不会拷贝虚函数表指针。(如果虚函数表指针拷贝过去就会产生一些问题:1.多态调用时,指向父类,调用的还是父类吗?[如果父类被子类虚函数表赋值过,里面有可能是子类的虚函数表]2.析构函数可能会被调错,因为析构函数也在虚表里面)

4.3 动态绑定和静态绑定

  1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
  2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。比如:虚函数就是在程序运行期间,去调用虚函数表查找该虚函数。

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

在多继承关系中我们主要关注的是子类的虚函数表,因为父类的虚函数表在虚函数表中不会跟单继承有多少区别。

5.1多继承中的虚函数表。

我们可以观察多继承中Derive子类的虚函数表。
那么我们该如何查看子类中的虚表呢?

1.首先就是先找到虚表指针。虚表指针一般都在对象的首四个字节处。于是我们可以取出对象的头4个字节,就拿到了虚表的指针
2.取到这个虚表指针之后,往后遍历,直到遇到nullptr为止


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) ();//对函数指针进行typedef
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*类型,因为要与上面PrintVTable函数传参 
	 VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
	 PrintVTable(vTableb1);
	 VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d+sizeof(Base1)));
	           							//sizeof(Base1)是跳过了Derive类中base1的大小,去访问Base2的虚函数表。记得要强转为char*类型。
	           							//也可以强转为Base1*再+1
	 //这个写法也OK         							
	 //VFPTR* ptr = &d;
	 PrintVTable(vTableb2);
	
	 return 0;
}

在这里插入图片描述
通过执行后观察调试,我们会发现,Derive在继承两个父类后会产生两个虚表。
那么Derive类中的Func3函数放在哪了?(注意这里的调试窗口看不到Func3函数的地址不是因为没有,只是在vs环境下派生类中的虚函数几乎不会放到虚表里面。)

他的对象模型大概是像下图所展示的一样。
在这里插入图片描述
该子类的虚函数就会放在第一张虚表中

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

中断处理过程介绍

概念 中断 中断源 分类 中断处理过程 中断请求 实现器件 中断判优 软件判优 过程 器件实现 程序实现 硬件判优 链路判优 器件实现 控制器判优 中断响应 中断服务 中断返回

C语言作为计算机行业的基础之一,是否制约了行业本身的发展?

c不是计算机行业的基础啦&#xff0c;你想&#xff0c;c语言出现时已经有一套成熟的计算机体系&#xff0c;有基于内存地址的寻找指令、数据的工作方式&#xff0c;有汇编语言&#xff0c;那搞出c这种高级语言就很正常啊&#xff01;刚好我有一些资料&#xff0c;是我根据网友给…

代码随想录算法训练营第20天 |● 654.最大二叉树 ● 617.合并二叉树 ● 700.二叉搜索树中的搜索 ● 98.验证二叉搜索树

文章目录 前言654.最大二叉树思路方法一 递归法方法一2 老师的优化递归法 617.合并二叉树思路方法一 递归法方法二 迭代法 700.二叉搜索树中的搜索思路方法一 递归法方法二 迭代法 98.验证二叉搜索树思路方法一 使用数组方法二 不使用数组代码注意点&#xff1a; 方法二 使用双…

【Linux】Linux的基本指令_3

文章目录 二、基本指令15. date16. cal16. find17. grep18. zip 和 unzip19. tar20. uname 未完待续 二、基本指令 15. date date 命令可以显示当前时间。 常用标记列表&#xff1a; %H : 小时(00…23) %M : 分钟(00…59) %S : 秒(00…61) %X : 相当于 %H:%M:%S %d : 日 (01……

简易计算器

前言 简易计算器&#xff0c;旨在实现一个简单的计算器功能。 整形&#xff0c;浮点型数据的加减乘除运算&#xff1b;数据的统计(如文件中某字符的出现频数)&#xff1b;期望&#xff0c;方程运算&#xff1b;平均数&#xff0c;最小值&#xff0c;最大值&#xff0c;中位数…

C++之“流”-第2课-C++和C标准输入输出同步

为什么C和C的标准输入输出不同步时&#xff0c;数据会混乱&#xff1f;同步会带来多大性能损失&#xff1f;为什么说这个损失通常不用太在乎&#xff1f; 0. 课堂视频 C之“流”-第2课&#xff1a;和C输入输出的同步 1. 理解cin和cout的类型与创建过程 std::cout 是std::ostre…

Css 提高 - 获取DOM元素

目录 1、根据选择器来获取DOM元素 2.、根据选择器来获取DOM元素伪数组 3、根据id获取一个元素 4、通过标签类型名获取所有该标签的元素 5、通过类名获取元素 目标&#xff1a;能查找/获取DOM对象 1、根据选择器来获取DOM元素 语法&#xff1a; document.querySelector(css选择…

C/C++运行时库和UCRT系统通用运行时库总结及问题实例分享

目录 1、概述 2、不同版本的Visual Studio对应的运行时库说明 3、在Windbg10.0安装目录中获取UCRT通用运行时库 4、微软官网对UCRT通用运行时库的相关说明 5、使用Visual Studio 2017开发软件初期遇到的UCRT通用运行时库问题 6、如何查看软件依赖了哪些C/C运行时库&#…

vueRouter路由总结

https://blog.csdn.net/qq_24767091/article/details/119326884

奇门遁甲古籍《烟奇要览》

《烟奇要览》 全书共178页 时间有限&#xff0c;仅上传部分图片&#xff01;

js深入理解对象的 属性(properties)的特殊 特性(attributes)

对象 js对象 // 构造一个对象 let obj {}; let obj new Object(); 我们知道js中一切皆对象&#xff0c;对象是一个键值对集合&#xff08;key: value)&#xff0c;一个键(key)对应一个值(value)&#xff0c;而每个键都是这个对象的属性&#xff0c;我们可以通过对象的属性来…

在CentOS 8上卸载与安装MySQL 8的详细步骤

关键词&#xff1a;MySQL 8安装、CentOS 8、YUM源配置、卸载MySQL、MySQL残留文件删除、首次登录MySQL临时密码、服务状态检查、MySQL社区服务器 阅读建议&#xff1a;本文适合需要在CentOS 8操作系统上部署最新MySQL 8数据库的系统管理员或开发者阅读。文中步骤简洁清晰&#…

ResizeObserver loop completed with undelivered notifications.

报错信息 ResizeObserver loop completed with undelivered notifications. 来源 在用vue3 element-plus写项目的时候报的错&#xff0c;经过排查法&#xff0c;发现是element-plus的el-table组件引起的错误。 经过初步排查&#xff0c;这个错误并不是vue以及element-plus…

springboot投票统计管理系统的设计与实现-计算机毕业设计源码73598

摘 要 随着互联网趋势的到来&#xff0c;各行各业都在考虑利用互联网将自己推广出去&#xff0c;最好方式就是建立自己的互联网系统&#xff0c;并对其进行维护和管理。在现实运用中&#xff0c;应用软件的工作规则和开发步骤&#xff0c;采用Java技术建设投票统计管理系统。 …

最新扣子(Coze)实战教程:扣子​使用基础,完全免费,快来学习吧~

&#x1f9d9;‍♂️ 诸位好&#xff0c;吾乃斜杠君&#xff0c;编程界之翘楚&#xff0c;代码之大师。算法如流水&#xff0c;逻辑如棋局。 &#x1f4dc; 吾之笔记&#xff0c;内含诸般技术之秘诀。吾欲以此笔记&#xff0c;传授编程之道&#xff0c;助汝解技术难题。 &#…

电脑由于ntdll.dlI丢失导致exe崩溃有什么解决办法?解决ntdll.dll丢失问题

相信有一些用户正在面临一个叫做“ntdll.dll丢失”的问题&#xff0c;这种情况多半发生在试图运行某个程序时&#xff0c;系统会提示一条错误消息&#xff1a;“程序无法启动&#xff0c;因为计算机中丢失了ntdll.dll”。那么&#xff0c;为何ntdll.dll文件会丢失&#xff0c;又…

OrangePi Kunpeng Pro 开箱测评之一步到喂

前情提要&#xff1a;大家好&#xff0c;我是Samle。有幸接到 CSDN 发来的测评邀请&#xff0c;下面针对 OrangePi Kunpeng Pro 开发板进行一些实践操作&#xff0c;让大家能更好的上手这块板子。 以下内容来自 官方说明 OrangePi Kunpeng Pro采用4核64位处理器AI处理器&#…

JavaScript数组重构数据,数组转换成对象

在后端返回的数据中&#xff0c;可能不太满意&#xff0c;所以需要自己重构数据。 原始数据 let arr [ {title:"光头强",age:18,id:"0"}, {title:"孙悟空",age:18,id:"9"}, {title:"熊大",age:18,id:"0"}, {ti…

RAG-GPT实践过程中遇到的挑战

引言 前面介绍了使用RAG-GPT和OpenAI快速搭建LangChain官网智能客服。有些场景&#xff0c;用户可能无法通过往外网访问OpenAI等云端LLM服务&#xff0c;或者由于数据隐私等安全问题&#xff0c;需要本地部署大模型。本文将介绍通过RAG-GPT和Ollama搭建智能客服。 RAG技术原理…