C++多态与虚函数的原理与关系

news2024/11/27 13:16:42

C++多态

多态可以分为编译时的多态和运行时的多态。前者主要是指 函数的重载(包括运算符的重载)、对重载函数的调用,在编译时就能根据实参确定应该调用哪个函数,因此叫编译时的多态;而后者则和继承、虚函数等概念有关。

有了虚函数,基类指针指向基类对象时就使用基类的成员(包括 成员函数 和 成员变量 ),指向派生类对象时就使用派生类的成员。换句话说,基类指针可以按照基类的方式来做事,也可以按照派生类的方式来做事,它有多种形态,或者说有多种表现方式,我们将这种现象称为多态(Polymorphism)。

C++ 提供多态的目的是:可以通过基类指针对所有派生类(包括直接派生和间接派生)的成员变量和成员函数进行 “全方位” 的访问,尤其是成员函数。如果没有多态,我们只能访问成员变量。

C++多态的使用

#include <iostream>
#include <cstring>
using namespace std;
//Person类
class Person
{
public:
    Person(string name, int age):m_name(name),m_age(age)
    {
	}
	void info()
	{
		cout << "Call Person info, Name = " << this->m_name << " Age = " << this->m_age << endl;
	}
protected:
    string m_name;
    int m_age;
};
class Student:public Person
{
public:
    Student(string name, int age, float score):Person(name, age),m_score(score)
    {
	}
	void info()
	{
		cout << "Call Student info, Name = " << this->m_name << " Age = " << this->m_age << " Score = " << m_score << endl;
	}
protected:
    float m_score;
};
int main()
{
	Person *person = new Person("Bob", 18);
    person->info();
   	Student *student = new Student("Bob", 18, 99);
    student->info();
	return 0;
}

运行结果

Call Person info, Name = Bob Age = 18
Call Student info, Name = Bob Age = 18 Score = 99

我们分别定义了一个 Person 类和一个 Student 类,Student 类继承自 Person 类,接着,在 main 函数里面,我们分别实例化了一个 Person 对象和一个 Student 对象。

最后,我们分别调用了 Person 类对象的 info 方法和 Student 类对象的 info 方法,我们发现,它们各自调用了自己的 info 函数,现在,我们用 Student 来实例化 Person 类,我们修改程序,如下

#include <iostream>
#include <cstring>
using namespace std;
//Person类
class Person
{
public:
    Person(string name, int age):m_name(name),m_age(age)
    {
	}
	void info()
	{
		cout << "Call Person info, Name = " << this->m_name << " Age = " << this->m_age << endl;
	}
protected:
    string m_name;
    int m_age;
};
class Student:public Person
{
public:
    Student(string name, int age, float score):Person(name, age),m_score(score)
    {
	}
	void info()
	{
		cout << "Call Student info, Name = " << this->m_name << " Age = " << this->m_age << " Score = " << m_score << endl;
	}
protected:
    float m_score;
};
int main()
{
	Person *person = new Person("Bob", 18);
    person->info();
    person = new Student("Bob", 18, 99);
    person->info();
	return 0;
}

运行结果

Call Person info, Name = Bob Age = 18
Call Person info, Name = Bob Age = 18

这次,我们用 Student 类实例化了 Person 类,最终调用 info 函数,此时的 info 函数还是调用的 Person 类的,这不是我们想要的效果,我们期望的是还是调用 Student 类的 info 函数。

也就是说通过基类指针只能访问派生类的成员变量,但是不能访问派生类的成员函数。为了能让基类指针访问派生类的成员函数,C++ 增加了虚函数(Virtual Function),现在,我们将 info 函数声明为虚函数,修改程序如下

#include <iostream>
#include <cstring>
using namespace std;
//Person类
class Person
{
public:
    Person(string name, int age):m_name(name),m_age(age)
    {
	}
	virtual void info()
	{
		cout << "Call Person info, Name = " << this->m_name << " Age = " << this->m_age << endl;
	}
protected:
    string m_name;
    int m_age;
};
class Student:public Person
{
public:
    Student(string name, int age, float score):Person(name, age),m_score(score)
    {
	}
	virtual void info()
	{
		cout << "Call Student info, Name = " << this->m_name << " Age = " << this->m_age << " Score = " << m_score << endl;
	}
protected:
    float m_score;
};
int main()
{
	Person *person = new Person("Bob", 18);
    person->info();
    person = new Student("Bob", 18, 99);
    person->info();
	return 0;
}

运行结果

Call Person info, Name = Bob Age = 18
Call Student info, Name = Bob Age = 18 Score = 99

