【C++ —— 多态】

C++ —— 多态

  • 多态的概念
  • 多态的定义和实现
    • 多态的构成条件
    • 虚函数
    • 虚函数的重写
    • 虚函数重写的两个例外
        • 协变:
        • 析构函数的重写
    • C++11 override和final
    • 重载、覆盖(重写)、隐藏(重定义)的对比
  • 抽象类
    • 概念
    • 接口继承和实现继承
  • 多态的继承
    • 虚函数表
    • 多态的原理
    • 动态绑定和静态绑定
  • 单继承和多继承关系的虚函数表
    • 单继承中的虚函数表
    • 多继承中的虚函数表

多态的概念

在C++中, 多态性(Polymorphism) 是面向对象编程中的一个重要概念,它允许以统一的方式 处理 不同类型的对象 ,从而提高代码的灵活性和可扩展性。多态性基于继承和虚函数实现,主要有两种形式:编译时多态(静态多态)和运行时多态(动态多态)。

通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会 产生出不同的状态。

多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了Person。Person对象买票全价,Student对象买票半价。

多态演示:

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

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

void Func(Person* p)
{
	p->BuyTicket();
}

void test1()
{
	Person lt;
	Student hcx;

	Func(&lt);		// "全价买票"
	Func(&hcx);		// "半价买票"

}

多态的定义和实现

多态的构成条件

那么在继承中要构成多态还有两个条件:

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

虚函数

虚函数: 即被virtual修饰的类成员函数称为虚函数。

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

例如上述的 Person 类的 BuyTicket 函数被 virtual 修饰,所以他是一个虚函数!

注意: 这里的 virtual 和菱形继承那一块的 virtual 的作用不同,二者作用完全不同,只是关键字的名字一样而已。

虚函数的重写

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

代码演示:

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

class Student :public Person
{
	virtual void BuyTicket()		//派生类重写基类的虚函数
	{
		cout << "半价买票" << endl;
	}
};

注意: 这里的派生类的 virtual 关键字不写也构成虚函数的重写,但是这种写法并不推荐!

虚函数重写的两个例外

  1. 协变 (基类与派生类虚函数返回值类型不同)
  2. 析构函数的重写 (基类与派生类析构函数的名字不同)
协变:

派生类重写基类虚函数时,与基类虚函数返回值类型不同
即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变

//基类
class A {};

//派生类
class B : public A {};

//基类
class Person
{
public:
	virtual A* f()			//基类的f()函数返回值是A*
	{
		cout << "A* Person::f() " << endl;
		return new A;
	}
};

//派生类
class Student : public Person
{
public:
	virtual B* f()			//衍生类f()函数的重写,其返回值是B*
	{
		cout << "B* Student::f()" << endl;
		return new B;
	}
};

void Func(Person* p)
{
	p->f();
}

void test2()
{
	Person p;
	Student s;
	Person* ptr1 = &p;
	Person* ptr2 = &s;

	Func(ptr1);		//A* Person::f()
	Func(ptr2);		//B* Student::f()
}
析构函数的重写

在多态中,析构函数(destructor) 的调用有一个特殊的问题。如果不将基类的析构函数声明为虚函数(virtual),那么当使用 基类指针 删除 派生类对象 时,只会调用基类的析构函数而不会调用派生类的析构函数。 这可能导致派生类中的资源没有被正确释放,造成内存泄漏或其他问题。

类似下面这个问题:

class Person  
{  
public:  
    ~Person()  
    {  
        cout << "~Person()" << endl;  
    }  
};  
  
class Student : public Person  
{  
public:  
    ~Student()  
    {  
        cout << "~Student()" << endl;  
    }  
};  
  
void test3()  
{  
    Person* ptr1 = new Person;  
    Person* ptr2 = new Student;  
  
    delete ptr1; 	//~Person()
    // 调用 Person 的析构函数  
    
    delete ptr2; 	//~Person()
    // 如果 Person 的析构函数不是虚函数,则只调用 Person 的析构函数  
}

