C++ 多态以及多态的原理

文章目录

    • 多态的概念
    • 多态的构成条件
      • 虚函数的重写
        • 虚函数重写的两个例外
    • 重载、重写(覆盖)、重定义(隐藏)对比
    • C++11 final 和 override关键字
    • 抽象类
    • 接口继承和普通继承
    • 多态的原理
      • 虚函数表
      • 多态的原理
    • 单继承和多继承关系的虚函数表
      • 单继承中的虚函数表
      • 多继承中的虚函数表

多态的概念

多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会 产生出不同的状态
比如,买票时都是同一个景点有学生票半价和成人票全价等等

多态的构成条件

多态的构成条件主要涉及两个概念:虚函数和继承。
虚函数

  • 虚函数是C++中用于实现运行时多态性的关键概念。
  • 被virtual修饰的类成员函数称为虚函数
class Person {
public:
	//虚函数
	virtual void BuyTicket() 
	{
		//....
	}
};

继承中构成多态还需要满足两个条件

  • 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
  • 父类的指针或者引用进行调用

举个栗子

class Person {
public:
	//虚函数
	virtual void BuyTicket() 
	{
		cout << "买票-全价" << endl;
	}
};
class Student : public Person
{
  //重写基类函数
	virtual void BuyTicket()
	{
		cout << "买票-半价" << endl;
	}
};
//必须是基类对象指针或引用调用
void Func(Person& people)
{
	people.BuyTicket();
}
void Test()
{
	Person Mike;
	Func(Mike);
	Student Johnson;
	Func(Johnson);
}
int main()
{
	Test();
	return 0;
}

父类对象和子类对象调用同一个函数,得到的结果不一样

运行结果

image.png
解释

image.png

虚函数的重写

虚函数重写(也叫覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的 返回值类型、函数名字、参数列表完全相同),称派生类的虚函数重写了基类的虚函数。

在上面的例子中 派生类Student中的BuyTicket函数就重写了基类Person的虚函数

虚函数重写的两个例外
  1. 协变
    派生类重写基类虚函数时,与基类虚函数返回值类型不同,且返回值必须是父子关系的指针或者引用。即基类虚函数返回基类对象的指 针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变
    比如
class Person
{
public:
	// 基类虚函数返回基类指针
	virtual Person* BuyTicket() 
	{
		return new Person();
	}
};
class Student : public Person
{
	// 派生类协变,返回更具体的类型 Student*
	virtual Student* BuyTicket()
	{
		return new Student();
	}
};

上面例子中也构成虚函数的重写,派生类和基类的返回值不同,称为协变

  1. 析构函数的重写(基类与派生类析构函数的名字不同)
    如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字, 都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同, 看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处 理,编译后析构函数的名称统一处理成destructor。
class Person
{
public:
	virtual ~Person()
	{
		cout << "~Person()" << endl;
	}
};
class Student : public Person
{
public:
	~Student()
	{
		cout << "~Student()" << endl;
	}
};
int main()
{
	Person* p1 = new Person();
	Person* p2 = new Student();
	// 只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函数
	//才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。
	delete p1;
	delete p2;
}

运行结果:

image.png
12行加不加virtual关键字 都构成重写

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

image.png

C++11 final 和 override关键字

C++对函数重写的要求比较严格,有些情况可能由于疏忽,导致无法构成重写,这种情况编译器不会报错,程序会正常运行,但是得到的结果不是正确的,所以C++11引入了final和override关键字

  • final 修饰虚函数,表示该虚函数不能再被重写
class Person
{
	virtual void Func() final
	{
		cout << "virtual void Func() final" << endl;
	}
};

class Student : public Person
{
	virtual void Func()//error
	{
		cout << "virtual void Func()" << endl;
	}
};

编译时报错:

image.png

  • override 检查派生类是否重写了基类虚函数。如果没重写,编译器会报错。
class Person
{
public:
	virtual void Func() const 
	{
		cout << "virtual void Func() " << endl;
	}
};

class Student : public Person
{
public:
	virtual void Func() override//error 派生类没有正确重写基类Func函数  编译器会报错,少了const修饰
	{
		cout << "virtual void Func()" << endl;
	}
};

抽象类

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

class Person
{
public:
	virtual void Abstract() = 0;//纯虚函数  Person类为抽象类  Person类不能实例化出对象
};

//派生类
class Student : public Person
{
public:
	virtual void Abstract() override//派生类必须重写纯虚函数,派生类才可以实例化出对象,否则不行
	{
		cout << "Hello World\n";
	}

};
int main()
{
	//Person p1; //error 抽象类无法实例化对象
	Student s1;
	Person* s1prt = &s1;
	//使用基类指针访问
	s1prt->Abstract();
	return 0;
}

