【C++ —— 多态】

news2024/10/6 22:25:49

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

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

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

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

相关文章

IntelliJ IDEA - Auto filling Java call arguments 插件教程

首先&#xff0c;安装该插件&#xff0c;下载完毕后重启 IDEA 当 userService 中方法需要参数的时候&#xff0c;我们一般都是自己手动写这些参数&#xff0c;是很费劲的。因此就出现了一个插件解决这类问题 Auto filling Java call arguments 光标点击需要填写参数的位置 Alt …

【CTF-Crypto】修复RSA证书入门汇总

证书修复 文章目录 证书修复基础知识Truncated 1Truncated 2Jumbled 基础知识 为什么要引入证书&#xff1f; 在正常题目中&#xff0c;大部分直接给出了数字&#xff0c;但是数字在现实世界中传输不稳定&#xff0c;容易在某处出现错误&#xff0c;所以我们将所有的数字信息…

【skill】远程连接的Win服务器,几分钟无操作就进入登录界面

远程连接的Win服务器&#xff0c;几分钟无操作就进入登录界面&#xff0c;这时候必须输入密码或者重新连接才能进到桌面 错误的解决方法&#xff1a; 与电源管理没半毛关系&#xff01;这是远程连接的Win服务器&#xff01; 根源是“屏保”的问题&#xff0c;所以正确、有效的…

【中断】【ARM64】学习总结

optee中的异常向量表解读–中断处理解读 https://mp.weixin.qq.com/s/gBsy4YDYTHGRsy2zcVr6Vg

一键切换ip地址的软件哪个好用

随着互联网的快速发展&#xff0c;IP地址的重要性日益凸显。它不仅代表着每台设备在网络上的唯一标识&#xff0c;还关乎到我们的网络隐私、访问权限以及工作效率。一键切换IP地址的软件应运而生&#xff0c;为我们提供了极大的便利。那么&#xff0c;在众多选择中&#xff0c;…

使用jdbc方式操作ClickHouse

1、创建测试表&#xff0c;和插入测试数据 create table t_order01(id UInt32,sku_id String,total_amount Decimal(16,2),create_time Datetime ) engine MergeTreepartition by toYYYYMMDD(create_time)primary key (id)order by (id,sku_id);insert into t_order01 values …

2024年UX/UI发展趋势

我的新书《Android App开发入门与实战》已于2020年8月由人民邮电出版社出版&#xff0c;欢迎购买。点击进入详情 2023年是科技创新的一年&#xff1a;我们见证了苹果虚拟眼镜的推出、人工智能驱动的衍生式设计的兴起以及三星的可折叠智能手机。网络现在融入了越来越多明亮且对比…

前沿科技应用:AIGC技术的广泛渗透

✨✨ 欢迎大家来访Srlua的博文&#xff08;づ&#xffe3;3&#xffe3;&#xff09;づ╭❤&#xff5e;✨✨ &#x1f31f;&#x1f31f; 欢迎各位亲爱的读者&#xff0c;感谢你们抽出宝贵的时间来阅读我的文章。 我是Srlua小谢&#xff0c;在这里我会分享我的知识和经验。&am…

【redis】redix在Linux下的环境配置和redis的全局命令

˃͈꒵˂͈꒱ write in front ꒰˃͈꒵˂͈꒱ ʕ̯•͡˔•̯᷅ʔ大家好&#xff0c;我是xiaoxie.希望你看完之后,有不足之处请多多谅解&#xff0c;让我们一起共同进步૮₍❀ᴗ͈ . ᴗ͈ აxiaoxieʕ̯•͡˔•̯᷅ʔ—CSDN博客 本文由xiaoxieʕ̯•͡˔•̯᷅ʔ 原创 CSDN 如…

Apache SSI远程命令执行漏洞

什么是SSI Apache SSI&#xff08;Server Side Include)&#xff0c;通常称为"服务器端嵌入"或者叫"服务器端包含"&#xff0c;是一种类似于ASP的基于服务器的网页制作技术。默认扩展名是 .stm、.shtm 和 .shtml。 从技术层面来讲&#xff0c;SSI是一种在静…

微服务架构下规范实践-研发管理