如果 Person 的析构函数不是虚函数,当 执行 delete ptr2; 时,只有Person 的析构函数会被调用,而 Student 的析构函数则不会被调用。但是 Student 类可能包含一些额外的资源(如动态分配的内存、打开的文件句柄等)需要清理。
为了避免这种情况,您应该将 Person 的析构函数声明为虚函数:

class Person  
{  
public:  
    virtual ~Person() // 声明为虚析构函数  
    {  
        cout << "~Person()" << endl;  
    }  
};

现在,当执行 delete ptr2; 时,首先会调用 Student 的析构函数(因为它被首先创建),然后调用 Person 的析构函数(因为它是基类)。这就是所谓的析构函数链(destructor chaining),它确保了对象的所有部分都被正确地清理。

C++11 override和final

从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写。

final:修饰虚函数,表示该虚函数不能再被重写。

比如下面的基类Car的虚函数Drive被final修饰后就不能再被重写了,子类若是重写了基类的Drivet函数则编译报错。

//基类
class Car
{
public:
virtual void Drive() final {}
};

//衍生类
class Benz :public Car
{
public:
virtual void Drive() {cout << "Benz-舒适" << endl;}
};

override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。

下面这个衍生类Drive()的成员函数被override所修饰,所以在编译时将检查是否派生类虚函数是否重写了基类的这个虚函数,没有的话就会报错。

//基类
class Car {
public:
	virtual void Drive() {}
};

//衍生类
class Benz :public Car {
public:
	virtual void Drive() override
	{
		cout << "Benz-舒适" << endl;
	}
};

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

在这里插入图片描述

抽象类

概念

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

class Car						//包含纯虚函数所以叫抽象类
{
public:
	virtual void Drive() = 0	//纯虚函数
	{}
};

class BMW :public Car
{
	void Drive()				//派生类必须重写纯虚函数
	{
		cout << "操控" << endl;
	}
};

class Benz :public Car			
{
	void Drive()				//派生类必须重写纯虚函数
	{
		cout << "舒适" << endl;
	}
};

void Func(Car* c)
{
	c->Drive();
}

void test1()
{
	BMW b1;
	Benz b2;
	Car* c1 = &b1;				
	Car* c2 = &b2;				
	Func(c1);					//操控
	Func(c2);					//舒适

	cout << endl;
	c1->Drive();
	c2->Drive();
}

这里的Car类中包含了纯虚函数 Drive ,所以 Car类 是抽象类,其衍生类必须重写纯虚函数,才能实例化对象。

接口继承和实现继承

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

多态的继承

虚函数表

先来看一道面试题:
这里的 sizeof(Base) 等于多少呢?

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

答案是:

在这里插入图片描述

那为什么是12呢?

在这里插入图片描述

b对象当中除了_b成员和_ch外,实际上还有一个_vfptr放在对象的前面(有些平台可能会放到对象的最后面,这个跟平台有关)。对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。

在这里插入图片描述

那这个虚函数表指针的原理是什么呢?

下面Base类当中有三个成员函数,其中Func1和Func2是虚函数,Func3是普通成员函数,子类Derive当中仅对父类的Func1函数进行了重写。

#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:
	//重写虚函数Func1
	virtual void Func1()
	{
		cout << "Derive::Func1()" << endl;
	}
private:
	int _d = 2;
};
int main()
{
	Base b;
	Derive d;
	return 0;
}

通过调试可以发现,父类对象b和基类对象d当中除了自己的成员变量之外,父类和子类对象都有一个虚表指针,分别指向属于自己的虚表。

