C++之多态

文章目录

  • 前言
  • 一、多态的概念
  • 二、多态的定义及实现
    • 1.多态的构成条件
    • 2.虚函数
    • 3.虚函数的重写(覆盖)
    • 4.虚函数重写的两个例外
    • 4.C++11中的override和final关键字
  • 三、重载、重定义(隐藏)、重写(覆盖)的区分
  • 四、抽象类
    • 1.概念
    • 2.接口继承和实现继承
  • 五、多态的原理
    • 1.虚函数表
    • 2.多态的原理
    • 3.动态绑定和静态绑定
      • 静态绑定
      • 动态绑定
  • 六、单继承和多继承关系的虚函数表
    • 1.单继承中的虚函数表
    • 2.多继承中的虚函数表
  • 总结


前言

本文主要介绍了C++中面向对象三大特性之一的多态的相关概念。主要介绍了多态的原理,如何实现多态以及虚函数等相关概念。


一、多态的概念

通俗来说,多态就是去完成一个行为不同的对象去完成时会有不同的状态。
例如,买火车票这一个行为,不同的对象去完成会有不同的状态:
普通人:成人票
学生:学生票
军人:优先买票

二、多态的定义及实现

1.多态的构成条件

多态,是指在不同继承关系中类对象,去调用同一函数产生了不同的行为。比如,Student继承了Person,Person对象买票是全票,Student对象买票是半价票。
在继承种构成多态要满足两个条件:

  1. 必须通过基类的指针或者引用调用虚函数(该指针或者引用操作的是派生类种基类的那一部分内容)
  2. 调用的函数必须是虚函数,且派生类必须对虚函数进行重写
    在这里插入图片描述

2.虚函数

被关键字virtual修饰的类成员函数称为虚函数

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

3.虚函数的重写(覆盖)

派生类中有一个与基类完全相同的虚函数函数名,参数列表返回值类型等完全相同),称子类的虚函数重写了父类的虚函数。
注意:在重写虚函数时,子类的虚函数前可以不加virtual关键字,因为它是继承自父类的虚函数,其虚函数的属性是被继承了下来,但是一般还是写上更加规范。

4.虚函数重写的两个例外

  1. 协和:
    派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时(返回值类型为继承关系的指针),称为协变。
  2. 析构函数:
    如果基类的析构函数定义为虚函数,则派生类的析构函数无论是否加virtual关键字都与基类的析构函数构成重写,这里可以理解为编译器对析构函数进行特殊处理将析构函数的函数名统一处理为destuctor。因此一般都会将基类的析构函数定义为虚函数。
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;
}

在这里插入图片描述

4.C++11中的override和final关键字

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

  1. final修饰父类的虚函数,该虚函数不能被重写
  2. override修饰子类的虚函数检查是否完成重写,如果没有完成重写则会编译报错。
class Car
{
public:
	virtual void Drive() final 
	{}
};
class Benz :public Car
{
public:
	virtual void Drive() 
	{ 
	cout << "Benz" << endl; 
	}
};

在这里插入图片描述

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

三、重载、重定义(隐藏)、重写(覆盖)的区分

在这里插入图片描述

四、抽象类

1.概念

在虚函数的后面写上 = 0,这个虚函数就是纯虚函数。包含纯虚函数的类,称为抽象类。
抽象类不能实例化处对象,它的派生类也不能实例化处对象,只有派生类重写纯虚函数之后才能实例化出对象。
纯虚函数规范了派生类必须重写,纯虚函数体现出接口继承。

2.接口继承和实现继承

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

五、多态的原理

1.虚函数表

测试下面代码,计算类Base的大小:

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

在这里插入图片描述
如图可以看到Base类的大小为8,为什么会是8呢?
我们调试观察:
在这里插入图片描述
可以看到对象b中除了成员变量_b以外还有一个__vfptr指针变量。放在对象的前面(注意:有些平台可能会到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表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;
};
int main()
{
	Base b;
	Derive d;
	return 0;
}