多态构成条件

  1. 必须存在继承关系;
  2. 继承关系中必须有同名的虚函数,并且它们是覆盖关系(函数原型相同)
  3. 存在基类的指针,通过该指针调用虚函数。

在 C++ 中,多态 的实现,除了可以使用子类的指针指向父类的对象之外,还可以通过引用来实现多态,不过引用不像 指针 灵活,指针可以随时改变指向,而引用只能指代固定的对象,在多态性方面缺乏表现力

#include <iostream>
#include <cstring>
using namespace std;
//Person类
class Person
{
public:
    Person(string name, int age):m_name(name),m_age(age)
    {
	}
	virtual void info()
	{
		cout << "Call Person info, Name = " << this->m_name << " Age = " << this->m_age << endl;
	}
protected:
    string m_name;
    int m_age;
};
class Student:public Person
{
public:
    Student(string name, int age, float score):Person(name, age),m_score(score)
    {
	}
	virtual void info()
	{
		cout << "Call Student info, Name = " << this->m_name << " Age = " << this->m_age << " Score = " << m_score << endl;
	}
protected:
    float m_score;
};
int main()
{	
	Person person("Bob", 18);
	Student student("Bob", 20, 110);
	Person &rPerson = person;
    Person &rStudent = student;
    rPerson.info();
    rStudent.info();
	return 0;
}

由于引用类似于常量,只能在定义的同时初始化,并且以后也要从一而终,不能再引用其他数据,所以本例中必须要定义两个引用变量,一个用来引用基类对象,一个用来引用派生类对象。从运行结果可以看出,当基类的引用指代基类对象时,调用的是基类的成员,而指代派生类对象时,调用的是派生类的成员。

在 C++ 中,多态的实现,除了可以使用子类的指针指向父类的对象之外,还可以通过引用来实现多态,不过引用不像指针灵活,指针可以随时改变指向,而引用只能指代固定的对象,在多态性方面缺乏表现力。


C++虚函数

在 C++ 中,使用 virtual 关键字 修饰的 函数 被称为虚函数,虚函数对于 多态 具有决定性的作用,有虚函数才能构成多态。

  1. 只需要在虚函数的声明处加上 virtual 关键字,函数定义处可以加也可以不加。
  2. 为了方便,可以只将基类中的函数声明为虚函数,这样所有派生类中具有遮蔽关系的同名函数都将自动成为虚函数。
  3. 当在基类中定义了虚函数时,如果派生类没有定义新的函数来遮蔽此函数,那么将使用基类的虚函数。
  4. 只有派生类的虚函数覆盖基类的虚函数(函数原型相同)才能构成多态(通过基类指针访问派生类函数)。
  5. 构造函数 不能是虚函数。对于基类的构造函数,它仅仅是在派生类构造函数中被调用,这种机制不同于 继承。也就是说,派生类不继承基类的构造函数,将构造函数声明为虚函数没有什么意义。
  6. 析构函数 可以声明为虚函数,而且有时候必须要声明为虚函数。

虚函数使用

什么时候需要将函数声明为虚函数,首先看 成员函数 所在的类是否会作为基类。然后看成员函数在类的继承后有无可能被更改功能,如果希望更改其功能的,一般应该将它声明为虚函数。

如果成员函数在类被继承后功能不需修改,或派生类用不到该函数,则不要把它声明为虚函数。

#include <iostream>
#include <cstring>
using namespace std;
//Person类
class Person
{
public:
    Person(string name, int age):m_name(name),m_age(age)
    {
	}
	virtual void info()
	{
		cout << "Call Person info, Name = " << this->m_name << " Age = " << this->m_age << endl;
	}
protected:
    string m_name;
    int m_age;
};
class Student:public Person
{
public:
    Student(string name, int age, float score):Person(name, age),m_score(score)
    {
	}
	virtual void info()
	{
		cout << "Call Student info, Name = " << this->m_name << " Age = " << this->m_age << " Score = " << m_score << endl;
	}
protected:
    float m_score;
};
int main()
{
	Person *person = new Person("Bob", 18);
    person->info();
    person = new Student("Bob", 18, 99);
    person->info();
	return 0;
}

因为,我们不同的子类需要实现不同的 info 函数,所以,我们必须将 info 函数声明为虚函数,不然,没办法通过子类对象指向父类成员时,访问子类对象的 info 方法。


C++虚析构函数

在 C++ 中,使用 virtual 关键字 修饰的 函数 被称为 虚函数,C++ 的 构造函数 不可以被声明为虚函数,但 析构函数 可以被声明为虚函数,并且有时候必须将析构声明为虚函数。