在这里插入图片描述
通过对调试的观察,我们发现这几个小点:

  1. 实际上这个虚表存储的就是虚函数的地址,可以通过观察b对象的虚表内容可知。方便存储的是Func1Func2的地址。
  2. 派生类对象d中也有一个虚表。这个虚表指针实际上是就是继承了基类的虚表,只不过在派生类对象d中重写了Func1所以,两个虚表就有所不同。所以d的虚表是继承了b的 Func2 地址和重写 Func1 地址。所以这也就是为什么函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
  3. 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表。此外,虚函数表本质是一个存虚函数指针的指针数组,一般情况下会在这个数组最后放一个nullptr

总结一下派生类的虚表生成:

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

多态的原理

还记得之前实现的Func函数传Person调用的Person::BuyTicket,传Student调用的是Student::BuyTicket
在这里插入图片描述

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 Mike;
	Func(Mike);
	Student Johnson;
	Func(Johnson);
	return 0;
}

观察下图的红色箭头我们看到,p是指向mike对象时,p->BuyTicketmike的虚表中找到虚函数是Person::BuyTicket。观察下图的蓝色箭头我们看到,p是指向johnson对象时,p->BuyTicketjohson的虚表中找到虚函数是Student::BuyTicket
这样就实现出了不同对象去完成同一行为时,展现出不同的形态。
在这里插入图片描述

动态绑定和静态绑定

  1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
//父类
class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票-全价" << endl;
	}
};
//子类
class Student : public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票-半价" << endl;
	}
};

void test2()
{
	Student s;		//实例化对象s
	Person p = s;	//切片
	p.BuyTicket();	//通过对象直接调用,不满足多态,即静态绑定

}

在这里插入图片描述
此时直接调用函数,不满足多态条件,不构成多态,通过反汇编观察为静态绑定。

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

观察下图中的监视窗口中我们发现看不见func3和func4。这里是编译器的监视窗口故意隐藏了这两个函数,也可以认为是他的一个小bug。那么我们如何查看d的虚表呢?下面我们使用代码打印出虚表中的函数。

typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
	// 依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数
	cout << " 虚表地址>" << vTable << endl;
	for (int i = 0; vTable[i] != nullptr; ++i)
	{
		printf(" 第%d个虚函数地址 :0X%p,->", i, vTable[i]);
		VFPTR f = vTable[i];
		f();
	}
	cout << endl;
}
void test3 ()
{
	Base b;
	Derive d;
	
	VFPTR * vTableb = (VFPTR*)(*(int*)&b);
	PrintVTable(vTableb);
	VFPTR* vTabled = (VFPTR*)(*(int*)&d);
	PrintVTable(vTabled);
}

在这里插入图片描述

多继承中的虚函数表

class Base1 {
public:
	virtual void func1() { cout << "Base1::func1" << endl; }
	virtual void func2() { cout << "Base1::func2" << endl; }
private:
	int b1;
};
class Base2 {
public:
	virtual void func1() { cout << "Base2::func1" << endl; }
	virtual void func2() { cout << "Base2::func2" << endl; }
private:
	int b2;
};
class Derive : public Base1, public Base2 {
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }
private:
	int d1;
};
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;
}

void test4()
{
	Derive d;
	VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
	PrintVTable(vTableb1);
	VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));
	PrintVTable(vTableb2);
}

观察下图可以看出:多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中。
在这里插入图片描述

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

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

相关文章

VTK 的可视化方法:Glyph

VTK 的可视化方法&#xff1a;Glyph VTK 的可视化方法&#xff1a;Glyph标量、向量、张量将多边形数据的采集点法向量标记成锥形符号参考 VTK 的可视化方法&#xff1a;Glyph 模型的法向量数据是向量数据&#xff0c;因此法向量不能像前面讲到的通过颜色映射来显示。但是可以通…

25 JavaScript学习:var let const

JavaScript全局变量 JavaScript中全局变量存在多种情况和定义方式&#xff0c;下面详细解释并提供相应的举例&#xff1a; 使用var关键字声明的全局变量&#xff1a; var globalVar "我是全局变量";未使用var关键字声明的变量会成为全局变量&#xff08;不推荐使用&…

