【C++】多态and多态原理

news2025/1/10 20:58:00

目录

一、多态的概念

二、多态的定义及实现

🌟多态的构成条件

🌟虚函数

🌟虚函数的重写

🌠小贴士:

🌟C++11 override 和 final

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

三、抽象类

🌟概念

🌟接口继承和实现继承

四、多态的原理

🌟虚函数表

🌟多态的原理 

🌟动态绑定与静态绑定

🌠小贴士:


一、多态的概念

多种形态,去完成某个行为,当不同的对象去完成时会产生出不同的状态。

二、多态的定义及实现

🌟多态的构成条件

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

继承中构成多态的两个条件(指向谁调用谁)

1、必须通过基类的指针或者引用调用虚函数;

2、被调用的函数必须是虚函数(对象都不行),且派生类必须对基类的虚函数进行重写;

🌟虚函数

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

class A
{
public:
	virtual void test()
	{ 
		cout << "A : test()" << endl;
	} 
}; 

🌟虚函数的重写

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

#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;
	Student s;

	Func(p);
	Func(s);

	return 0;
}

 注意:

在重写基类虚函数时,派生类的虚函数在不加 virtual 关键字时,虽然也可以构成重写(因为继承后,基类的虚函数被继承下来了,在派生类依旧保持虚函数属性),但是改种写法不是很规范,不建议这样使用。

函数重写的两个例外:

<1>协变(基类与派生类虚函数返回值类型不同)

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

class A
{};

class B : public A
{};

class Person
{
public:
	virtual A* f()
	{
		return new A;
	}
};

class Student : public Person
{
public:
	virtual B* f()
	{
		return new B;
	}
};

<2>析构函数的重写(基类与派生类析构函数的名字不同) 

       如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加 virtual 关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。函数名不相同,看起来违背了重写的规则,其实不然,在这可以理解为编译器对析构函数的名称做了特殊处理,编译析构函数的名称统一处理成 destructor

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

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

//只有派生类Student的析构函数重写了Person的析构函数,
//下面的delete对象调用析构函数,才能构成多态
//才能保证p1和p2指向的对象正确的调用析构函数
int main()
{
	Person* p1 = new Person;
	Person* p2 = new Student;

	delete p1;
	delete p2;

	return 0;
}
🌠小贴士:

<1> 多态调用,看指向对象类型,指向谁调用谁的虚函数;

       普通调用,看调用者的类型,调用调用者的函数;

<2> 重写是一种特殊的隐藏;

       隐藏:不符合多态就是隐藏。

<3>对于普通对象,写不写成虚函数都不受影响:

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

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

int main()
{
	Student s;//加不加 virtaul 普通对象都不受影响 
	          //析构先调用子类析构,在调用父类析构
	          //所以说在子类/派生类结束了以后会自动调用父类的析构
	          //派生类里面有一个父类对象

	return 0;
}

<4>为什么析构函数一定建议设计成虚函数?

只有派生类的析构函数重写了基类的析构函数,delete对象调用析构函数才能构成多态,才能保证指向的对象能正确的调用析构函数。

//加 virtual 变成多态,此时指向子类,调用子类;指向父类,调父类
//析构是先析构子类,再自动调用父类的析构
//如果不加虚函数,就是直接调用父类,
// 这样子的话,子类的析构就没有调用到,就会产生内存泄露
class Person {
public:
	virtual ~Person()
	{
		cout << "~Person()" << endl;
	}
};

class Student : public Person {
public:
	virtual ~Student()
	{
		delete _ptr;
		cout << "~Student():" << _ptr << endl;
	}
protected:
	int* _ptr = new int[10];
};
//内存泄漏
//没有调到派生类的析构函数( *_ptr 这个指针没有被释放)