用 C++ 开发的时候,用来做基类的类的析构函数一般都是虚函数。

虚析构函数的作用

虚析构函数是为了避免内存泄露,而且是当子类中会有指针 成员变量 时才会使用得到的。也就说虚析构函数使得在删除指向子类对象的基类指针时可以调用子类的析构函数达到释放子类中堆内存的目的,而防止内存泄露的。

当父类的析构函数不声明成虚析构函数的时候,当子类继承父类,父类的指针指向子类时,delete掉父类的指针,只调动父类的析构函数,而不调动子类的析构函数。

当父类的析构函数声明成虚析构函数的时候,当子类继承父类,父类的指针指向子类时,delete掉父类的指针,先调动子类的析构函数,再调动父类的析构函数。

#include <iostream>
#include <cstring>
using namespace std;
//Person类
class Person
{
public:
    Person(string name, int age):m_name(name),m_age(age)
    {
	}
	virtual void info()
	{
		cout << "Call Person info, Name = " << this->m_name << " Age = " << this->m_age << endl;
	}
	~Person() // 析构函数不是虚函数
	{
		cout << "Call ~Person" << endl;
	}
protected:
    string m_name;
    int m_age;
};
class Student:public Person
{
public:
    Student(string name, int age, float score):Person(name, age),m_score(score)
    {
	}
	virtual void info()
	{
		cout << "Call Student info, Name = " << this->m_name << " Age = " << this->m_age << " Score = " << m_score << endl;
	}
	~Student()
	{
		cout << "Call ~Student" << endl;
	}
protected:
    float m_score;
};
int main()
{
	Person *person = new Student("Bob", 18, 99);
    person->info();
    delete person;
	return 0;
}

运行结果

Call Student info, Name = Bob Age = 18 Score = 99
Call ~Person

在 main 函数里面,使用了父类指向了子类对象,最终,我们释放子类对象时,调用了父类的析构函数,这样会导致,我们子类对象的一些数据成员没发得到释放,会造成内存泄露,现在,我们将析构修改为虚析构,修改程序如下:

#include <iostream>
#include <cstring>
using namespace std;
//Person类
class Person
{
public:
    Person(string name, int age):m_name(name),m_age(age)
    {
	}
	virtual void info()
	{
		cout << "Call Person info, Name = " << this->m_name << " Age = " << this->m_age << endl;
	}
	virtual ~Person()
	{
		cout << "Call ~Person" << endl;
	}
protected:
    string m_name;
    int m_age;
};
class Student:public Person
{
public:
    Student(string name, int age, float score):Person(name, age),m_score(score)
    {
	}
	virtual void info()
	{
		cout << "Call Student info, Name = " << this->m_name << " Age = " << this->m_age << " Score = " << m_score << endl;
	}
	~Student()
	{
		cout << "Call ~Student" << endl;
	}
protected:
    float m_score;
};

int main()
{
	Person *person = new Student("Bob", 18, 99);
    person->info();
    delete person;
	return 0;
}

运行结果

Call Student info, Name = Bob Age = 18 Score = 99
Call ~Student
Call ~Person

这次,我们将父类的析构函数声明为了虚析构,再次运行程序,我们发现,这次首先调用了子类的析构函数,再次调用了父类的构造函数,这样就不会存在资源泄露的问题了,因此,存在继承时,最好将父类的析构声明为虚析构。


C++ 虚函数表

在 C++ 中,多态 是由 虚函数 实现的,而虚函数主要是通过虚函数表(V-Table)来实现的。对象不包含虚函数表,只有虚指针,类 才包含虚函数表,派生类会生成一个兼容基类的虚函数表。

如果一个类中包含虚函数(virtual 修饰的函数),那么这个类就会包含一张虚函数表,虚函数表存储的每一项是一个虚函数的地址。

虚函数表
在这里插入图片描述
这个类的每一个对象都会包含一个虚指针(虚指针存在于对象实例地址的最前面,保证虚函数表有最高的性能),这个虚指针指向虚函数表

原始基类的虚函数表

原始基类的对象,可以看到虚指针在地址的最前面,指向基类的虚函数表(假设基类定义了3个虚函数),如下图所示

在这里插入图片描述
单继承时的虚函数(无重写基类虚函数)

假设现在派生类继承基类,并且重新定义了 3 个虚函数,派生类会自己产生一个兼容基类虚函数表的属于自己的虚函数表,如下图所示

在这里插入图片描述
Derive class 继承了 Base class 中的三个虚函数,准确的说,是该函数实体的地址被拷贝到 Derive 类的虚函数表,派生类新增的虚函数置于虚函数表的后面,并按声明顺序存放

