由浅入深分析c++多态原理

news2024/11/24 13:24:09

多态

  • 背景
  • 多态
    • 构成多态的两个条件
    • 虚函数
    • 虚函数重写
      • 虚函数重写的两个例外
    • c++11的override和final
    • 重载、覆盖(重写)、隐藏(重定义的对比)
  • 抽象类
    • 接口继承和实现继承
  • 多态底层原理
    • 虚函数表
      • 易错问题:
    • 多态原理
  • 动态绑定和静态绑定
  • 打印虚函数表
    • 单继承下的虚函数表
    • 多继承下的虚函数表
  • 总结

背景

多态就是多种状态,不同的对象去完成同一件事时,会有不同的结果。
例如买票:普通人买票是全价买,学生买票就是半价买。

  • 我们设计一个Person类是父类,Student是子类,分别实现买票的成员函数。
#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;
}

在这里插入图片描述

该功能就是由多态实现的。

多态

构成多态的两个条件

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

虚函数

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

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

虚函数重写

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

  • 参数列表完全相同:只要参数的类型相同即可!

注意引言中的例子,若子类不加virtual,也可以构成重写(因为子类在继承基类的虚函数之后,已久保持虚函数的属性),编译器仍然认为构成重写。实际上这样写并不规范,还是建议显式声明一下。

//仍然构成重写
class Person
{
public:
	virtual void BuyTicket() { cout << "全价票" << endl; }
};

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

虚函数重写的两个例外

  1. 协变(基类与派生类返回值类型不同)
    只能是基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或引用。
    返回值可以不同,必须是父子关系指针或者引用(可以是其他父子的指针或引用)
    如下例子:
class Person
{
public:
	virtual Person* BuyTicket() { cout << "全价票" << endl; return this; }
};

class Student : public Person
{
public:
	virtual Student* BuyTicket() { cout << "半价票" << endl; return this; }
};

如何有需求其他父子的指针或引用,也可以:

class A
{};
class B :public A
{};


class Person
{
public:
	virtual A* BuyTicket() { cout << "全价票" << endl; return new A; }
};

class Student : public Person
{
public:
	virtual B* BuyTicket() { cout << "半价票" << endl; return new B; }
};
  1. 析构函数的重写(基类与派生类析构函数的名字不同)
    如果基类的析构函数为虚函数,此时派生类的析构函数只要定义,无论是否加virtual关键字,都构成重写。因为编译器会对析构函数名称做特殊处理,编译后析构函数的名称统一处理成destructor
class Person
{
public:
	virtual ~Person() { cout << "~Person()" << endl; }
};

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

在这里插入图片描述

如果不写virtual,就不构成多态,就是什么类型调用谁的成员函数。
在这里插入图片描述

c++11的override和final

可以感受到,c++对函数重写的要求比较严格,为了防止出错c++提供了两个关键字来帮助程序员更好完成任务。

  1. final:修饰虚函数,表示该虚函数不能再被重写。
class Person
{
public:
	virtual void BuyTicket() final { cout << "全价票" << endl; }
};

class Student : public Person
{
public:
	void BuyTicket() { cout << "半价票" << endl; }   //报错,因为父类的函数使用final修饰,不能再重写。
};
  1. override:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写则编译报错。
class Person
{
public:
	virtual void BuyTicket() { cout << "全价票" << endl; }
};

class Student : public Person
{
public:
	void BuyTicket(int a=0) override { cout << "半价票" << endl; }   //报错,因为子类没有完成重写!
};

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

在这里插入图片描述

抽象类

在虚函数的后面写上 =0,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。

  • 派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。体现了接口继承。
class Car
{
public:
	virtual void Drive() = 0;
};

class BWM : public Car
{
	int a = 0;   
};

int main()
{
	BWM a;  // 报错,因为没有重写纯虚函数
	return 0;
}

修改成以下形式即可:

class Car
{
public:
	virtual void Drive() = 0;
};

