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指针。

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

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

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

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

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

相关文章

C++Day 7 作业

1、lambda #include <iostream>using namespace std;int main() {int a 100;int b 90;int temp;auto fun [&]()mutable->int {temp a;ab;btemp;};fun();cout<<a<<endl;return 0; } 2、vector #include <iostream> #include <vector>…

python安卓自动化pyaibote实践------学习通自动刷课

前言 欢迎来到我的博客 个人主页:北岭敲键盘的荒漠猫-CSDN博客 本文是一个完成一个自动播放课程&#xff0c;避免人为频繁点击脚本的构思与源码。 加油&#xff01;为实现全部电脑自动化办公而奋斗&#xff01; 为实现摆烂躺平的人生而奋斗&#xff01;&#xff01;&#xff…

python项目入门新手攻略

最近工作需要接手了代码量比较大的python开发的项目&#xff0c;平时写python不多&#xff0c;记录一下如何熟悉项目。 分析调用流程-pycallgraph 因为代码量比较大&#xff0c;所以希望通过工具生成代码调用流程&#xff0c;因此用到了pycallgraph。 pycallgraph&#xff0…

翻译: 什么是ChatGPT 通过图形化的方式来理解 Transformer 架构 深度学习三

合集 ChatGPT 通过图形化的方式来理解 Transformer 架构 翻译: 什么是ChatGPT 通过图形化的方式来理解 Transformer 架构 深度学习一翻译: 什么是ChatGPT 通过图形化的方式来理解 Transformer 架构 深度学习二翻译: 什么是ChatGPT 通过图形化的方式来理解 Transformer 架构 深…

56.基于SSM实现的在线教育网站系统(项目 + 论文)

项目介绍 本站是一个B/S模式系统&#xff0c;采用Java的SSM框架作为开发技术&#xff0c;MYSQL数据库设计开发&#xff0c;充分保证系统的稳定性。系统具有界面清晰、操作简单&#xff0c;功能齐全的特点&#xff0c;使得基于SSM的在线教育网站的设计与实现管理工作系统化、规范…

Scikit-Learn回归树

Scikit-Learn回归树 1、决策树1.1、什么是决策树1.2、决策树学习的步骤1.3、决策树算法 1、决策树 决策树&#xff08;DTs&#xff09;是一种用于回归和分类的有监督学习方法。通常&#xff0c;决策树用于分类问题&#xff1b;当决策树用于回归问题时&#xff0c;称为回归树。回…

Midjourney之绘画背景的选择

hello 小伙伴们&#xff0c;我是你们的老朋友——树下&#xff0c;今天分享Midjourney提示词中绘画背景的选择&#xff0c;话不多说&#xff0c;直接开始~ 对于背景的选择&#xff0c;Midjourney中主要体现在年代和所处的环境对绘画产生不同的影响 科技的发展&#xff0c;我们…

matlab学习006-使用matlab绘出系统的冲激响应和阶跃响应波形并求其冲激响应的数值解

目录 题目 1&#xff0c;绘出系统的冲激响应和阶跃响应波形 1&#xff09;基础 2&#xff09;效果 3&#xff09;代码 2&#xff0c;求出t0.5s,1s,1.5s,2s时系统冲激响应的数值解。 1&#xff09;基础 2&#xff09;效果 ​☀ 3&#xff09;代码 题目 已知描述某连续系…

【Python】Anaconda 使用笔记

文章目录 一、创建环境1.1 在任意磁盘中创建环境1.2 添加环境路径envs_dirs 二、安装和使用Python环境三、删除已有的Python环境 前言   笔者使用Python的目的主要是为了学习神经网络等深度学习算法。但是在学习之初配置环境的时候发现之前的环境配置一团乱麻&#xff0c;不仅…

Mybatis进阶(动态SQL)

文章目录 1.动态SQL1.基本介绍1.为什么需要动态SQL2.基本说明3.动态SQL常用标签 2.环境搭建1.新建子模块2.删除不必要的两个文件夹3.创建基本结构4.父模块的pom.xml5.jdbc.properties6.mybatis-config.xml7.MyBatisUtils.java8.MonsterMapper.java9.MonsterMapper.xml10.测试Mo…