接口继承和普通继承

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

多态的原理

虚函数表

#include <iostream>
using namespace std;
class Person
{
public:
	virtual void Func();
private:
	int _a;
	char _b;
};
int main()
{
	cout << sizeof(Person) << endl;
	return 0;
}

上面代码求Person所占字节数大小
按照内存对齐的规则,Person类的大小应该是8(32位下)。
但实际结果是12

image.png
这里不仅要内存对齐,当实例化一个对象后发现,成员变量不仅仅只有_a,和_b 。还有一个指针_vfptr(虚函数表指针)

image.png

  • 一个含有虚函数的类都会有至少一个虚函数表指针,虚函数表指针存放在对象的前4个字节或者前8个字节(32位下4个字节,64位下8个字节)。
  • 而虚函数的地址会被存放到虚函数表中。虚函数表也简称为虚表
  • 虚函数表指针指向虚表。
  • 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr(VS编译器下做了处理,g++没有处理)

image.png
一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。
通过下面的代码进行分析派生类中的虚表。

class Person
{
public:
  virtual void Func()
  {
  	cout << "virtual void Func()" << endl;
  }
  virtual void Func2()
  {
  	cout << "virtual void Func()" << endl;
  }
  void Func3()//普通函数
  {
  	cout << "void Func3()" << endl;
  }
private:
  int _a = 0;
  char _b = 0;
};
class Student : public Person
{
  virtual void Func() override//重写基类函数
  {
  	cout << "virtual void Func()" << endl;
  }
};
int main()
{	
  Person p1;//基类对象
  Student s1;//派生类对象
  return 0;
}
  • Person类中有两个虚函数,一个非虚函数。
  • Student继承了Person类,并且重写了Func函数。
  • 实例化出基类和派生类对象 监视窗口如下
    image.png
    可以发现:
  • 派生类对象也有一个虚表指针,虚表由两部分组成,一部分是继承基类的成员,另一部分是自己的成员。
  • 基类对象和派生类对象的虚表是不一样的,派生类重写了基类的Func虚函数,所以派生类对象虚表中存的是派生类重写后的函数地址。所以虚函数的重写也覆盖,覆盖值得是虚表中虚函数的覆盖,重写是语法的叫法,覆盖是底层的原理。
  • Func2虚函数被继承下来后也会被放到虚表中,Func3也会被继承下来,但是Func3不是虚函数,所以不会放到虚表中

多态的原理

class Person {
public:
	//虚函数
	virtual void BuyTicket()
	{
		cout << "买票-全价" << endl;
	}
};
class Student : public Person
{
	//重写基类函数
	virtual void BuyTicket()
	{
		cout << "买票-半价" << endl;
	}
};
//必须是基类对象指针或引用调用
void Func(Person& people)
{
	people.BuyTicket();
}
void Test()
{
	Person p1;
	Func(p1);
	Student s1;
	Func(s1);
}

对于上面的例子

image.png

  • 当people指向的是基类对象时,people.BuyTicket();就会在基类对象p1中的虚表中找到对应虚函数
  • 当people指向的是派生类对象时,,people.BuyTicket();就会在派生类对象s1中的虚表中找到对应的虚函数
  • 通过虚表,实现了了不同对象去完成同一行为时,展现出不同的形态。

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

单继承中的虚函数表

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 b1;
	Derive d1;
	return 0;
}
  • 上面代码中基类Base有两个虚函数
  • 派生类Derive继承了Base类,并且重写了func1函数,且新增了两个虚函数func3 和func4

单继承对象模型
image.png

image.png

通过监视窗口发现:派生类中新增的虚函数func3 和func4 没有进虚函数表。(不知道是编译器故意的 还是编译器的
bug)
image.png
我们可以利用程序自己打印虚表来观察,参考代码如下。

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 (*VF_Ptr)();//函数指针
//VF_Prt table[];//函数指针数组

//打印虚函数表
void PrintVFTable(VF_Ptr table[])
{
	for (int i = 0; table[i] != nullptr; ++i)
	{
		printf("table[%d] = %p\n", i, table[i]);
                
		VF_Ptr Fun = table[i];//取出函数地址对其进行访问
		Fun();
	}
	cout << endl;
}
int main()
{
	Base b1;
	Derive d1;
	//虚函数表指针在对象的头四个字节(32位下), 拿到对象的地址对其强制类型转换:(int*)&p1
	//在解引用就能拿到对象前四个字节地址:*((int*)&p1),在将其强制类型转换位函数指针:(VF_Ptr*)(*(int*)&p1)
	PrintVFTable((VF_Ptr*)(*(int*)&b1));
	PrintVFTable((VF_Ptr*)(*(int*)&d1));
	return 0;
}