class BWM : public Car
{
	virtual void Drive(){}
	int a = 0;
};

接口继承和实现继承

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

如果不实现多态,不要把函数定义成虚函数。

多态底层原理

先看一道题:sizeof(A)是多少?

class A
{
public:
	virtual void Func() { cout << "Func" << endl; }

protected:
	char _str;
	int _a;
};

int main()
{

	cout << sizeof A << endl;   //12
	return 0;
}

我们观察下面这段代码:

int main()
{
	A a;
	cout << sizeof A << endl;
	return 0;
}

在这里插入图片描述

对象a里面还存着一个指针(*vftable),该指针指向虚函数表。

虚函数表

一个含有虚函数的类中,至少有一个虚函数表指针,因为虚函数的地址要放在虚表中。我们再定义几个函数来看看。

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

protected:
	char _str;
	int _a;
};

在这里插入图片描述
可知虚函数表中又多了几个虚函数的地址。


继承下的虚函数表的结构,如下代码:

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

protected:
	char _str;
	int _a;
};

class B :public A
{
public:
	virtual void Func1() { cout << "*******B" << endl; }
private:
	int _b;
};

在这里插入图片描述

观察监视图可得到如下结果:

  • 派生类对象b由两部分组成,一部分是基类A,一部分是自己的成员。其中A类中也包含着一个虚基表指针。
  • 子类重写了Func1函数,虚表里面Func1的地址是不一样的(两个函数,完成了重写)。相当于重写就是将本来Func1的地址,覆盖为修改后的地址。
  • Func2继承下来由于是虚函数,也被放进了虚表中。Func3也被继承下来了,但是Func3不是虚函数,所以没有放进虚表中。

总结一下派生类虚表形成过程:(是在派生类对象构造函数中初始化列表形成的)

  1. 先将基类中的虚表内容拷贝一份到派生类的虚表中
  2. 如果派生类重写了某个虚函数,就把虚表中相应位置覆盖成重写后的地址。
  3. 派生类自己新增加的虚函数按其在派生类的声明次序增加到派生类的虚表最后。

易错问题:

虚表指针存在哪?虚函数存在哪?虚表存在哪?
虚表指针存在对象中,虚函数存在代码段(常量区),虚表也存在代码段(常量区)。
解释:因为虚表在编译期就已经生成了,在构造函数的初始化列表初始化了。它也是不变的,因此存在代码段比较合适。

下面的程序印证了虚函数表在代码段。

int main()
{
	A aa;   
	B bb;
	int a = 0;   //栈区
	int b = 0;

	static int sta_a = 1;   // 数据段(静态区)
	static int sta_b = 1;

	const char* c= "cccccc";   // 代码段(常量区)
	const char* d = "ddddd";
	
	int* p1 = new int;     //堆区
	int* p2 = new int;

	printf("  栈区地址%p\n", &a);
	printf("  栈区地址%p\n", &b);
	printf("静态区地址%p\n", &sta_a);
	printf("静态区地址%p\n", &sta_b);
	printf("常量区地址%p\n", c);
	printf("常量区地址%p\n", d);
	printf("  堆区地址%p\n", p1);
	printf("  堆区地址%p\n", p2);
	printf("aa虚表地址%p\n", *((int*)&aa));
	printf("bb虚表地址%p\n", *((int*)&bb));
	return 0;
}

在这里插入图片描述
下面两张图证明了子类的虚表是在初始化列表中进行。再Debug模式下,按了一下F11.
在这里插入图片描述在这里插入图片描述

多态原理

我们看到的是学生买半价票,普通人买全价票。那么底层到底是怎么调用的呢?有了上面的知识,这个问题就轻而易举了!

下面看汇编代码:

在这里插入图片描述
四个框框:

  1. 将p内容取出来放到eax中。
  2. 将eax内容前4字节取出来,放在edx中。(找到了虚表指针)
  3. 将edx内容的前四字节取出来,放到eax中。(找到了虚函数地址)
  4. call 该地址。