int main()
{
	// 只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函
	//数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。


	Person* p1 = new Student;//指向子类调用子类的析构,最后默认调用父类的析构
	delete p1;

	Person* p2 = new Person;//指向父类,调用父类的析构,
	delete p2;              //但是不会自动调用子类的析构,就会有可能参数子类的内存泄漏

	//因此析构函数一定建议设计成虚函数,在继承里面,尤其是基类的析构函数
	//因为只有当基类的析构函数设计成虚函数,派生类不管加不加virtual,派生类都构成了重写
	//构成了重写后,才能正常去选择调用子类/父类的析构,才能符合指向谁调用谁
	return 0;
}

🌟C++11 override 和 final

C++对函数重写的要求比较严格,这两个关键字可以帮助用户检测是否重写。

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

class Person {
public:
	virtual ~Person()  final //不想被完成重写,此时子类就不能被重写
	{
		cout << "~Person()" << endl;
	}
};

class Student : public Person {
public:
	~Student() //报错,不能完成重写
	{
		delete _ptr;
		cout << "~Student():" << _ptr << endl;
	}
protected:
	int* _ptr = new int[10];
};


int main()
{

	Person* p1 = new Student;
	delete p1;

	Person* p2 = new Person;
	delete p2;

	return 0;
}

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

class Person {
public:
	 ~Person()//没有完成重写
	{
		cout << "~Person()" << endl;
	}
};

class Student : public Person {
public:
	 ~Student() override //检查派生类是否完成重写,没有完成就报错
	{
		delete _ptr;
		cout << "~Student():" << _ptr << endl;
	}
protected:
	int* _ptr = new int[10];
};


int main()
{
	Person* p1 = new Student;
	delete p1;

	Person* p2 = new Person;
	delete p2;              

	return 0;
}

 🌠小贴士:

<1> final可修饰类,修饰的类叫最终类,语法规定,这个类不能被继承,继承就会报错。

class A final
{};

class B : public A//报错
{};

<2>把基类的构造函数私有,子类就生不成对象:

class A
{
private:
	A()
	{}
};

class B : public A//报错
{

};

int main()
{
	B bb;
	return 0;
}

此时可以把基类的构造函数写成静态成员,就可以进行构造:

class A
{
public:
	static A CreateObj()//此时就可以用构造
	{
		return A();
	}
private:
	A()
	{}
};

class B : public A
{};

int main()
{
	//B bb;
	A::CreateObj();
	return 0;
}

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

三、抽象类

🌟概念

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

在实践中,一个类型在现实没有实体对象,不想实例化出对象,设计成抽象类。

class Car
{
public:
	virtual void Drive() = 0;
};
//当父类是纯虚函数,派生类不想继承时,要重写虚函数
class Benz :public Car
{
public:
	virtual void Drive()//重写虚函数
	{
		cout << "Benz-舒适" << endl;
	}
};
class BMW :public Car
{
public:
	virtual void Drive()//重写虚函数
	{
		cout << "BMW-操控" << endl;
	}
};
int main()
{
	Benz bz;//不想继承父类的纯虚函数时,在派生类中重写虚函数

	Car* pBenz = new Benz;
	pBenz->Drive();
	Car* pBMW = new BMW;
	pBMW->Drive();

	return 0;
}

🌟接口继承和实现继承

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

四、多态的原理

🌟虚函数表

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

int main()
{
	Base b;
	cout << sizeof(Base) << endl;
	return 0;
}

• x86的环境下,通过观察测试我们发现 b 的对象是 8bytes除了_b成员,还多一个 _vfptr 放在对象的前面(注意有些平台可能放在对象的最后面,这跟平台有关),对象中的这个 _vfptr 我们叫做虚函数表指针(vvirtualf function。一个含有虚函数的类中至少都有一个虚函数表指针,因为虚函数的地址要被放到对象的最后面,这个跟平台有关)。

• 虚函数表 本质是一个函数指针数组。

• 一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。那派生类中这个表放了些什么呢?咱接着往下看:

🌟多态的原理 

// 针对上面的代码我们做出以下改造
// 1.我们增加一个派生类Derive去继承Base
// 2.Derive中重写Func1
// 3.Base再增加一个虚函数Func2和一个普通函数Func3
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;
};

