【C++】多态学习

多态

  • 多态的概念与定义
    • 多态的概念
    • 构成多态的两个条件
      • 虚函数与重写
        • 重写的两个特例
  • final 和 override
  • 重载、重写(覆盖)、重定义(隐藏)的对比
  • 抽象类
  • 多态的原理
    • 静态绑定与动态绑定
  • 单继承与多继承关系下的虚函数表(派生类)
    • 单继承中的虚函数表查看
    • 多继承中的虚函数表查看
  • 菱形继承与菱形虚拟继承
    • 菱形继承
    • 菱形虚拟继承
  • 继承与多态一些常见问题

多态的概念与定义

多态的概念

多态就是多种形态,简单理解就是不同的对象去执行某个行为时会产生出不同的状态表现。
多态表现在继承关系中,继承关系的类对象去调用同一函数,会产生不同的状态行为表现。
例如,在买票体系中,普通人(Person)买票是全价,学生(Student)买票是半价。

构成多态的两个条件

  1. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数重写。
  2. 必须是通过基类的指针或者引用调用虚函数。

虚函数与重写

虚函数:被virtual关键字修饰的类成员函数
虚函数的重写:
重写也叫覆盖。重写要满足三同条件,三同条件也是建立在虚函数的基础上。
三同条件要求派生类虚函数与基类虚函数的返回值类型、函数名、参数列表完全相同。

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

class Student : public Person
{
public:
	/* 
	* 注意:子类虚函数不加virtual,依旧构成重写
	* 因为继承后基类的虚函数被继承下来在派生类依旧保持虚函数属性
	* 但实际最好加上virtual,否则写法不是很规范
	*/
	void BuyTicket()
	{
		cout << "买票-半价" << endl;
	}
};

void Func(Person& p)
{
	p.BuyTicket();
}
void Test1()
{
	Person p;
	Student st;

	Func(p);
	Func(st);
}

要实现多态,那多态的两个条件必须严格遵守,任何一个条件不符合规则,或任何一个条件下的小条件不满足,都无法成功实现多态。

重写的两个特例

  1. 协变
    派生类重写基类虚函数时,基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用。
class A
{};

class B : public A
{};

class Person
{
public:
	//virtual Person* BuyTicket()
	virtual A* BuyTicket()
	{
		cout << "买票-全价" << endl;
		//return this;
		return nullptr;
	}
};

class Student : public Person
{
public:
	// 重写的协变:返回值可以不同,要求必须是父子关系的指针或者引用
	// 这里满足父子关系即可,不一定非要某类父子关系
	virtual B* BuyTicket()
	{
		cout << "买票-半价" << endl;
		//return this;
		return nullptr;
	}
};

void Func(Person& p)
{
	p.BuyTicket();
}
void Test2()
{
	Person p;
	Student st;

	Func(p);
	Func(st);
}
  1. 析构函数的重写
    一眼看去,基类与派生类中析构函数的重写似乎不满足三同中的函数名相同,其实不然,可以理解为编译器对析构函数的名称做了特殊处理,在程序编译后析构函数的名称统一处理成了destructor
    所以,只要基类的析构函数是虚函数,此时派生类的析构函数只要定义,都与基类的析构函数构成重写。
    而且一般建议,将继承体系中析构函数定义成虚函数。下面的例子可以帮助参考。
class Person
{
public:
	//~Person()
	virtual ~Person()
	{
		cout << "~Person()" << endl;
	}
};

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

void Test3()
{
	Person* p1 = new Person;
	delete p1;

	Person* p2 = new Student;
	delete p2;
}

在这里插入图片描述

final 和 override

  1. final
    修饰虚函数,表示该虚函数不能被重写。
class Car
{
public:
	virtual void Drive() final
	{}
};

class Benz : public Car
{
public:
	// 无法实现重写
	virtual void Drive()
	{
		cout << "Benz" << endl;
	}
};

final也可以修饰类,表示该类不能被继承。

  1. override
    用于检查派生类虚函数是否重写了基类某个虚函数,如果没有重写会编译报错。
class Car
{
public:
	virtual void Drive()
	{}
};

class Benz : public Car
{
public:
	// override 检查子类虚函数是否完成重写
	virtual void Drive() override
	{
		cout << "Benz" << endl;
	}
};

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