运行结果:

image.png
可以看出,不论是派生类还是基类,只要是虚函数都会存到虚表中

多继承中的虚函数表

class Base1 {
public:
	virtual void func1() { cout << "Base1::func1" << endl; }
	virtual void func2() { cout << "Base1::func2" << endl; }
private:
	int _a;
};
class Base2 {
public:
	virtual void func1() { cout << "Base2::func1" << endl; }
	virtual void func2() { cout << "Base2::func2" << endl; }
private:
    int _b;
};
class Derive : public Base1, public Base2 {
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }
private:
	int _d;
};

多继承对象模型
多继承对象模型对比单继承模型就复杂很多

image.png

image.png

image.png
派生类会有两个虚表,监视窗口仍然无法观察, 通过程序打印查看

typedef void(*VFPTR) ();
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* vTableb1 = (VFPTR*)(*(int*)&d);
	PrintVTable(vTableb1);
        //派生类第二个虚表指针需要加行Base对象大小的偏移量才能获得
	VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));
	PrintVTable(vTableb2);
	return 0;
}

结果如下

image.png
和上面的对象模型一样。
可以发现:
多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

C#实现个人账本管理系统

git地址&#xff1a;https://gitee.com/myshort-term/personal-ledger-management-system 1.系统简介 LedgerManagementSystem是一个小型的个人账本管理系统&#xff0c;可对收支项目进行增加、删除、修改、查询以及导入和导出。可对每日的各类收支项目进行汇总并查看和修改收…

vue3 ts defineProps、defineEmits、defineExpose、defineOptions、defineSlots

文章目录 前言一、defineProps二、defineEmits三、defineExpose四、defineOptions&#xff08; Vue3.3 新特性&#xff09;五、defineSlots(Vue3.3 新特性) 前言 本章我们来讲解vue3 ts 中 defineProps、defineEmits、defineExpose、defineOptions、defineSlots的使用及作用。 …

x-cmd pkg | you-get - web 媒体内容下载工具

目录 简介首次用户功能特点竞品和相关作品进一步阅读 简介 You-Get 是一个开源的命令行小型下载工具&#xff0c;用于从各种网站下载视频、音频和其他媒体文件。 它可以解析和下载嵌套在网页中的媒体&#xff0c;能从 YouTube、优酷、Niconico 、bilibili 等热门网站下载视频、…

C++ vector模拟实现

C vector模拟实现 一.我们要实现的大致框架1.STL库中是如何实现的呢?1.迭代器2.成员变量3.vector的特性4.vector的成员变量大致情况 2.我们要实现的大致框架3.前言 二.具体实现1.迭代器,begin,end2.无参构造,析构,简单函数3.push_back4.reserve1.reserve的第一大坑点:野指针问…

React Native 桥接原生常量

一、编写并注册原生常量方法 在 SmallDaysAppModule 这个模块中有一个方法 getConstans &#xff0c;重载这个方法就可将自定义的常量返回&#xff0c;系统会自行调用该方法并返回定义的常量将其直接注入到 JS 层&#xff0c;在 JS 层直接获取即可。 二、JS 层获取原生常量&am…

电脑USB接口不同颜色的含义

当你看到笔记本电脑或台式机的USB端口时&#xff0c;你会发现USB端口的颜色很多&#xff1b;这些颜色可不只是为了好看&#xff0c;实际上不同颜色代表着不同的性能&#xff0c;那么这些带颜色的USB端口都是什么含义呢&#xff0c;下面就具体介绍下不同颜色代表的含义。-----吴…

钉钉逐浪AI Agent

文&#xff5c;郝 鑫 编&#xff5c;刘雨琦 “大公司代表落后生产力&#xff0c;是慢半拍的”&#xff0c;“小创新靠大厂&#xff0c;大创新仍然要靠小厂”&#xff0c;这是以李彦宏和王小川为代表的创业老炮&#xff0c;在2023年总结出来的创新规律&#xff0c;从移动互…

单片机原理及应用:中断嵌套

​中断嵌套是指中断系统正在执行一个中断服务时&#xff0c;有另一个优先级更高的中断提出中断请求&#xff0c;这时会暂时终止当前正在执行的级别较低的中断源的服务程序&#xff0c;去处理级别更高的中断源&#xff0c;待处理完毕&#xff0c;再返回到被中断了的中断服务程序…

阿里云的通义千问VS百度的文心一言~~