单继承时的虚函数(重写基类虚函数)

现在派生类重写基类的 x 函数,可以看到这个派生类构建自己的虚函数表的时候,修改了 base::x() 这一项,指向了自己的虚函数,如下图所示

在这里插入图片描述
Derive class 继承了 Base class 中的三个虚函数,准确的说,是该函数实体的地址被拷贝到 Derive 类的虚函数表,派生类新增的虚函数置于虚函数表的后面,并按声明顺序存放

多重继承时的虚函数

这个派生类多重继承了两个基类 base1,base2,因此它有两个虚函数表,如下图所示

加粗样式

它的对象会有多个虚指针(据说和编译器相关),指向不同的虚函数表。

虚继承时的虚函数表

虚继承的引入把对象的模型变得十分复杂,除了每个基类(MyClassA 和 MyClassB)和公共基类(MyClass)的虚函数表指针需要记录外,每个虚拟继承了 MyClass 的父类还需要记录一个虚基类表 vbtable 的指针 vbptr。MyClassC 的对象模型如图:

在这里插入图片描述
虚基类表每项记录了被继承的虚基类子对象相对于虚基类表指针的偏移量。比如 MyClassA 的虚基类表第二项记录值为 24,正是 MyClass::vfptr 相对于 MyClassA::vbptr 的偏移量,同理 MyClassB 的虚基类表第二项记录值 12 也正是 MyClass::vfptr 相对于 MyClassA::vbptr 的偏移量

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

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

相关文章

智慧交通项目实战全流程-DeepSort多目标跟踪车道线检测

1. 项目介绍 2. 算法库 2.1 numba介绍 numba是⼀个⽤于编译Python数组和数值计算函数的编译器&#xff0c;这个编译器能够⼤幅提⾼直接使⽤Python编写的函数的运算速度。 使用方法 numba对代码进⾏加速时&#xff0c;给要优化的函数加上jit优化器即可。使⽤jit的时候可以让…

IDEA断点调试(debug)

目录 1.断点调试介绍 2.F8逐行执行代码 3.调试遇异常 4.调试时如何看源码 5.如何直接执行到下一个断点 F9 6.利用断点调试查看动态绑定机制 1.断点调试介绍 断点调试是指在程序的某一行设置一个断点&#xff0c;调试时&#xff0c;程序运行到这一 行就会停住&#xff0…

2023年国赛数学建模思路 - 案例:FPTree-频繁模式树算法

文章目录 算法介绍FP树表示法构建FP树实现代码 建模资料 ## 赛题思路 &#xff08;赛题出来以后第一时间在CSDN分享&#xff09; https://blog.csdn.net/dc_sinor?typeblog 算法介绍 FP-Tree算法全称是FrequentPattern Tree算法&#xff0c;就是频繁模式树算法&#xff0c…

【Rust日报】2023-08-11 candle:一个极简的Rust机器学习框架

Bevys Third Birthday Bevy 是一个用 Rust 构建的令人耳目一新的数据驱动的游戏引擎&#xff0c;如果你想学习如何使用 Bevy 制作 2D/3D 游戏、可视化、用户界面或其他图形应用程序&#xff0c;那可以访问Bevy官网去了解更多。 阅读原文&#xff1a;https://bevyengine.org/new…

GPU Microarch 学习笔记 [1]

WARP GPU的线程从thread grid 到thread block&#xff0c;一个thread block在CUDA Core上执行时&#xff0c;会分成warp执行&#xff0c;warp的颗粒度是32个线程。比如一个thread block可能有1024个线程&#xff0c;分成32个warp执行。 上图的CTA&#xff08;cooperative thre…

西门子PLC模拟量接线及程序

接线 2线制接线 3线制接线 4线制接线 程序 指令 S_ITR 输入参数 EN 使能信号 AIW0 模拟量通道 ISH ISL 0-20ma对应 0-32000 4-20ma 对应 6400-32000 OSH OSL 传感器的测量值的最大和最小值 300 和 -50 输出参数 VD0 当前的测量温度

docker部署springboot

基础知识 什么是docker 官网&#xff1a; Docker Docs: How to build, share, and run applications | Docker Documentation Docker 是一个基于go语言开发的开源的应用容器引擎&#xff0c;让开发者可以打包他们的应用以及依赖包到一个可移植的容器中&#xff0c;然后发布到…

Unity 鼠标实现对物体的移动、缩放、旋转