void Func1(Base* p)
{
	//运行时绑定/动态绑定
	//运行时去虚表里面找到函数的地址,确认函数的地址,所以指向谁调用谁
	p->Func1();//有虚函数,放进虚表里面

	//普通的调用
	//编译时绑定/静态绑定 
	//即编译时,用函数名找,如果只有声明就链接的时候找,如果直接就有定义,就在编译的时候,用这个函数名,在符号表里面找这个函数的地址
	p->Func3();//没有虚函数,不放进虚表
}

int main()
{
	Base b;
	Derive d;
	Func1(&b);
	Func1(&d);


	//Func1(new Base);
	//Func1(new Derive);//指向的是子类当中父类的那一部分
	return 0;
}

 

 •  派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分是自己的成员。

 •  基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。

 •  另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函
数,所以不会放进虚表。

 •  虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr

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

    a.先将基类中的虚表内容拷贝一份到派生类虚表中;

    b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数       c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。

🌠注意:
虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是
他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。

🌟动态绑定与静态绑定

  满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象中去找的,不满足多态的函数调用时编译时就确认好的。

  静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态
比如:函数重载

  动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体
行为,调用具体的函数,也称为动态多态。

 🌠小贴士:

<1>指定类域后,还能实现多态吗? 不能。

原因:编译器在编译时,指定类域,就不会识别成多态调用,就不会按照运行时绑定的方式去生成指令,此时就跟指向的对象没有关系;

<2>为什么虚函数要放到虚表?

 运行的时候要去虚表里面找对应的虚函数,如果完成了重写就可以达到指向那个对象调用哪个对象;

<3>普通函数为什么不用放到虚表?

虚表时运行时才用的,普通函数是在编译的时候,通过函数名去确定地址,所以就支持了函数重载,按照函数名修饰规则,参数不同,修饰出来的函数名就不相同,就可以找到对应的地址;

函数名的地址有可能是在编译的时候找,也有可能是在链接的时候找,

在编译的时候,当前文件就用定义,编译好的时候当前文件就有这个函数的地址,编译就可以变成call的地址;

在链接的时候,只有声明,定义在其他.cpp,编译的时候时匹配的,再链接时才会拿修饰的函数名去其他文件的符号表里面找,找不到就会链接报错。

🌠小贴士:

<1> inline函数可以是虚函数吗?

     可以,不过编译器就忽略了inline属性,这个函数就不再是Inline,因为虚函数要放到虚表中去。

<2> 静态成员可以是虚函数吗?

       不能,因为静态成员函数没有this指针,使用 类型::成员函数 的调用方法无法访问虚函数表,所以静态成员函数无法放进虚函数表。

<3> 构造函数可以是虚函数吗?

      不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。

<4>析构函数可以是虚函数吗?什么场景下析构函数是虚函数?

      可以,并且最好把基类的析构函数定义成虚函数。

<5>对象访问普通函数快还是虚函数快?

     首先如果是普通对象,是一样快的。

     如果是指针对象或者引用对象,则调用普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。

<6>虚函数表是在什么阶段生成的?存在哪?

      虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的。

<7>什么是抽象类?抽象类的作用?

      抽象类强制重写了虚函数,另外抽象类体现了接口继承关系。

如若对你有帮助,记得点赞、收藏、关注哦!

若有误,望各位,在评论区留言或者私信我 指点迷津!!!谢谢^ ^ ~

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

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

相关文章

POD内的容器之间的资源共享

概述 摘要&#xff1a;本文通过实践描述并验证了pod内容器如何实现网络、文件、PID、UTC、mount的共享。 pod实战之容器内资源共享与隔离 container容器之间的共享实战 从实际场景说起&#xff1a;有2个容器nginx与wordpress分别运行了紧密耦合且需要共享资源的应用程序。我…

英语学习交流平台|基于java的英语学习交流平台系统小程序(源码+数据库+文档)