【前端】-【防止接口重复请求】

文章目录 需求实现方案方案一方案二方案三 需求 对整个的项目都做一下接口防止重复请求的处理 实现方案 方案一 思路&#xff1a;通过使用axios拦截器&#xff0c;在请求拦截器中开启全屏Loading&#xff0c;然后在响应拦截器中将Loading关闭。 代码&#xff1a; 问题&…

(刷题记录2)随机链表的复制

[刷题记录2]随机链表的复制 题目信息&#xff1a;题目思路(环境来自力扣OJ的C语言)&#xff1a;复杂度&#xff1a;代码和解释&#xff1a;1.遍历一遍原链表的同时&#xff0c;在每个原节点后面插入一个相同的新节点&#xff0c;共插入 n 个新节点。2.利用两者联系&#xff0c;…

神奇的Vue3 - 组件探索

神奇的Vue3 第一章 神奇的Vue3—基础篇 第二章 神奇的Vue3—Pinia 文章目录 神奇的Vue3了解组件一、注册组件1. 全局注册​2. 局部注册3. 组件命名 二、属性详解1. Props&#xff08;1&#xff09;基础使用方法&#xff08;2&#xff09;数据流向&#xff1a;单项绑定原则&…

ThreeJS:Mesh网格与三维变换

Mesh网格 ThreeJS中&#xff0c;Mesh表示基于以三角形为多边形网格(polygon mesh)的物体的类&#xff0c;同时也作为其它类的基类。 通过Mesh网格&#xff0c;我们可以组合Geometry几何体与Material材质属性&#xff0c;在3D世界中&#xff0c;定义一个物体。例如&#xff1a;之…

vue2(4)之scoped解决样式冲突/组件通信/非父子通信/ref和$refs/异步更新/.sync/事件总线/provide和inject

vue2 一、学习目标1.组件的三大组成部分&#xff08;结构/样式/逻辑&#xff09;2.组件通信3.综合案例&#xff1a;小黑记事本&#xff08;组件版&#xff09;4.进阶语法 二、scoped解决样式冲突**1.默认情况**&#xff1a;2.代码演示3.scoped原理4.总结 三、data必须是一个函数…

Copilot Venture Studio創始合伙人楊林苑確認出席“邊緣智能2024 - AI開發者峰會”

隨著AI技術的迅猛發展&#xff0c;全球正逐步進入邊緣計算智能化與分布式AI深度融合的新時代&#xff0c;共同書寫著分布式智能創新應用的壯麗篇章。邊緣智能&#xff0c;作為融合邊緣計算和智能技術的新興領域&#xff0c;正逐漸成為推動AI發展的關鍵力量。借助分布式和去中心…

由于找不到mfc140u.dll,无法继续执行的多种解决方法

在我们日常与计算机的密切互动中&#xff0c;或许不少用户都曾遇到过这样一个棘手的问题&#xff1a;系统突然弹出一个提示窗口&#xff0c;告知我们“找不到mfc140u.dll文件”。这个文件是Microsoft Foundation Class&#xff08;MFC&#xff09;库的一部分&#xff0c;用于支…

提升编码技能:学习如何使用 C# 和 Fizzler 获取特价机票

引言 五一假期作为中国的传统节日&#xff0c;也是旅游热门的时段之一&#xff0c;特价机票往往成为人们关注的焦点。在这个数字化时代&#xff0c;利用爬虫技术获取特价机票信息已成为一种常见的策略。通过结合C#和Fizzler库&#xff0c;我们可以更加高效地实现这一目标&…

20240502在WIN10下给X99平台上的M6000显卡安装驱动程序

20240502在WIN10下给X99平台上的M6000显卡安装驱动程序 2024/5/2 9:32 人工智能计算领域的领导者 | NVIDIA https://www.nvidia.cn/ C:\NVIDIA\DisplayDriver\552.22\Win11_Win10-DCH_64\International IMPORTANT NOTICE -- READ CAREFULLY: -------------------------------…