在这里插入图片描述
两个基类和派生类的同名函数不构成重写,就是构成重定义。

抽象类

虚函数的后面加上=0,则表示这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(接口类)。
抽象类无法直接实例化出对象。抽象类被派生类继承后,派生类如果不重写纯虚函数,派生类也不能实例化出对象。
纯虚函数规范了派生类必须进行重写,体现了接口继承。

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

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

void Test4()
{
	Benz b;
	b.Drive();
}

多态的原理

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

void Test5()
{
	cout << "sizeof Base: " << sizeof Base << endl;
	Base b;
}

在这里插入图片描述
在这里插入图片描述
从上面结果可以看出,b对象中,除了_b成员,还多了一个_vfptr的指针(虚函数表指针,v代表virtual,f代表function)。
一个含有虚函数的类中都至少有一个虚函数表指针,而虚函数的地址被放到虚函数表(简称虚表)中。
Test5的代码改造一下,进一步观察。

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 = 0;
};

class Derive : public Base
{
public:
	virtual void Func1()
	{
		cout << "Derive::Func1()" << endl;
	}
private:
	int _d = 0;
};

void Test6()
{
	Base b;
	Derive d;
}

在这里插入图片描述
通过观察,可以知道基类b对象和派生类d对象的虚表是不一样的。因为Func1完成了重写,所以d对象的虚表中存的是重写的Derive::Func1(),这也是重写被叫做覆盖的道理,即覆盖就是虚表中虚函数的覆盖。(重写是语法层的叫法,覆盖是原理层的叫法)
其实,派生类虚表是从基类虚表拷贝过来的,如果派生类重写了基类的某个虚函数,就用派生类自己的虚函数覆盖虚表中基类的虚函数,派生类自己新增加的虚函数按其在派生类中的声明次序,依次增加到派生类虚表的最后。
下面再通过之前买票的例子Test1帮助阐述多态的原理。

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

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

void Func(Person& p)
{
	p.BuyTicket();
}
void Test7()
{
	Person Mike;
	Student Allen;

	Func(Mike);
	Func(Allen);
}

MikeAllen通过Func传给p
p指向Mike时,就是在Mike的虚表中找到虚函数Person::BuyTicket
p指向Allen时,就是在Allen的虚表中找到虚函数Student::BuyTicket
这样就实现了不同对象去执行同一行为时,展现出不同形态的情况。
多态的本质总结:
对象多态成员函数调用时,会到对象的虚表中找到对应的虚函数地址,进行调用。

静态绑定与动态绑定

  1. 静态绑定又称前期绑定/早绑定。
    是指在程序编译期间就确定了程序的行为。也称静态/编译时多态。
    像重载,或是普通类成员函数的调用(直接call函数地址)。
    在这里插入图片描述
  2. 动态绑定又称后期绑定/晚绑定。
    是指在程序运行过程中,需要根据具体情况确定程序的具体行为。也称动态/运行时多态。
    在这里插入图片描述

单继承与多继承关系下的虚函数表(派生类)

单继承中的虚函数表查看

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

	virtual void func2()
	{
		cout << "Base:func2" << endl;
	}

private:
	int _b = 0;
};

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 _d = 1;
};

void Test8()
{
	Base b;
	Derive d;
}

Test8测试代码调试时看到的虚表可能不完整,可以通过下面函数对虚表进行打印。

// VFPTR是一个函数指针,指向的函数参数为void,返回值为void
typedef void(*VFPTR)();

void PrintVFTable(VFPTR table[])
{
	for (size_t i = 0; table[i] != nullptr; ++i)
	{
		printf("vft[%d]:%p\n", i, table[i]);
		table[i](); // 函数回调
	}
}

对于PrintVFTable函数的调用如下。

/*
* 1.先取对象的地址,强转成int*,可以拿到头四个字节的地址
* 2. 在解引用取到的是虚函数表的指针,强转成VFPTR*,就可以进行传递
*/
PrintVFTable((VFPTR*)(*(int*)&b));
cout << endl;
PrintVFTable((VFPTR*)(*(int*)&d));

在这里插入图片描述

多继承中的虚函数表查看

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 _d = 3;
};