这就是多态的原理,通过虚函数表来找对应函数。


当不构成多态时,普通对象调用函数,根据类型去调用。这种情况函数的地址其实在编译时已经从符号表确认了函数的地址,调用时直接call就好了。

在这里插入图片描述

动态绑定和静态绑定

  • 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为 ,也称为静态多态。
    比如函数重载。

  • 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体函数,也成为动态多态。

打印虚函数表

最后将虚函数表打印出来看一看,表里面到底是什么。

单继承下的虚函数表

//A有虚函数Func1/Func2和自己的函数Func3
//B重写了Func1,并且自己定义了新的虚函数Func4
class A
{
public:
	virtual void Func1() { cout << "A::Func1" << endl; }
	virtual void Func2() { cout << "A::Func2" << endl; }
	void Func3() { cout << "A::Func3()" << endl; }

protected:
	char _str='x';
	int _a=0;
};

class B :public A
{
public:
	B()
		:_b(0)
	{}
	virtual void Func1() { cout << "B::Func1" << endl; }
	virtual void Func4() { cout << "B::Func4" << endl; }
private:
	int _b;
};

定义打印的虚函数:

//将函数指针重定义为 VFPTR
typedef void(*VFTPTR)();

//得传进来虚函数的指针
void PrintVFTable(VFTPTR* table)
{
	for (int i = 0; table[i] != nullptr; i++)
	{
		printf("[%d]:%p->", i, table[i]);
		VFTPTR func = table[i];
		func();

	}
	cout << endl;
}


int main()
{

	A a;
	B b;
	//1.先取a的地址,强转成int*型(为了得到前4B的地址)
	//2.得到前4B的地址后解引用,获得虚表指针
	//3.因为虚表指针的类型是VFTPTR*,所以要强转一下,就可以了
	PrintVFTable((VFTPTR*)(*((int*)&a)));     

	//因为虚表的内容就是VFTPTR,并且虚表也是一个数组。
	//因此指向虚表的指针就是一个2级指针。
	PrintVFTable(*(VFTPTR**)&b);
	return 0;
}

测试结果可知:
在这里插入图片描述
结论:

  1. 不是虚函数不进虚函数表。
  2. 继承下来的虚函数,如重写则覆盖,未重写,继承父类虚函数的地址。
  3. 子类新增的虚函数,从虚表的末尾开始增加,按声明顺序增加。

多继承下的虚函数表


//类A.B都有虚函数Func1/Func2
//类C继承AB,并且重写Func1.
//类C有两个虚函数表,分别是继承AB得来的。
class A
{
public:
	virtual void Func1() { cout << "A::Func1" << endl; }
	virtual void Func2() { cout << "A::Func2" << endl; }

protected:
	char _str = 'x';
	int _a = 0;
};

class B
{
public:
	B()
		:_b(0)
	{}
	virtual void Func1() { cout << "B::Func1" << endl; }
	virtual void Func2() { cout << "B::Func2" << endl; }
private:
	int _b;
};

class C :public A,public B
{
public:
	virtual void Func1() { cout << "C::Func1" << endl; }
	virtual void Func3() { cout << "C::Func3" << endl; }

};

在这里插入图片描述
观察后发现,先声明的A表为主表,因为自己写的Func3,进入到了A表中。B表修改只跟是否重写有关。

总结

多态的知识大而庞杂,关于菱形继承的多态概念本篇没有介绍,若想了解可以参考下面两篇文章。

c++虚函数表解析
c++对象内存布局

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

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

相关文章

深入源码解析ArrayList:探秘Java动态数组的机制与性能

文章目录 一、 简介ArrayList1.1 介绍ArrayList的基本概念和作用1.2 与数组的区别和优势 二、 内部实现2.1 数据结构&#xff1a;动态数组2.2 添加元素&#xff1a;add()方法的实现原理2.3 扩容机制&#xff1a;ensureCapacity()方法的实现原理 三、 常见操作分析3.1 获取元素&…