第七篇:专家级指南:Python异常处理的艺术与策略

专家级指南&#xff1a;Python异常处理的艺术与策略 1 引言 在编程的世界中&#xff0c;异常处理是一门必修的艺术。它不仅涉及到程序的错误处理&#xff0c;更广泛地影响着软件的稳定性、健壮性和用户体验。本篇文章将深入探讨Python中的异常处理&#xff0c;展示如何通过精心…

Linux:服务器间同步文件的脚本(实用)

一、功能描述 比如有三台服务器&#xff0c;hadoop102、hadoop103、hadoop104&#xff0c;且都有atguigu账号 循环复制文件到所有节点的相同目录下&#xff0c;且脚本可以在任何路径下使用 二、脚本实现 1、查看环境变量 echo $PATH2、进入/home/atguigu/bin目录 在该目录下…

三. Django项目之电商购物商城 -- 校验用户名 , 数据入库

Django项目之电商购物商城 – 校验用户名 , 数据入库 需要开发文档和前端资料的可私聊 一. 路由匹配获得用户名 在注册时 , 用户输入用户名 , 通过ajax请求发送到服务器 , 在路由中设置对应url , 响应视图 , 将用户输入的用户名传入视图 , 与数据库进行校验检查用户名是否重…

信息技术内涵及意义

一、信息技术及其演进趋势 &#xff08;一&#xff09;信息技术概况概念 信息技术&#xff08;Information Technology&#xff0c;IT&#xff09;指“应用在信息加工和处理中的科学、技术与工程的训练方法与管理技巧&#xff1b;上述方法和技巧的应用&#xff1b;计算机及其…

linux高性能服务器--Ngix内存池简单实现

文章目录 内存模型&#xff1a;流程图内存对齐code 内存模型&#xff1a; 流程图 内存对齐 对齐计算 要分配一个以指定大小对齐的内存&#xff0c;可以使用如下公式&#xff1a; 假设要分配大小为n&#xff0c;对齐方式为x&#xff0c;那么 size(n(x-1)) & (~(x-1))。 举个…

【大模型系列】大模型的上下文长度解释与拓展

文章目录 1 什么是大模型的上下文长度&#xff1f;2 拓展大模型上下文长度的方式参考资料 1 什么是大模型的上下文长度&#xff1f; 大模型的上下文长度&#xff08;Context Length&#xff09;是指在自然语言处理&#xff08;NLP&#xff09;的大型语言模型&#xff08;Large…

自动的异地组网工具?

越来越多的企业和个人对远程访问和异地组网需求日益增加。为了满足这一需求&#xff0c;各种技术和服务也不断涌现。其中一项备受关注的技术就是自动的异地组网。本文将介绍这一技术的优势和特点。 【天联】组网的优势 天联组网技术以其卓越的性能和稳定性备受用户称赞。它的优…

数据结构:实验七:数据查找

一、 实验目的 &#xff08;1&#xff09;领会各种查找算法的过程和算法设计。 &#xff08;2&#xff09;掌握查找算法解决实际问题。 二、 实验要求 &#xff08;1&#xff09;编写一个程序exp8-1.cpp, 按提示输入10个任意的整形数据&#xff08;无序&#xff09;&…

数字旅游引领未来智慧之旅:科技应用深度重塑旅游生态,智慧服务全面升级打造极致高品质旅游体验

随着信息技术的飞速发展&#xff0c;数字旅游作为旅游业与科技融合的新兴业态&#xff0c;正以其独特的魅力和优势&#xff0c;引领着旅游业迈向智慧之旅的新时代。数字旅游不仅通过科技应用重塑了旅游生态&#xff0c;更通过智慧服务为游客带来了高品质的旅游体验。本文将深入…

从键入网址到网页显示,期间发生了什么?

从键入网址到网页显示&#xff0c;期间发生了什么&#xff1f; 孤单小弟【HTTP】真实地址查询【DNS】指南帮手【协议栈】可靠传输【TCP】远程定位【IP】两点传输【MAC】出口【网卡】送别者【交换机】出境大门【路由器】互相扒皮【服务器与客户端】相关问答 不少小伙伴在面试过程…