【C++】多态学习

news2025/1/16 1:49:23

多态

  • 多态的概念与定义
    • 多态的概念
    • 构成多态的两个条件
      • 虚函数与重写
        • 重写的两个特例
  • 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. 虚函数表是什么时候生成的?存在哪的?
    虚函数表是编译阶段就生成好的,存在于代码段(常量区)。所以一个类的不同对象共享该类的虚表。
    (构造函数初始化列表阶段初始化的是虚函数表指针,对象中存的也是虚函数表指针)

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

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

相关文章

JavaScript Web APIs -03 事件流、事件委托、其他事件(加载、滚动、尺寸)

Web APIs - 03 文章目录 Web APIs - 03事件流捕获和冒泡阻止冒泡 事件委托其他事件页面加载事件元素滚动事件页面尺寸事件 元素尺寸与位置 进一步学习 事件进阶&#xff0c;实现更多交互的网页特效&#xff0c;结合事件流的特征优化事件执行的效率 掌握阻止事件冒泡的方法理解事…

第五章 树与二叉树 四、线索树(手算与代码实现)

一、定义 1.线索树是一种二叉树&#xff0c;它在每个节点上增加了两个指针&#xff0c;分别指向其前驱和后继。 2.这些指针称为“线索”&#xff0c;因此线索树也叫做“线索化二叉树”。 3.在线索树中&#xff0c;所有的叶子节点都被线索化&#xff0c;使得遍历树的过程可以…

小程序实现图片上传、数量配置、预览、删除功能的开发指南

当谈到在小程序中实现图片上传、预览和删除等功能时,我们必须认识到这些功能对于提升用户体验和丰富应用的交互性非常关键。随着智能手机的普及,人们越来越习惯于通过图片来表达自己的想法、分享生活点滴,因此,使用户能够方便地在小程序中处理图片是非常重要的一步。 目录 …

MonoDETR: Depth-guided Transformer for Monocular 3D Object Detection 论文解读

MonoDETR论文解读 abstract 单目目标检测在自动驾驶领域&#xff0c;一直是一个具有挑战的任务。现在大部分的方式都是沿用基于卷积的2D 检测器&#xff0c;首先检测物体中心&#xff0c;后通过中心附近的特征去预测3D属性。 但是仅仅通过局部的特征去预测3D特征是不高效的&…

2023开学季中大许少辉著《乡村振兴战略下传统村落文化旅游设计》建筑畅销榜排名465位

2023开学季中大许少辉著《乡村振兴战略下传统村落文化旅游设计》建筑畅销榜排名465位

W5100S-EVB-PICO通过SNTP获取网络时间(十一)

前言 上一章我们用开发板进行ping测试&#xff0c;本章我们用它通过SNTP获取网络时间并在串口显示。 什么是SNTP? 能用来做什么? SNTP(Simple Network Time Protocal简单网络时间协议)&#xff0c;用于跨广域网或局域网同步时间的协议&#xff0c;具有较高的精确度&#xff…

PXE网络批量装机(centos7)

目录 前言 一、实验拓扑图 二、PXE的组件 三、配置PXE装机服务器 1、设置防火墙、selinux 2.安装、启动vsftp 3、拷贝系统文件到/var/ftp用于装机 4、配置tftp 5、准备pxelinx.0文件、引导文件、内核文件 6、配置本机IP 7、配置DHCP服务 8、创建default文件 四、配…

撤回IPO背后:透视树根互联“以退为进”的成长逻辑

如果说&#xff0c;互联网的上半场属于消费互联网&#xff0c;那么下半场的主角将会是工业互联网&#xff0c;它也被称为“第四次工业革命的重要基石”。 工业互联网属于典型的“长坡厚雪”型赛道&#xff0c;前期需要在技术、资金、人才等方面进行大量投入&#xff0c;而等待…

【强化学习】基本概念

基本大概框架 强化学习的主要角色是 智能体 &#xff08;agent&#xff09;和 环境,环境是智能体存在和互动的世界。智能体根据当前的环境做出action&#xff0c;action影响环境。然后智能体根据新的环境再进行action。 基础用语 状态&#xff08;state, s&#xff09;&…

Krahets 笔面试精选 88 题——40. 组合总和 II