英语学习交流平台系统小程序 目录 基于java的英语学习交流平台系统小程序 一、前言 二、系统设计 三、系统功能设计 四、数据库设计 五、核心代码 六、论文参考 七、最新计算机毕设选题推荐 八、源码获取&#xff1a; 博主介绍&#xff1a;✌️大厂码农|毕设布道师&…

基于SpringBoot的校园社团活动管理系统设计与实现

文未可获取一份本项目的java源码和数据库参考。 一、设计&#xff08;论文&#xff09;研究背景与意义 在当今的社会&#xff0c;可以说是信息技术的发展时代&#xff0c;在社会的方方面面无不涉及到各种信息的处理。[1]信息是人们对客观世界的具体描述&#xff0c;是人们进行…

性能优化一:oracle 锁的原则

文章目录 锁的原则查看具体会话阻塞过程 锁的原则 1、只有被修改时,行才会被锁定。 2、当条语句修改了一条记录,只有这条记录上被锁定,在Oracle数据库中不存在锁升 3、当某行被修改时 &#xff0c;它将阻塞别人对它的修改。 4、当一个事务修改一行时.将在这个行上加上行锁(TX…

测试开发基础——测试用例的设计

三、测试用例的设计 1. 什么是测试用例 测试用例(Test Case)是为了实施测试而向被测试的系统提供的一组集合&#xff0c;这组集合包含&#xff1a;测试环境、操作步骤、测试数据、预期结果等要素。 设计测试用例原则一&#xff1a;测试用例中一个必需部分是对预期输出或结果进…

带你如何使用CICD持续集成与持续交付

目录 一、CICD是什么 1.1 持续集成&#xff08;Continuous Integration&#xff09; 1.2 持续部署&#xff08;Continuous Deployment&#xff09; 1.3 持续交付&#xff08;Continuous Delivery&#xff09; 二、git工具使用 2.1 git简介 2.2 git的工作流程 2.3 部署g…

【MRI基础】Partial volume 伪影

基本概念 partial volume 伪影是 MRI 中的一种常见伪影&#xff0c;当图像中的体素包含不同组织类型或结构的混合时就会出现这种伪影。这种伪影是由于成像系统的空间分辨率有限而产生的&#xff0c;导致具有不同信号强度的相邻结构在一个体素内混合在一起。 抑制MRI 中的parti…

图数据库的力量:深入理解与应用 Neo4j

图数据库的力量&#xff1a;深入理解与应用 Neo4j 文章目录 图数据库的力量&#xff1a;深入理解与应用 Neo4j1、什么是 Neo4j&#xff1f;版本说明 2、Neo4j 的部署和安装Neo4j Web 工具介绍 3、体验 Neo4j加载数据查询数据数据结构 4、Cypher 入门创建数据查询数据关系深度查…

Matlab simulink建模与仿真 第十五章(信号源库)

参考视频&#xff1a;simulink1.1simulink简介_哔哩哔哩_bilibili 一、信号源库中的模块概览 注&#xff1a;部分模块在第二章中有介绍&#xff0c;本章不再赘述。 二、from输入源模块 1、From Workspace模块 &#xff08;1&#xff09;该模块可从MATLAB工作区、模型工作区…

JVM 运行时数据区域

目录 前言 程序计数器 java虚拟机栈 本地方法栈 java堆 方法区 运行时常量池 前言 首先, java程序在被加载在内存中运行的时候, 会把他自己管理的内存划分为若干个不同的数据区域, 就比如你是一个你是一个快递员, 一堆快递过来需要你分拣, 这个时候, 你就需要根据投放的目…

数据稀缺条件下的时间序列微分:符号回归(Symbolic Regression)方法介绍与Python示例

时间序列概况在日常生活和专业研究中都很常见。简而言之,时间序列概况是一系列连续的数据点 y(0), y(1), …, y(t) ,其中时间 t 的点依赖于时间 t-1 的前一个点(或更早的时间点)。 在许多应用中,研究者致力于预测时间序列概况的未来行为。存在各种建模方法。这些模型通常基于过…

Django学习实战篇四(适合略有基础的新手小白学习)(从0开发项目)