最近人工智能热度迅速升温&#xff0c;我体验了一下各大厂商的大模型的能力&#xff0c;发现他们确实很智能&#xff01; 我想问一下“南方小土豆”这个梗是如何火起来的&#xff0c;结果如下&#xff1a; 文心一言&#xff1a; 回答的比较准确&#xff0c;但有一些过于“官方”…

ChatGPT4+Python近红外光谱数据分析及机器学习与深度学习建模进阶应用

2022年11月30日&#xff0c;可能将成为一个改变人类历史的日子——美国人工智能开发机构OpenAI推出了聊天机器人ChatGPT3.5&#xff0c;将人工智能的发展推向了一个新的高度。2023年4月&#xff0c;更强版本的ChatGPT4.0上线&#xff0c;文本、语音、图像等多模态交互方式使其在…

vue知识-03

购物车案例 要实现的功能&#xff1a; 1、计算商品总价格 2、全选框和取消全选框 3、商品数量的增加和减少 <body> <div id"app"><div class"row"><div class"col-md-6 col-md-offset-3"><h1 class"text-center…

SpringCloudAlibaba微服务架构实战派上下册技术交流!

另外我的新书RocketMQ消息中间件实战派上下册&#xff0c;在京东已经上架啦&#xff0c;目前都是5折&#xff0c;非常的实惠。 https://item.jd.com/14337086.html​编辑https://item.jd.com/14337086.html “RocketMQ消息中间件实战派上下册”是我既“Spring Cloud Alibaba微…

Springboot药物不良反应智能监测系统源码

一、系统简介 ADR指的是药品不良反应&#xff0c;即在合格药品在正常用法用量下&#xff0c;出现与用药目的无关或意外的有害反应。ADR数据辨别引擎、药品ADR信号主动监测引擎、ADR处置行为分析引擎。ADR数据辨别引擎&#xff0c;通过主动监测患者具象临床指标&#xff0c;比如…

Simpy简介:python仿真模拟库-03/5

一、说明 在过去的两篇文章中&#xff0c;我们了解了 simpy 的基础知识、声明变量和处理表达式。值得注意的例子包括评估导数和积分。现在&#xff0c;让我们继续使用函数。 二、SymPy — 函数类 SymPy 包包含 sympy.core.function 模块中的 Function 类。该类作为各种数学函数…

腾讯云优惠券介绍、领取方法及使用教程

腾讯云作为国内领先的云服务提供商&#xff0c;为了吸引更多的用户使用其产品&#xff0c;经常会推出各种优惠券活动。本文将详细介绍腾讯云的优惠券、领取方法和使用教程。 一、腾讯云优惠券介绍 腾讯云优惠券是腾讯云为了吸引用户使用其产品而推出的促销活动。用户可以通过领…

软件测试工程师,从6K到25k的测试之路养成,一路狂飙...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 1、技术方向 就技…

静态网页设计——滑板官网(HTML+CSS+JavaScript)(dw、sublime Text、webstorm、HBuilder X)

前言 声明&#xff1a;该文章只是做技术分享&#xff0c;若侵权请联系我删除。&#xff01;&#xff01; 感谢大佬的视频&#xff1a;https://www.bilibili.com/video/BV1Cw411u7hj/?vd_source5f425e0074a7f92921f53ab87712357b 源码&#xff1a;https://space.bilibili.com…

基于传统机器学习的项目开发过程——@挑大梁

1 场景分析 1.1 项目背景 描述开发项目模型的一系列情境和因素&#xff0c;包括问题、需求、机会、市场环境、竞争情况等 1.2. 解决问题 传统机器学习在解决实际问题中主要分为两类&#xff1a; 有监督学习&#xff1a;已知输入、输出之间的关系而进行的学习&#xff0c;从而…

statsmodels.tsa 笔记 detrend(去趋势)

1 基本使用方法 statsmodels.tsa.tsatools.detrend(x, order1, axis0) 2 参数说明 x数据。如果是二维数组&#xff0c;那么每一行或每一列将独立地去除趋势&#xff0c;但趋势的阶数是一样的。order趋势的多项式阶数。0 表示常数趋势&#xff08;即没有趋势&#xff09;&…

炫技作品!极好!独家原创!一种新型改进的蜣螂优化算法(CCCDBO)

炫技作品&#xff01;&#xff0c;独家原创&#xff01; 蜣螂优化算法DBO的含金量不用我多介绍了吧&#xff0c;这是和麻雀优化算法SSA同一个课题组出的算法&#xff0c;业内公认的比较好的算法&#xff0c;这个算法认可度很高&#xff01; 一种新型改进蜣螂优化算法&#xf…