使用深度搜索的方法&#xff1a; 由于题目说候选数组中的每个数字在每个组合只能出现一次&#xff0c;所以&#xff0c;为了避免重复&#xff0c;在开始之前对候选数组进行升序排序&#xff0c;这样优先选择小的数&#xff0c;如果当前的数都小于目标值&#xff0c;则后面的数就…

CSAPP的Lab学习——BombLab

文章目录 前言一、一号炸弹&#xff08;小试牛刀&#xff09;二、二号炸弹&#xff08;六重循环&#xff09;三、三号炸弹&#xff08;不同输入&#xff0c;不同答案&#xff09;四、四号炸弹&#xff08;判断语句的实现&#xff09;五、五号炸弹&#xff08;跳转&#xff0c;循…

VoxWeekly|The Sandbox 生态周报|20230828

欢迎来到由 The Sandbox 发布的《VoxWeekly》。我们会在每周发布&#xff0c;对上一周 The Sandbox 生态系统所发生的事情进行总结。 如果你喜欢我们内容&#xff0c;欢迎与朋友和家人分享。请订阅我们的 Medium 、关注我们的 Twitter&#xff0c;并加入 Discord 社区&#xf…

PHP多语言代入电商平台api接口采集拼多多根据ID获取商品详情原数据示例

拼多多商品详情原数据API接口的作用是获取拼多多电商平台上某一商品的详细信息&#xff0c;包括商品的标题、价格、库存、图片、描述、包邮信息、销量、评价、优惠券等数据。通过该API接口可以获取到商品的原始数据&#xff0c;用于分析、筛选和展示商品信息。 pinduoduo.item…

typeerror: web3 is not a constructor

typeerror: web3 is not a constructor

PXE批量装机

目录 目录 一、概念 二、PXE的组件 三、操作步骤 一、配置基础环境 二、安装软件 三、文件配置 一、DHCP配置 二、配置TFTP 三、pxelinx.0 四、新机器验证 一、第一次验证 二、第二次验证 五、配置pxe无人值守安装 六、无人值守验证 一、概念 PXE是批量装机系统…

上海亚商投顾:沪指震荡微跌 北向资金单月净卖出900亿

上海亚商投顾前言&#xff1a;无惧大盘涨跌&#xff0c;解密龙虎榜资金&#xff0c;跟踪一线游资和机构资金动向&#xff0c;识别短期热点和强势个股。 一.市场情绪 三大指数今日震荡调整&#xff0c;科创50走势较强&#xff0c;盘中一度涨超1%&#xff0c;随后上演冲高回落走…

找不到msvcp140.dll解决方法?msvcp140.dll修复教程

今天&#xff0c;我想和大家分享一个关于电脑问题的解决方法——找不到msvcp140.dll。这个问题在很多使用Windows系统的电脑上都可能出现&#xff0c;尤其是在使用某些软件时&#xff0c;可能会提示缺少msvcp140.dll文件。那么&#xff0c;我们该如何解决这个问题呢&#xff1f…

qt day

#include "widget.h" #include "ui_widget.h" void Widget::my_slot() {} Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget) {ui->setupUi(this);this->setWindowIcon(QIcon(":/wodepeizhenshi.png"));//設置窗口的…

光流法相关论文-LK光流法,HS光流法,Farneback光流法,FlowNet: 端到端的深度光流估计, RAFT: 结构化的光流估计

目录 光流法 1. Lucas-Kanade光流法&#xff08;稀疏光流法&#xff09;&#xff1a; 2. Horn-Schunck光流法&#xff08;稠密光流法&#xff09;&#xff1a; 3. Farneback光流法&#xff1a; 4 FlowNet: 端到端的深度光流估计&#xff1a; 5. RAFT: 结构化的光流…

CSP的理解与绕过

文章目录 前言CSP简介CSP如何工作CSP指令CSP指令值 例题[AFCTF 2021]BABY_CSP 前言 刚学习完xss&#xff0c;把xsss-labs靶场都通了打算试试水&#xff0c;遇到此题[AFCTF 2021]BABY_CSP&#xff0c;借此机会学习下CSP CSP简介 Content Security Policy (CSP)内容安全策略&am…