微服务架构下规范 目录概述需求&#xff1a; 设计思路实现思路分析3.代码托管 4.统计分析 参考资料和推荐阅读 Survive by day and develop by night. talk for import biz , show your perfect code,full busy&#xff0c;skip hardness,make a better result,wait for change…

机器学习:深入解析SVM的核心概念【四、软间隔与正则化】

软间隔与正则化 问题一&#xff1a;优化目标函数是如何得到的&#xff1f;得到的过程是怎样的&#xff1f;问题二&#xff1a;拉格朗日乘子法计算详细过程问题三&#xff1a;KKT条件求解过程问题四&#xff1a;结构风险最小化&#xff08;SRM&#xff09;的原理 在前面的讨论中…

【Linux系统编程】第十二弹---编辑器gcc/g++使用

✨个人主页&#xff1a; 熬夜学编程的小林 &#x1f497;系列专栏&#xff1a; 【C语言详解】 【数据结构详解】【C详解】【Linux系统编程】 目录 1、什么是gcc/g 2、gcc/g编辑器的安装 3、gcc/g编译的四个步骤 2.1、预处理 2.2、编译 2.3、汇编 2.4、链接 4、函数库 …

jadx-gui添加dex文件失败,提示Bad checksum

之前都是使用jadx直接打开的apk文件&#xff0c;要添加dex文件时突然添加失败并报错 也就是会校验dex的checksum&#xff0c;直接关闭会导致dex文件反编译失败。 解决方案&#xff1a;查日志后得到关闭校验即可 操作方式如下&#xff1a;

Java高阶私房菜:JVM分代收集算法介绍和各垃圾收集器原理分解

目录 什么是分代收集算法 GC的分类和专业术语 什么是垃圾收集器 垃圾收集器的分类及组合 ​编辑 应关注的核心指标 Serial和ParNew收集器原理 Serial收集器 ParNew收集器 Parallel和CMS收集器原理 Parallel 收集器 CMS收集器 新一代垃圾收集器G1和ZGC G1垃圾收集器…

未来科技的前沿:深入探讨人工智能的进展、机器学习技术和未来趋势

文章目录 一、人工智能的定义和概述1. 人工智能的基本概念2. 人工智能的发展历史 二、技术深入&#xff1a;机器学习、深度学习和神经网络1. 机器学习2. 深度学习3. 神经网络 三、人工智能的主要目标和功能1. 自动化和效率提升2. 决策支持和风险管理3. 个性化服务和预测未来 本…

【目标检测】DEtection TRansformer (DETR)

一、前言 论文&#xff1a; End-to-End Object Detection with Transformers 作者&#xff1a; Facebook AI 代码&#xff1a; DEtection TRansformer (DETR) 特点&#xff1a; 无proposal&#xff08;R-CNN系列&#xff09;、无anchor&#xff08;YOLO系列&#xff09;、无NM…

本地大语言模型LLM的高效运行专家 | Ollama

Ollama简介 Ollama是一个开源的大型语言模型服务工具&#xff0c;它帮助用户快速在本地运行大模型。通过简单的安装指令&#xff0c;用户可以执行一条命令就在本地运行开源大型语言模型&#xff0c;如Llama 2。Ollama极大地简化了在Docker容器内部署和管理LLM的过程&#xff0…

ICode国际青少年编程竞赛- Python-1级训练场-基本操作

ICode国际青少年编程竞赛- Python-1级训练场-基本操作 1、 Dev.step(3)2、 Dev.step(1)3、 Dev.step(7)4、 Dev.step(-1)5、 Dev.step(-5)6、 Dev.step(3) Dev.step(-8)7、 Dev.turnRight() Dev.step(1)8、 Dev.turnLeft() Dev.step(1)9、 Dev.step(4) Dev.tur…

2024年Q1季度户外装备线上市场数据分析:垂钓类用品占据市场主流

五一期间&#xff0c;随着外出旅游、露营、游玩增多&#xff0c;消费者也将目光投向户外装备。 但今年Q1季度&#xff0c;由于季节变换、天气气候等原因&#xff0c;户外装备市场表现不如预期。根据鲸参谋数据显示&#xff0c;今年Q1季度&#xff0c;在线上电商平台&#xff0…