在这里插入图片描述
通过调试观察监视窗口,我们有以下结论

  1. 基类和派生类对象中各有一个虚函数表指针,它们所指向的虚函数表不同,派生类的虚函数表中除了基类的虚函数成员外还有一部分自己的虚函数成员。
  2. 上面的例子中,基类的虚函数Func1被子类重写,所以派生类中的虚函数表中存的是重写后的虚函数Func1。重写又叫覆盖,重写是语法层面的叫法,覆盖是原理层的叫法。
  3. 基类的成员函数Func2被派生类继承下来,由于是虚函数,所以进入虚函数表;
    基类的成员函数Func3也被派生类继承下来,由于不是虚函数,所以没有进入虚表。
  4. 虚函数表的本质是一个数组,存放的是指向虚函数的指针,一般这个数组最后放的是nullptr标志该虚表结束。

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

  1. 基类的虚函数直接进派生类的虚表;
  2. 基类的虚函数如果在派生类中被重写,就将重写后的虚函数覆盖基类的虚函数;
  3. 派生类自己的虚函数,按照其在派生类中声明顺序依次增加在虚表的最后;

虚表存在哪里,是存放在对象中吗?虚表里面存放的是什么,是存放着虚函数吗?
答:虚表存放在代码段,对象中存放的是虚表指针;虚函数存放在代码段,虚表里存放的是虚函数指针。

2.多态的原理

分析了这么多,多态的原理到底是什么呢?

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

运行结果:
在这里插入图片描述
可以看到Person对象和Student对象调用同一个函数Func得到不同的结果,这是因为基类调用函数的传参基类对象,而派生类对象调用函数时的传参是派生类对象中基类的那一部分。导致基类的指针p是调用基类的成员函数,派生类的指针p是调用派生类的成员函数。

简单来说:

  1. 普通函数调用传谁调用谁; 符合多态的函数调用就是指向谁调用谁
  2. 满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象的中取找的(动态绑定);不满足多态的函数调用是在编译时就确定的(静态绑定)。

3.动态绑定和静态绑定

静态绑定

静态绑定是指在编译期间就确定的程序行为。比如,函数重载;

动态绑定

动态绑定是指在运行时确定的程序行为,根据具体拿到的类型确定程序的具体行为,调用具体的函数。比如,多态。

六、单继承和多继承关系的虚函数表

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;
};
int main()
{
	Base b;
	Derive d;
	return 0;
}

在这里插入图片描述
通过调试观察监视窗口我们发现派生类对象的虚函数表中没有派生类自己的虚函数func3和func4,这是怎么回事呢?
我们可以将这个现象理解为一个Bug,并不是派生类的虚表里没有它自己的虚函数,而是这两个虚函数被监视窗口隐藏了。那么如果我们想查看派生类的虚函数都有那些该如何进行查看呢?
我们可以用代码将虚表中的虚函数打印出来。

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;
};
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;
}
int main()
{
	Base b;
	Derive d;
	// 思路:取出b、d对象的头4bytes,就是虚表的指针,前面我们说了虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr
	// 1.先取b的地址,强转成一个int*的指针
	// 2.再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针
	// 3.再强转成VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。
	// 4.虚表指针传递给PrintVTable进行打印虚表
	// 5.需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最后面没有放nullptr,导致越界,这是编译器的问题。我们只需要点目录栏的 - 生成 - 清理解决方案,再编译就好了。
	VFPTR* vTableb = (VFPTR*)(*(int*)&b);
	PrintVTable(vTableb);
	VFPTR* vTabled = (VFPTR*)(*(int*)&d);
	PrintVTable(vTabled);
	return 0;
}

2.多继承中的虚函数表

继承几个基类就有几张虚表,派生类自己的虚函数直接放在第一个继承基类部分的虚表中。

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;
}
int main()
{
	Derive d;
	VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
	PrintVTable(vTableb1);
	VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));
	PrintVTable(vTableb2);
	return 0;
}