void Test9()
{
	cout << "sizeof Derive: " << sizeof Derive << endl;
	Derive d;
}

在这里插入图片描述
对内存的查看:
在这里插入图片描述
d对象继承自两个父类,具有两张虚表。
下面通过PrintVFTable对两张表中的内容进行查看。

// 第一个虚表的查看
PrintVFTable((VFPTR*)(*(int*)&d));
cout << endl;
// 第二个虚表的查看 - 方法一
PrintVFTable((VFPTR*)(*(int*)((char*)&d + sizeof(Base1))));
// 第二个虚表的查看 - 方法二
Base2* pb = &d;
PrintVFTable((VFPTR*)(*(int*)(pb)));

在这里插入图片描述
可以看到多继承派生类的未重写的虚函数放在第一个所继承基类部分的虚函数表中。
其实子类有几个父类,如果父类有虚函数,则就会有几张虚表,子类自己的虚函数只会放到第一个父类的虚表后面。
这里深入查看,发现两张虚表中虽然存的都是Derive::Func1,但调用时所用的地址却是不一样的,这是如何做到的?下面通过查看汇编来看看。

Derive d;

Base1* pb1 = &d;
Base2* pb2 = &d;

d.Func1(); // 普通函数调用

pb1->Func1(); // 多态调用
pb2->Func1(); // 多态调用

在这里插入图片描述
通过汇编的查看可以发现,虽然最初的地址不同,但最后都能跳到同一处进行函数调用,即Deriver::Func1

菱形继承与菱形虚拟继承

菱形继承

class A
{
public:
	virtual void func1() { cout << "A::func1" << endl; };
public:
	int _a;
};
class B : public A
{
public:
	virtual void func1() { cout << "B::func1" << endl; };
	virtual void func2() { cout << "B::func2" << endl; };
public:
	int _b;
};
class C : public A
{
public:
	virtual void func1() { cout << "C::func1" << endl; };
	virtual void func2() { cout << "C::func2" << endl; };
public:
	int _c;
};
class D : public B, public C
{
public:
	int _d;
};

void Test10()
{
	D d;
	d.B::_a = 1;
	d.C::_a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;
}

在这里插入图片描述

菱形虚拟继承

class A
{
public:
	virtual void func1() { cout << "A::func1" << endl; };
public:
	int _a;
};
class B : virtual public A
{
public:
	virtual void func1() { cout << "B::func1" << endl; };
	virtual void func2() { cout << "B::func2" << endl; };
public:
	int _b;
};
class C : virtual public A
{
public:
	virtual void func1() { cout << "C::func1" << endl; };
	virtual void func2() { cout << "C::func2" << endl; };
public:
	int _c;
};
class D : public B, public C
{
public:
	// 此时D必须对func1进行重写
	virtual void func1() { cout << "D::func1" << endl; };
public:
	int _d;
};

void Test11()
{
	D d;
	d.B::_a = 1;
	d.C::_a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;
}

D必须对func1进行重写,因为B和C都有fun1,虚拟继承为了解决数据冗余和二义性,D的虚表里面只能存放一个,就无法确定存哪一个。
在这里插入图片描述
通过内存查看,菱形虚拟继承的关系可以如下表示。
在这里插入图片描述

继承与多态一些常见问题

  1. inline函数可以是虚函数吗?
    可以,当一个函数是虚函数,在多态调用中,inline就失效了。
  2. static函数可以是虚函数吗?
    不可以,static成员函数都是在编译时进行地址确定。虚函数是为了实现多态,需要运行时去虚表进行地址确定,static函数是virtual的话没有意义,因为本来就不会去虚表。
  3. 析构函数可以是虚函数吗?
    不可以,对象中的虚表指针都是构造函数初始化列表阶段才进行初始化的,所以构造函数是虚函数是没有意义的。
  4. 析构函数可以是虚函数吗?
    可以,并且建议基类的析构函数定义成虚函数。
  5. 拷贝构造函数可以是虚函数吗?
    不可以,拷贝构造函数也是构造函数。
  6. 赋值函数可以是虚函数吗?
    语法上可以,但是没有什么实际价值。
  7. 对象访问普通函数快还是虚函数快?
    虚函数不构成多态,是一样快;
    虚函数构成多态调用,普通函数更快。因为多态调用是运行时去虚函数表中找虚函数地址。
  8. 虚函数表是什么时候生成的?存在哪的?
    虚函数表是编译阶段就生成好的,存在于代码段(常量区)。所以一个类的不同对象共享该类的虚表。
    (构造函数初始化列表阶段初始化的是虚函数表指针,对象中存的也是虚函数表指针)

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

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