OLED屏幕,如何成为商显主流

OLED屏幕在商显领域的应用逐渐增加&#xff0c;成为商显主流的原因主要有以下几点&#xff1a; 显示效果优异&#xff1a;OLED屏幕具有自发光原理&#xff0c;色彩鲜艳、对比度高、视角广&#xff0c;能够提供更好的视觉体验。在商业展示、广告宣传等场景中&#xff0c;OLED屏幕…

高通平台开发系列讲解(USB篇)Composite USB gadget framework

文章目录 一、Gadget framework二、Composite driver and gadget driver interaction沉淀、分享、成长,让自己和他人都能有所收获!😄 📢本篇章主要图解高通平台PCIe EP软件架构 一、Gadget framework Composite USB gadget framework 架构如下所示: The composite fram…

【二分查找】【区间合并】LeetCode2589:完成所有任务的最少时间

作者推荐 【动态规划】【广度优先】LeetCode2258:逃离火灾 本文涉及的基础知识点 二分查找算法合集 有序向量的二分查找&#xff0c;向量只会在尾部增加删除。 题目 你有一台电脑&#xff0c;它可以 同时 运行无数个任务。给你一个二维整数数组 tasks &#xff0c;其中 ta…

java常用集合的区别与联系以及应用场景

文章目录 Java集合三大类&#xff08;1&#xff09;List概述&#xff08;2&#xff09;Set概述&#xff08;3&#xff09;Map概述 集合间的区别与联系List&#xff0c;Set和Map的区别ArrayList、Vector和LinkedList的区别HashSet、LinkedHashSet和TreeSet的区别HashSet与HashMa…

探索未来新趋势:鸿蒙系统的崭新时代

探索未来新趋势&#xff1a;鸿蒙系统的崭新时代 随着科技的不断发展&#xff0c;操作系统作为计算机和移动设备的核心&#xff0c;扮演着至关重要的角色。近年来&#xff0c;一种备受瞩目的操作系统——鸿蒙系统&#xff08;HarmonyOS&#xff09;崭露头角&#xff0c;正引领着…

Standoff 12 网络演习

在 11 月 21 日至 24 日于莫斯科举行的 "Standoff 12 "网络演习中&#xff0c;Positive Technologies 公司再现了其真实基础设施的一部分&#xff0c;包括软件开发、组装和交付的所有流程。安全研究人员能够在安全的环境中测试系统的安全性&#xff0c;并尝试将第三方…

Java - Spring中BeanFactory和FactoryBean的区别

BeanFactory Spring IoC容器的顶级对象&#xff0c;BeanFactory被翻译为“Bean工厂”&#xff0c;在Spring的IoC容器中&#xff0c;“Bean工厂”负责创建Bean对象。 BeanFactory是工厂。 FactoryBean FactoryBean&#xff1a;它是一个Bean&#xff0c;是一个能够辅助Spring实例…

Qt 中文处理

windows下 Qt显示中文的几种方式&#xff1a; 1&#xff0c; 环境&#xff1a;Qt 5.15.2 vs2019 64位 win11系统 默认用Qt 创建的文件使用utf-8编码格式&#xff0c;此环境下 中文没有问题 ui->textEdit->append("中文测试"); 2&#xff0c; 某些 低于…

js Array.every()的使用

2023.12.13今天我学习了如何使用Array.every()的使用&#xff0c;这个方法是用于检测数组中所有存在的元素。 比如我们需要判断这个数组里面的全部元素是否都包含张三&#xff0c;可以这样写&#xff1a; let demo [{id: 1, name: 张三}, {id: 2, name: 张三五}, {id: 3, name…

高效数组处理的Numpy入门总结

NumPy是Python中一个重要的数学库&#xff0c;它提供了高效的数组操作和数学函数&#xff0c;是数据科学、机器学习、科学计算等领域的重要工具。下面是一个简单的NumPy学习教程&#xff0c;介绍了NumPy的基本用法和常用函数。 安装NumPy 在使用NumPy之前&#xff0c;需要先安…