总结

以上就是今天要讲的内容,本文介绍了C++中多态的相关概念。本文作者目前也是正在学习C++相关的知识,如果文章中的内容有错误或者不严谨的部分,欢迎大家在评论区指出,也欢迎大家在评论区提问、交流。
最后,如果本篇文章对你有所启发的话,希望可以多多支持作者,谢谢大家!

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

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

相关文章

【美赛】2023年ICM问题Z:奥运会的未来(思路、代码)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

【面试】MySQL面试题

文章目录数据库基础知识为什么要使用数据库什么是SQL&#xff1f;什么是MySQL?MySql, Oracle&#xff0c;Sql Service的区别数据库三大范式是什么mysql有关权限的表都有哪几个MySQL的binlog有有几种录入格式&#xff1f;分别有什么区别&#xff1f;数据库经常使用的函数数据类…

C++设置动态库的版本号(软链接)

1,动态库版本命名规则 假设有一个动态库&#xff1a;libfooSdk.so.1.1.0&#xff0c;其对应的三个名称如下。 realname&#xff1a;libfooSdk.so.1.1.0 soname&#xff1a;libfooSdk.so.1 linkname&#xff1a;libfooSdk.solinux的动态库的命名格式是libfooSdk.so.x.y.z 版本…

大数据概述及其软件生态

一、大数据的诞生 &#xff08;1&#xff09;当全球互联网逐步建成&#xff08;2000年左右&#xff09;&#xff0c;各大企业或政府单位拥有了海量的数据亟待处理。 &#xff08;2&#xff09; 基于这个前提逐步诞生了以分布式的形式&#xff08;即多台服务器集群&#xff09;…

PCB生产工艺流程三:生产PCB的内层线路有哪7步

PCB生产工艺流程三&#xff1a;生产PCB的内层线路有哪7步 在我们的PCB生产工艺流程的第一步就是内层线路&#xff0c;那么它的流程又有哪些步骤呢&#xff1f;接下来我们就以内层线路的流程为主题&#xff0c;进行详细的分析。 由半固化片和铜箔压合而成&#xff0c;用于…

Vue|计算属性

1. 计算属性1.1 差值语法1.2 methods1.3 计算属性1. 计算属性 1.1 差值语法 开始前分别在项目目录创建文件夹及页面如下 需求1&#xff1a;在两个文本框中分别输入姓和名的同时需要在下方将数据进行拼接组装&#xff0c;效果如下图 如果用传统的方式来实现的话&#xff0c;需要…

投屏软件:ApowerMirror Crack

一个软件&#xff0c;两个系统 ApowerMirror是一个跨平台的屏幕镜像应用程序&#xff0c;可用于iOS和Android设备&#xff0c;与Windows和Mac兼容。对于运行支持 Chromecast 的 Android 5.0 或更高版本的手机&#xff0c;用户可以使用此程序镜像屏幕。而对于支持AirPlay的iOS设…

bfs与dfs详解(经典例题 + 模板c-代码)

文章首发于&#xff1a;My Blog 欢迎大佬们前来逛逛 文章目录模板解析dfsbfs1562. 微博转发3502. 不同路径数165. 小猫爬山模板解析 DFS&#xff08;深度优先搜索&#xff09;和BFS&#xff08;广度优先搜索&#xff09;是图论中两个重要的算法。 dfs 其中DFS是一种用于遍历…

spring源码之扫描前设置

扫描前设置 &#x1f6f9;源码源码说明总结启动一个springboot项目源码 org.springframework.context.annotation.ComponentScanAnnotationParser#parse public Set<BeanDefinitionHolder> parse(AnnotationAttributes componentScan, String declaringClass) {// 创建C…

清明-微信小程序

# 云开发接入 初始化云开发 环境保密

深度学习部署(十三): CUDA RunTime API thread_layout线程布局