相关文章

Unity3d:GameFramework解析:实体,对象池,资源管理,获取计数,引用计数,自动释放

基本概念 1.GF万物基于引用池IReference 2.ObjectBase : IReference类的m_Target持有unity中Mono&#xff0c;资源&#xff0c;GameObejct 3.AssetObject : ObjectBase类m_Target持有Assetbundle中的Asset&#xff0c;具有获取&#xff0c;引用两个计数管理释放 4.ResourceObj…

sql:SQL优化知识点记录(三)

&#xff08;1&#xff09;explain之select_type和table介绍 简单的查询类型是&#xff1a;simple 外层 primary&#xff0c;括号里subquery 用到了临时表&#xff1a;derived &#xff08;2&#xff09;explain之type介绍 trpe反映的结果与我们sql是否优化过&#xff0c;是否…

从零起步:学习数据结构的完整路径

文章目录 1. 基础概念和前置知识2. 线性数据结构3. 栈和队列4. 树结构5. 图结构6. 散列表和哈希表7. 高级数据结构8. 复杂性分析和算法设计9. 实践和项目10. 继续学习和深入11. 学习资源12. 练习和实践 &#x1f389;欢迎来到数据结构学习专栏~从零起步&#xff1a;学习数据结构…

电气特征分析(ESA)技术是什么及有何应用场景

在现代工业领域&#xff0c;电机扮演着不可或缺的角色&#xff0c;驱动着生产设备的正常运行。然而&#xff0c;电机的故障可能会导致生产中断、计划外停机以及高昂的经济损失。为了保障生产的连续性和效率&#xff0c;预测性维护变得至关重要。在这个背景下&#xff0c;电气特…

HTML基础--标签

目录 列表标签 有序列表 type属性 有序列表嵌套 无序列表 type属性 无序列表嵌套 常见应用场景 表格标签 表格展示效果 表格属性 表格单元格合并 单元格合并属性 列表标签 HTL作为构建网页内容的标记语言&#xff0c;提供了多种列表标签&#xff0c;用于在网页中展…

如何使用CSS实现一个3D旋转效果?

聚沙成塔每天进步一点点 ⭐ 专栏简介⭐ 3D效果实现⭐ 写在最后 ⭐ 专栏简介 前端入门之旅&#xff1a;探索Web开发的奇妙世界 记得点击上方或者右侧链接订阅本专栏哦 几何带你启航前端之旅 欢迎来到前端入门之旅&#xff01;这个专栏是为那些对Web开发感兴趣、刚刚踏入前端领域…

实战教学:农产品小程序商城的搭建与运营

随着移动设备的普及和互联网技术的发展&#xff0c;小程序商城已经成为农产品销售的一种新兴渠道。本文将以乔拓云网为平台&#xff0c;详细介绍如何搭建和运营农产品小程序商城。 步骤一&#xff1a;登录乔拓云网后台 首先&#xff0c;进入乔拓云网站后台&#xff0c;找到并点…

Vite打包性能优化及填坑

最近在使用 Vite4.0 构建一个中型前端项目的过程中&#xff0c;遇到了一些坑&#xff0c;也做了一些项目在构建生产环境时的优化&#xff0c;在这里做一个记录&#xff0c;以便后期查阅。(完整配置在后面) 上面是dist文件夹的截图&#xff0c;里面的内容已经有30mb了&#xff…

LNMP 平台搭建(四十)

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 目录 前言 搭建LNMP 一、安装Nginx 二、安装Mysql 三、安装PHP 四、部署应用 前言 LNMP平台指的是将Linux、Nginx、MySQL和PHP&#xff08;或者其他的编程语言&#xff0c;如…

成都智慧企业发展研究院总经理郑小华:践行双轮驱动,为能源电力数智化注入新活力丨数据猿专访...