pmp培训机构哪个比较好,求推荐-

寻找一个自己认为比较好的PMP培训机构千万不要盲目&#xff0c;先在网上看看大家都推荐什么&#xff0c;看一下各个机构的老学员反馈&#xff0c;这些对我们的选择有非常大的帮助&#xff0c;最起码有了一些风评上的参考&#xff0c;现状就是目前线上机构的竞争比较大&#xff…

c语言从入门到函数速成(1)

温馨提醒&#xff1a;本篇文章适合人群&#xff1a;刚学c又感觉那个地方不怎么懂的同学以及以及学了一些因为自身原因停学一段时间后又继续学c的同学 好&#xff0c;正片开始。 主函数 学c时最先学的是我们c语言程序的主体函数&#xff0c;c的主函数有两种写法&#xff0c;这…

【JavaEE】Thread的方法和属性

文章目录 1、Thread的常见构造方法2、Thread的几个常见属性2.1 ID2.2 名称2.3 状态2.4 优先级2.5 是否后台线程2.6 是否存活2.7 是否被中断 3.补充说明3.1 Thread.sleep()的作用3.2 Thread.sleep()的异常处理方式 1、Thread的常见构造方法 方法说明Thread()创建线程对象Thread…

动态规划-子序列问题1

文章目录 1. 最长递增子序列&#xff08;300&#xff09;2. 摆动序列&#xff08;376&#xff09;3. 最长递增子序列的个数&#xff08;673&#xff09;4. 最长数对链&#xff08;646&#xff09; 1. 最长递增子序列&#xff08;300&#xff09; 题目描述&#xff1a; 状态表…

Linux 进程间通信之命名管道

&#x1f493;博主CSDN主页:麻辣韭菜&#x1f493;   ⏩专栏分类&#xff1a;Linux知识分享⏪   &#x1f69a;代码仓库:Linux代码练习&#x1f69a;   &#x1f339;关注我&#x1faf5;带你学习更多Linux知识   &#x1f51d; 目录 前言 命名管道 创建一个命名管道 …

LeetCode题练习与总结:删除排序链表中的重复元素--83

一、题目描述 给定一个已排序的链表的头 head &#xff0c; 删除所有重复的元素&#xff0c;使每个元素只出现一次 。返回 已排序的链表 。 示例 1&#xff1a; 输入&#xff1a;head [1,1,2] 输出&#xff1a;[1,2]示例 2&#xff1a; 输入&#xff1a;head [1,1,2,3,3] 输…

袁庭新ES系列17节|Spring Data Elasticsearch基础

前言 为了简化对Elasticsearch的操作Spring Data提供了Spring Data Elasticsearch。Spring Data Elasticsearch是Spring Data技术对Elasticsearch原生API封装之后的产物&#xff0c;它通过对原生API的封装&#xff0c;使得程序员可以简单的对Elasticsearch进行各种操作。接下来…

InfluxDB安装使用介绍

1.介绍 InfluxDB是一个由InfluxData开发的开源时序型数据。它由Go写成&#xff0c;着力于高性能地查询与存储时序型数据。InfluxDB被广泛应用于存储系统的监控数据&#xff0c;IoT行业的实时数据等场景。 2.对常见关系型数据库&#xff08;MySQL&#xff09;的基础概念对比 1…

满上! —— 十年之约#22(ROI 48%)

原创 | 刘教链 空头在忍耐了很久之后&#xff0c;趁五一劳动节东方放假发动突袭&#xff0c;把BTC&#xff08;比特币&#xff09;打到6万刀以下。这使得我们终于终结了7个月七连涨的趋势&#xff0c;确定4月以收跌结束。 4月开盘70k&#xff0c;最高72.8k&#xff0c;最低59.6…