1. 知识点 在.vscode/settings.json中配置"*.cu": "cuda-cpp"可以实现对cuda的语法解析 layout是设置核函数执行的线程数&#xff0c;要明白最大值、block最大线程数、warpsize取值 maxGridSize对应gridDim的取值最大值maxThreadsDim对应blockDim的取值最…

酷炫的青蛇探针serverMmon

本文软件由网友 114514 推荐&#xff1b; 什么是 serverMmon &#xff1f; serverMmon (青蛇探针)是 nodeJs 开发的一个酷炫高逼格的云探针、云监控、服务器云监控、多服务器探针。 主要功能介绍&#xff1a; 全球服务器分布世界地图服务器&#xff08;控制端&#xff09; ping…

简单3招教你设置电脑时间

案例&#xff1a;电脑时间怎么设置&#xff1f; 【我使用电脑时&#xff0c;电脑显示的时间一直不对&#xff0c;这导致我非常不方便&#xff0c;想问下大家平常使用电脑时有什么设置电脑时间比较简单的方法吗&#xff1f;】 电脑的时间设置很重要&#xff0c;不仅可以保证电…

Java单例模式、阻塞队列、定时器、线程池

目录1. 单例模式1.1 饿汉模式实现单例1.2 懒汉模式实现单例1.2.1 加锁实现懒汉模式线程安全1.2.2 volatile实现懒汉模式线程安全1.3 饿汉模式和懒汉模式小结&#xff08;面试题&#xff09;2. 阻塞队列2.1 单线程下阻塞队列2.2 多线程下阻塞队列——生产者消费者模型2.3 模拟写…

【蓝桥集训18】二分图(2 / 2)

二分图定义&#xff1a;将所有点分成两个集合&#xff0c;使得所有边只出现在集合之间&#xff0c;就是二分图 目录 860. 染色法判定二分图 1、dfs 2、bfs 861. 二分图的最大匹配 - 匈牙利算法ntr算法 860. 染色法判定二分图 活动 - AcWing 1、dfs 思路&#xff1a; 对每…

白农:Imagination将继续致力于推进车规半导体IP技术创新和应用

4月2日Imagination中国董事长白农在中国电动汽车百人论坛上发表演讲&#xff0c;探讨了车规半导体核心IP技术如何推动汽车智能化发展&#xff0c;并接受了媒体采访。本次论坛上&#xff0c;他强调了IP技术在汽车产业链中日益重要的地位和供应商的位置前移。类比手机行业的发展&…

树、森林、二叉树:相互之间的转换

你好&#xff0c;我是王健伟。 前面我们讲过了各种二叉树&#xff0c;这方面的知识已经够多的了&#xff0c;本节就来讲一讲更通用的概念&#xff1a;树、森林以及与二叉树之间的转换问题。 树的存储结构 前面我们学习了树形结构的基本概念&#xff0c;在满足这个概念的前提…

python 包、模块学习总结

、模块基础 1、基本概念 模块是最高级别的程序组织单元&#xff0c;它将程序代码和数据封装起来以便重用。从实际角度来看&#xff0c;模块往往对应于python程序文件&#xff08;或是用外部语言如C、Java或C#编写而成的扩展&#xff09;。每一个文件都是一个模块&#xff0c;并…

小驰私房菜_11_mm-camera 添加客制化分辨率

#小驰私房菜# #mm-camera# #客制化分辨率# 本篇文章分下面几点展开&#xff1a; 1) mm-camera框架下&#xff0c;是在哪个文件添加客制化分辨率&#xff1f; 2&#xff09; 新添加分辨率的stall duration如何计算&#xff1f; 3&#xff09; 新添加的分辨率会有哪些影响&…

CentOS7操作系统离线安装docker

前言 有时候我们没有办法联网安装各种软件包&#xff0c;这时候就需要提前下载好所需要的包&#xff0c;然后把包上传到服务&#xff0c;在服务器上进行安装。 今天我们一起来探讨了在centos7操作系统上&#xff0c;安装docker。 专栏地址&#xff1a;容器管理 &#xff0c;…