ArkTS的状态管理机制(State)

什么是ArkTS的状态管理机制 声明式UI中&#xff0c;是以状态(State)来驱动视图更新的(View)。 状态是指驱动视图更新的数据(被装饰器标记的变量)。 视图是指UI描述渲染得到的用户页面。 互动事件可以改变状态的值。状态改变以后&#xff0c;又会触发事件&#xff0c;渲染页面。…

统信UOS使用4种方法重置用户密码

原文链接&#xff1a;统信UOS使用4种方法重置用户密码 hello&#xff0c;大家好啊&#xff0c;今天我要给大家介绍的是在统信UOS操作系统上使用4种不同方法来重置用户密码。我们都知道&#xff0c;在日常使用中&#xff0c;偶尔会忘记密码&#xff0c;尤其是在使用多个账户的情…

蓝牙协议栈学习笔记

蓝牙协议栈学习笔记 蓝牙简介 蓝牙工作在全球通用的 2.4GHz ISM&#xff08;即工业、科学、医学&#xff09;频段&#xff0c;使用 IEEE802.11 协议 蓝牙 4.0 是迄今为止第一个蓝牙综合协议规范&#xff0c;将三种规格集成在一起。其中最重要的变化就是 BLE&#xff08;Blue…

淘宝订单API接口在电商行业中的应用与实现

一、引言 随着电商行业的快速发展&#xff0c;订单处理成为电商运营的核心环节。淘宝作为中国最大的电商平台之一&#xff0c;其订单API接口在电商行业中的应用越来越广泛。本文将详细介绍淘宝订单API接口在电商行业中的应用&#xff0c;并深入剖析相关的技术细节&#xff0c;…

前端设计模式之旅:命令模式

引言 使用命令模式&#xff0c;我们可以将执行特定任务的对象与调用该方法的对象解耦。 核心思想 命令模式的核心思想是将请求封装成一个对象&#xff0c;从而使请求的发起者和请求的执行者解耦。 这样&#xff0c;请求的发起者只需要知道如何创建命令对象并将其传递给请求者…

基于SSM架构的超市管理系统设计

基于SSM架构的超市管理系统设计 目录 基于SSM架构的超市管理系统设计 1 环境及工具1.1 IDEA软件安装1.2 JDK环境配置1.3 MySQL数据库安装1.3.1常规情况1.3.2非常规情况 1.4 Tomcat安装 2 部署与设计2.1 数据库信息创建2.2项目创建与部署 3 相关说明4 功能操作说明4.1 管理员操作…

ORA-600 [2662] “Block SCN is ahead of Current SCN“

ORA-600 [2662] "Block SCN is ahead of Current SCN" (Doc ID 28929.1)​编辑To Bottom Note: For additional ORA-600 related information please read Note:146580.1PURPOSE: This article discusses the internal error "ORA-600 [2662]"…

基于Java技术的会员制度管理的商品营销系统的设计与实现论文

摘 要 现代经济快节奏发展以及不断完善升级的信息化技术&#xff0c;让传统数据信息的管理升级为软件存储&#xff0c;归纳&#xff0c;集中处理数据信息的管理方式。本商品营销系统就是在这样的大环境下诞生&#xff0c;其可以帮助管理者在短时间内处理完毕庞大的数据信息&am…

anolisos8.8安装显卡+CUDA工具+容器运行时支持(containerd/docker)+k8s部署GPU插件

anolisos8.8安装显卡及cuda工具 一、目录 1、测试环境 2、安装显卡驱动 3、安装cuda工具 4、配置容器运行时 5、K8S集群安装nvidia插件 二、测试环境 操作系统&#xff1a;Anolis OS 8.8 内核版本&#xff1a;5.10.134-13.an8.x86_64 显卡安装版本&#xff1a;525.147.05 c…