文章目录 1. 代码2. 测试场景 1. 代码 using UnityEngine;public class ObjectManipulation : MonoBehaviour {// 缩放比例限制public float MinScale 0.2f;public float MaxScale 3.0f;// 缩放速率private float scaleRate 1f;// 新尺寸private float newScale;// 射线pri…

简绘ChatGPT支持Midjourney绘图 支持stable diffusion绘图

简绘支持Midjourney绘图和stable diffusion绘图。 这意味着简绘具备Midjourney绘图和stable diffusion绘图功能的支持。

分支语句和循环语句(1)

这篇文章我们详细的把分支语句和循环语句给大家进行讲解。 分支语句&#xff1a; if switch 循环语句&#xff1a; while for do while goto语句&#xff1a; 1.什么是语句&#xff1f; C语句可分为以下五类&#xff1a; 1. 表达式语句 2. 函数调用语句 3. 控制…

k8s常用资源管理 控制

目录 Pod&#xff08;容器组&#xff09;&#xff1a;Pod是Kubernetes中最小的部署单元&#xff0c;可以包含一个或多个容器。Pod提供了一种逻辑上的封装&#xff0c;使得容器可以一起共享网络和存储资源 1、创建一个pod 2、pod管理 pod操作 目录 创建Pod会很慢 Pod&…

LLM as Co-integrator:重塑团队间交互,持续改进信息对齐

封面由 Microsoft Designer 生成 在五月份的 QCon 大会上&#xff0c;我们分享了《探索软件开发新工序&#xff1a;LLM 赋能研发效能提升》。在那次分享里&#xff0c;我们重点提及了团队并非所有的时间在 SDLC 上&#xff0c;可能只有 30%~50% 时间在开发软件上&#xff0c;甚…

Gitlab CI/CD笔记-第二天-主机套接字进行构建并push镜像。

一、安装gitlab-runner 1.可以是linux也可以是docker的 2.本文说的是docker安装部署的。 二、直接上.gitlab-ci.yml stages: # List of stages for jobs, and their order of execution - build-image build-image-job: stage: build-image image: harbor.com:543/docke…

一周开发问题回顾(2023年08月07日-2023年08月13日)

一周开发问题回顾2023年08月07日-2023年08月13日 1. Arrays.asList()与 new ArrayList()的区别1.1 Arrays1.1.1补充 ArrayList(Arrays.asList(array)) 1.2 ArrayList()1.2.1 创建ArrayList的几种方法 2.Mysql中group by的使用方式3.画图4. 时间倒排5. 工厂策略设计模式6.List注…

第三课-界面介绍SD-Stable Diffusion 教程

前言 我们已经安装好了SD&#xff0c;这篇文章不介绍难以理解的原理&#xff0c;说使用。以后再介绍原理。 我的想法是&#xff0c;先学会画&#xff0c;然后明白原理&#xff0c;再去提高技术。 我失败过&#xff0c;知道三天打鱼两天晒网的痛苦&#xff0c;和很多人一样试了…

<Vite>HMR实现原理

什么是HMR&#xff1f; HMR&#xff08;Hot Module Replacement&#xff09;是一种开发工具&#xff0c;也就是热更新。用于在应用程序运行时替换、添加或删除模块&#xff0c;而无需完全重新加载整个页面或重新启动应用程序。这可以极大地提高开发效率和调试体验。 HMR的优势 …

MyEverything项目测试

一、自动化测试用例 二、功能测试 测试环境&#xff1a;win10、IDEA 2020.3.3 2.1目录文件选择功能 测试步骤&#xff1a; 1、运行项目&#xff0c;点击"选择目录"按钮 2、选择目标文件夹 3、点击"选择文件夹按钮" 4、重复上面三个步骤一次 期望结…

【C语言】每日一题(错误的集合)

最近在牛客、力扣上做题&#xff0c;花费海量时间&#xff0c;苦不堪言&#xff0c;有时绞尽脑汁也想不出&#xff0c;痛定思痛&#xff0c;每日记录写的比较困难的题。 错误的集合 题目如上图所示 题主乍看之下觉得很简单&#xff0c;再看例子&#xff0c;不就是一个有序数组…

【npm run dev报错】无法加载文件 C:\Program Files\nodejs\npm.ps1,因为在此系统上禁止运行脚本。

1.winX键&#xff0c;使用管理员身份运行power shell 2.输入命令&#xff1a;set-executionpolicy remotesigned 3.输入”Y“,回车&#xff0c;问题解决。 文章来源&#xff1a;无法加载文件 C:\Program Files\nodejs\npm.ps1&#xff0c;因为在此系统上禁止运行脚本。 - 前…

自制手写机器人

写字机器人模拟在画图板上写字效果 写了一套写字机器人代码&#xff0c;有多种字体可供选择&#xff0c;需要的朋友私信获取代码和软件