大数据产业创新服务媒体 ——聚焦数据 改变商业 随着全球经济走向数字化&#xff0c;中国正处于这一浪潮的前沿&#xff0c;进行前所未有的技术与产业深度融合。政府在2023年2月印发的《数字中国建设整体布局规划》等政策下&#xff0c;明确展示了对数字经济的支持与鼓励&…

基于“R语言+遥感“水环境综合评价方法教程

详情点击链接&#xff1a;基于"R语言遥感"水环境综合评价方法教程 一&#xff1a;R语言 1.1 R语言特点&#xff08;R语言&#xff09; 1.2 安装R&#xff08;R语言&#xff09; 1.3 安装RStudio&#xff08;R语言&#xff09; &#xff08;1&#xff09;下载地址…

Android studio实现圆形进度条

参考博客 效果图 MainActivity import androidx.appcompat.app.AppCompatActivity; import android.graphics.Color; import android.os.Bundle; import android.widget.TextView;import java.util.Timer; import java.util.TimerTask;public class MainActivity extends App…

rknn_toolkit以及rknpu环境搭建-rv1126

rknn_toolkit安装------------------------------------------------------------------------------- 环境要求&#xff1a;ubutu18.04 建议使用docker镜像 安装docker 参考https://zhuanlan.zhihu.com/p/143156163 镜像地址 百度企业网盘-企业云盘-企业云存储解决方案-同…

Vue3.0 新特性以及使用变更总结

Vue3.0 在2020年9月正式发布了&#xff0c;也有许多小伙伴都热情的拥抱Vue3.0。去年年底我们新项目使用Vue3.0来开发&#xff0c;这篇文章就是在使用后的一个总结&#xff0c; 包含Vue3新特性的使用以及一些用法上的变更。 图片.png 为什么要升级Vue3 使用Vue2.x的小伙伴都熟悉…

【python爬虫】4.爬虫实操(菜品爬取)

文章目录 前言项目&#xff1a;解密吴氏私厨分析过程代码实现&#xff08;一&#xff09;获取与解析提取最小父级标签一组菜名、URL、食材写循环&#xff0c;存列表 代码实现&#xff08;二&#xff09;复习总结 前言 上一关&#xff0c;我们学习了用BeautifulSoup库解析数据和…

【Linux】基础IO

目录 一、回顾C语言文件操作二、文件系统调用接口1. open2.write3.read 三、文件描述符四、重定向1.输出重定向2.输入重定向 五、dup2 一、回顾C语言文件操作 1 #include<stdio.h>2 #include<stdlib.h>3 4 #define LOG "log.txt"5 6 int main()7 {8 //…

nacos闪退等环境问题解决

nacos闪退&#xff1a;通常是jdk环境变量配置有问题&#xff0c;nacos获取不到环境变量所以闪退。因为nacos的启动文件会获取JAVA_HOME&#xff0c;如果配置的不对&#xff0c;会直接闪退。如图所示&#xff0c;nacos启动文件最开始就是获取环境变量&#xff0c;获取不到就提示…

7.Redis-list

list list常用命令lpushlrangelpushxrpushrpushxlpop / rpoplindexlinsertllenlremltrimlset 阻塞版本命令blpop/brpop 总结内部编码应用场景使用redis作为消息队列 redis中的 list 是一个双端队列, list 相当于是数组或者顺序表。list 并非是一个简单的数组&#xff0c;而是更…

宠物赛道,用AI定制宠物头像搞钱项目教程

今天给大家介绍一个非常有趣&#xff0c;而粉丝价值又极高&#xff0c;用AI去定制宠物头像或合照的AI项目。 接触过宠物行业应该知道&#xff0c;获取1位铲屎官到私域&#xff0c;这类用户的价值是极高的&#xff0c;一个宠物粉&#xff0c;是连铲个屎都要花钱的&#xff0c;每…

context.WithCancel()的使用

“ WithCancel可以将一个Context包装为cancelCtx,并提供一个取消函数,调用这个取消函数,可以Cancel对应的Context Go语言context包-cancelCtx[1] 疑问 context.WithCancel()取消机制的理解[2] 父母5s钟后出门&#xff0c;倒计时&#xff0c;父母在时要学习&#xff0c;父母一走…