前言&#xff1a; 在本章中&#xff0c;我们开始编写面向用户的界面&#xff0c;其中只涉及简单的HTML结构&#xff0c;不会做太多美化&#xff0c;目的就是把后台创建的数据展示到前台。 从技术上来讲&#xff0c;这一节将涉及Django 中function view和 class-based view 的用…

用Python实现时间序列模型实战——Day 22: LSTM 与 RNN 模型

一、学习内容 1. 长短期记忆网络 (LSTM) 的原理 LSTM&#xff08;长短期记忆网络&#xff09; 是一种专门用于处理时间序列数据的神经网络&#xff0c;它克服了传统 RNN 在处理长序列时出现的梯度消失问题。LSTM 通过引入 记忆单元 和 门控机制&#xff08;输入门、遗忘门、输…

Ruffle 继续在开源软件中支持 Adobe Flash Player

大多数人已经无需考虑对早已寿终正寝的 Adobe Flash 的支持&#xff0c;但对于那些仍有一些 Adobe Flash/SWF 格式的旧资产&#xff0c;或想重温一些基于 Flash 的旧游戏/娱乐项目的人来说&#xff0c;开源 Ruffle 项目仍是 2024 年及以后处理 Flash 的主要竞争者之一。 Ruffl…

【Hot100】LeetCode—4. 寻找两个正序数组的中位数

目录 1- 思路题目识别二分 2- 实现⭐4. 寻找两个正序数组的中位数——题解思路 3- ACM 实现 原题链接&#xff1a;4. 寻找两个正序数组的中位数 1- 思路 题目识别 识别1 &#xff1a;给定两个数组 nums1 和 nums2 &#xff0c;找出数组的中位数 二分 思路 将寻找中位数 —…

Python数据分析案例59——基于图神经网络的反欺诈交易检测(GCN,GAT,GIN)

以前的数据分析案例的文章可以参考&#xff1a;数据分析案例 案例背景 以前二维的表格数据的机器学习模型都做烂了&#xff0c;[线性回归,惩罚回归,K近邻,决策树,随机森林,梯度提升,支持向量机,神经网络]&#xff0c;还有现在常用的XGBoost&#xff0c;lightgbm&#xff0c;ca…

ffmpeg实现视频的合成与分割

视频合成与分割程序使用 作者开发了一款软件&#xff0c;可以实现对视频的合成和分割&#xff0c;界面如下&#xff1a; 播放时&#xff0c;可以选择多个视频源&#xff1b;在选中“保存视频”情况下&#xff0c;会将多个视频源合成一个视频。如果只取一个视频源中一段视频…

keil5进行stm32编程时常遇到的问题和ST-LINK在线仿真的连接问题

本文记录原因 最近一直在尝试usb的自定义键盘、无刷电机和pcb的一些东西&#xff0c;很久没使用stm32编写程序了。在浏览购物网站的时候发现很多便宜的小系统板。 使用小的系统板原因 1&#xff0c;在网上看到板子很便宜&#xff0c;以前很少看见&#xff0c;但现在网上对这…

大数据新视界 --大数据大厂之数据科学项目实战:从问题定义到结果呈现的完整流程

&#x1f496;&#x1f496;&#x1f496;亲爱的朋友们&#xff0c;热烈欢迎你们来到 青云交的博客&#xff01;能与你们在此邂逅&#xff0c;我满心欢喜&#xff0c;深感无比荣幸。在这个瞬息万变的时代&#xff0c;我们每个人都在苦苦追寻一处能让心灵安然栖息的港湾。而 我的…

uniapp 知识总结

1. uniapp 知识总结 uni-app是一个使用 Vue.js 开发所有前端应用的框架&#xff0c;开发者编写一套代码&#xff0c;可发布到iOS、Android、Harmony、Web&#xff08;响应式&#xff09;以及各种小程序&#xff08;微信/支付宝/百度/头条/飞书/QQ/快手/钉钉/淘宝&#xff09;、…