C++进阶篇2---多态

news2024/11/23 10:49:03

1.多态的概念

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

举个例子:同样是吃饭,狗吃狗粮,猫吃猫粮,不同的对象,对于同一个行为会有不同的状态

2.多态的定义和实现

2.1虚函数

虚函数:即被virtual修饰的类成员函数,注意是类成员函数,其他函数不能被virtual修饰

class A {
public:
	virtual void func() {
		cout << "hello C++" << endl;
	}
};

2.2虚函数的重写

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

class A {
public:
	virtual void func() {
		cout << "hello C++" << endl;
	}
};

class B : public A{
public:
    //虚函数重写
	virtual void func() {
		cout << "hello ZXWS" << endl;
	}
};

2.3多态的构成条件

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

2.被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

class A {
public:
	virtual void func() {
		cout << "hello C++" << endl;
	}
};

class B : public A{
public:
	virtual void func() {
		cout << "hello ZXWS" << endl;
	}
};

void test(A& a)//必须是基类的引用/指针
{
	a.func();//该函数是虚函数且被重写
}

int main()
{
	A a;
	B b;
	test(a);
	test(b);
	return 0;
}

 总结:多态的实现本质就是由虚函数的重写实现的,再次强调一下

虚函数重写规则的总结和补充

1.virtual关键字

2.三同(返回值类型/函数参数/函数名相同)

注意:

1.返回值不同也能构成虚函数重写,但是返回值的类型必须是引用/指针,称为协变,(返回值得全是指针/全是引用,且得是父子关系---父对父,子对子【基类中的虚函数返回值为父类,派生类中虚函数返回值为子类】,这里的父子关系可以是任意一对父子关系)

【注释】:基类、派生类和父类、子类是一个意思,上面的表述是为了表示两对父子关系

class A {

};
class B : public A {

};

class Person {
public:
    //父对父,子对子,对
	virtual A* func() {
		cout << "A* func()" << endl;
		return nullptr;
	}

    //父对子,子对父,错
	//virtual B* func() {
	//	cout << "A* func()" << endl;
	//	return nullptr;
	//}
};

class Student : public Person {
public:
	virtual B* func() {
		cout << "B* func()" << endl;
		return nullptr;
	}
    
	//virtual A* func() {
	//	cout << "B* func()" << endl;
	//	return nullptr;
	//}
};

2.析构函数的重写(正常来看基类和派生类的名字不同)

如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor
class Person {
public:
	virtual ~Person(){
		cout << "~Person()" << endl;
	}
};

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

int main()
{
	Person* p = new Person;
	Person* s = new Student;
	//如果析构函数不能实现虚函数重写,这里的空间释放就会出现问题
	//所以编辑器将析构函数的名字做了特殊处理
	delete p;
	delete s;
	return 0;
}
3.只要基类中的虚函数加了关键字virtual,派生类中可以不加,(推荐都加上)

2.4 C++11 override 和 final 

从上面的虚函数重写的规则,我们不难看出C++对虚函数的重写比较严格,这就导致我们在写的时候容易出错,且一些种错误在编译期间不会报错,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写

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

 2.override: 检查派生类虚函数是否重写了基类某个虚函数。

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

 3.抽象类

3.1概念

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

class B : public A {
public:
	virtual void func() {
		cout << "hello C++" << endl;
	};
};

class C : public A {
public:
	virtual void func() {
		cout << "hello ZXWS" << endl;
	};
};

int main()
{
    //A a;//抽象类不能实例化对象
	B b;
	C c;
	A* pb = &b;
	A* pc = &c;
	pb->func();
	pc->func();
	return 0;
}

 3.2接口继承和实现继承

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

大家来猜猜下面代码的打印结果

class A {
public:
	virtual void test() {
		func();
	}
	virtual void func(int x = 0) {
		cout << "A->" << x << endl;
	}
};
class B : public A {
public:
	virtual void func(int x = 1) {
		cout << "B->" << x << endl;
	}
};

int main()
{
	B b;
	A* pb = &b;
	pb->test();
	return 0;
}

代码分析:

首先pb调用A类中的test函数,因为B类中没有重写该虚函数,所以还是调用A类中的test。然后test函数里调用func函数,func函数实现了重写,所以调用B类中的,但是注意,这里的重写只是重写函数的内部实现,函数接口(函数的声明)还是A类的函数接口,所以x的缺省值为0,而不是1

大家要清楚普通继承和虚函数继承的差别!!!

4.多态的原理

在讲多态的原理之前,我们先来看看下面代码的运行结果

class A {
public:
	virtual void func(int x = 0) {
		cout << "A->" << x << endl;
	}
private:
	int _a;
	char ch;
};
int main()
{
	cout << sizeof(A) << endl;
	return 0;
}

 哎?为什么这是12呢,正常我们用内存对齐来算的结果应该是8才对呀,这个类和之前的类的唯一区别在于它多了一个虚函数,我们就猜测类里面来应该存了func这个函数的相关信息。

下面我们来调试看看

显然,a对象中确实多了一个指针,所以对象a的大小为12(用的是32位的编译环境,地址是4字节),那么这个指针有什么用呢?

这个指针指向的空间里存放了虚函数的地址,我们称这块空间为虚函数表,简称虚表,所以这个指针叫做虚函数表指针 (一个含有虚函数的类中都至少有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中)

那么多态是如何通过虚函数实现的呢?

通过上面的图,我们就应该能理解多态的实现原理了,本质就是在虚表里查找函数,里面放的啥函数就调用啥函数,而虚表里存放的虚函数,根据子类对父类的虚函数有没有重写,来决定存放哪个函数的地址

同时这里也能解释为什么多态的实现需要调用父类的引用/指针,因为父类对子类对象的引用并没有生成临时变量,而是直接引用的子类继承来的父类部分,那么当然也能通过虚函数表指针调用对应的函数实现多态,指针同理

(注意:有些平台可能会将虚函数表指针放到对象的最后面,这个跟平台有关)

 还有一点:同一个类的对象公用同一张虚函数表

 4.3动态绑定和静态绑定

1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为也称为静态多态,比如:函数重载
2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态

5.单继承和多继承关系的虚函数表

虚函数表存在哪个区域?栈?堆?还是什么?

class A {
public:
	virtual void func() { cout << "A" << endl; }
};
void test()
{
	A aa;
	int a=0;
	int* p = new int;
	const char* str = "hello ZXWS";
	static int c = 0;
	
	printf("代码区:%p\n",str);
	printf("静态区:%p\n", &c);
	printf("堆区:%p\n", p);
	printf("栈区:%p\n", &a);
	printf("虚表:%p\n", *((int*)&aa));//VS中虚函数表指针的地址一般在对象的起始位置
	printf("虚函数:%p\n", &A::func);
	return 0;
}

很显然,虚表的地址离代码区比较近,其实虚表就在代码区,这也符合我们的认识,因为虚表的内容是不能被修改的,当然虚函数其实和正常函数没什么区别,都在代码区

5.1单继承

class A {
public:
	virtual void func1() { cout << "A::func1" << endl; }
	virtual void func2() { cout << "A::func2" << endl; }
private:
	int _a;
};

class B {
public:
	virtual void func1() { cout << "B::func1" << endl; }
	virtual void func3() { cout << "B::func3" << endl; }
	virtual void func4() { cout << "B::func4" << endl; }
private:
	int _b;
};

int main()
{
	A a;
	B b;
	return 0;
}

当我们看到调试窗口,就会发现B类的虚函数表中只有两个虚函数,而B类中应该有4个虚函数才对,那么会为什么呢?

这里解释一下,这是编辑器自己做了处理,我们要想看清,还是得看地址空间,如下

 (VS编辑器会在虚函数表的结尾放一个空指针)从内存窗口来看,b的虚函数表确实是存了4个地址,但是我们不能确定,下面我们来验证一下

typedef void(*VF)();//重定义函数指针
void PrintVF(VF v[]) {
	for (int i = 0; v[i]; i++) {
		printf("[%d]:%p-> ", i, v[i]);
		v[i]();//用函数指针调用函数
		printf("\n");
	}
}
int main()
{
	A a;
	B b;
	PrintVF((VF*)(*(int*)&b));//这里传的是虚函数表指针
    //VS中虚函数表指针的地址一般在对象的起始位置,这里注意32位地址4字节,64位地址8字节
    //这里是32位下,用int*,64位下要用long long*
	return 0;
}

通过验证,单继承中虚函数确实都在虚函数表中(验证时要注意,编辑器有时候对虚表处理不干净,需要我们重新生成解决方案,重新编译一下)

5.2多继承中的虚函数表

class A1 {
public:
	virtual void func1() { cout << "A1::func1" << endl; }
	virtual void func2() { cout << "A1::func2" << endl; }
private:
	int a1;
};

class A2 {
public:
	virtual void func1() { cout << "A2::func1" << endl; }
	virtual void func2() { cout << "A2::func2" << endl; }
private:
	int a2;
};

class B : public A1, public A2 {
public:
	virtual void func1() { cout << "B::func1" << endl; }
	virtual void func3() { cout << "B::func3" << endl; }
private:
	int b;
};

int main()
{
	B b;
	printf("虚表地址:%p\n",* ((int*)&b));
	PrintVF((VF*)(*((int*)&b)));
	A2* p = &b;
	printf("虚表地址:%p\n", *((int*)p));
	PrintVF((VF*)(*((int*)p)));
	return 0;
}

可以看出:多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中,且子类会继承父类的虚函数表

这里简单说明一下,为什么两个虚表中B::func1函数的地址不同的问题,其实这是为了修正this指针,我们知道多态的实现离不开指针(引用),当我们用p去调用func1函数时,显然传过去的this指针是不对的,p仅仅指向A2的部分,所以编辑器会在调用B::func1这个函数之前,将this指针修正成b对象的起始地址,而继承来的A1成员的起始地址恰好和b对象的起始地址相同,不需要修正,所以直接填了函数地址,所以两者地址不同,但是最终调用的函数一样

5.3菱形继承、菱形虚拟继承

这里只要了解即可

菱形进程和多继承一样,无非是消耗了空间,这里就不多讲了

我们来看看菱形虚拟继承

class A {
public:
	virtual void func1() { cout << "A1::func1" << endl; }
private:
	int a=1;
};

class A1 : virtual public A{
public:
	virtual void func1() { cout << "A1::func1" << endl; }
	virtual void func2() { cout << "A1::func2" << endl; }
private:
	int a1=2;
};

class A2 : virtual public A {
public:
	virtual void func1() { cout << "A2::func1" << endl; }
	virtual void func2() { cout << "A2::func2" << endl; }
private:
	int a2=3;
};

class B : public A1, public A2 {
public:
	virtual void func1() { cout << "B::func1" << endl; }
	virtual void func3() { cout << "B::func3" << endl; }
private:
	int b=4;
};

(上面两个图是分开调试的,需要单独分析,仅仅当长个见识,不会也没关系)

六、一些小点

1.inline函数可以是虚函数吗?

可以,不过编译器就忽略inline属性,这个函数就不再是 inline,因为虚函数要放到虚表中
class A {
public:
	inline virtual void func() { cout << "A::func()"; }
};
int main()
{
	A a;
	A* p = new A;
	a.func();
	p->func();
	return 0;
}

(上面的调试内容需要将相关优化打开,正常调试看不到,这里也就是给大家看看)

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

不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。
3.构造函数可以是虚函数吗?
不可以,因为虚表指针是在构造函数初始化列表阶段才初始化的
4.对象访问普通函数快还是虚函数快?
首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。

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

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

相关文章

Javascript基础-DOM

文章目录 WEB APISDOM-Document Object Model概念DOM对象获取DOM对象通过css选择器获取其他获取方式 操作元素内容操作元素属性定时器 DOM-事件监听概念案例-关闭广告老版本事件类型事件对象获取事件对象属性 环境对象回调函数 事件流事件捕获事件冒泡阻止冒泡阻止默认行为 解绑…

【JAVA学习笔记】40 - 抽象类、模版设计模式(抽象类的使用)

项目代码 https://github.com/yinhai1114/Java_Learning_Code/tree/main/IDEA_Chapter10/src/com/yinhai/abstract_ 一、抽象类的引入 很多时候在创建类的时候有一个父类&#xff0c;比如animal类&#xff0c;他的子类会有各种方法&#xff0c;为了复用需要进行方法的重写&…

微信小程序OA会议系统个人中心授权登入

在我们的完成微信登入授权之前&#xff0c;首先我们要完成我们前面所写的代码&#xff0c;如果有不会的大家可以去看以下我发的前面几个文章链接我发下面了&#xff0c;各位加油&#xff01; 微信小程序OA会议系统数据交互-CSDN博客 微信小程序会议OA系统其他页面-CSDN博客 …

基于nodejs+vue视频网站的设计与实现mysql

目 录 摘 要 I ABSTRACT II 目 录 II 第1章 绪论 1 1.1背景及意义 1 1.2 国内外研究概况 1 1.3 研究的内容 1 第2章 相关技术 3 2.1 nodejs简介 4 2.2 express框架介绍 6 2.4 MySQL数据库 4 第3章 系统分析 5 3.1 需求分析 5 3.2 系统可行性分析 5 3.2.1技术可行性&#xff1a;…

机器学习---CNN(创建和训练一个卷积神经网络并评估其性能)上

1. cnn_operations模块 cnn_operations类 staticmethoddef calc_loss(Y, tilde_Y):# 训练样本个数n_samples Y.shape[0]# 网络代价loss 0for i in range(n_samples):loss np.sum((Y[i, :] - tilde_Y[i, :])**2)loss / (2 * n_samples)return loss计算网络代价&#xff1a; …

编程自学路线:开源免费的教育资源 | 开源专题 No.40

trekhleb/javascript-algorithms Stars: 174.1k License: MIT 这个项目是一个包含许多流行算法和数据结构的 JavaScript 示例。该项目提供了各种不同类型的数据结构&#xff0c;如链表、队列、栈等&#xff0c;并且还提供了各种常见的算法实现&#xff0c;如排序算法、搜索算…

Kubernetes技术与架构-网络 3

Kubernetes集群支持为Pod或者Service申请IPV4或者IPV6的地址空间。 kube-apiserver --service-cluster-ip-range<IPv4 CIDR>,<IPv6 CIDR> kube-controller-manager --cluster-cidr<IPv4 CIDR>,<IPv6 CIDR> --service-cluster-ip-range<IPv4 CI…

Java switch封神之路

Java switch升级之路 一&#xff0c;介绍 switch 是一种用于多分支条件判断的控制流语句。它通过检查一个表达式的值&#xff0c;然后根据不同的情况执行相应的代码块。 在大多数编程语言中&#xff0c;switch 语句由多个 case 分支组成&#xff0c;每个 case 后面跟着一个常…

unity游戏画质设置功能实现

在游戏中往往会出现游戏画质设置的功能。 如图&#xff1a; 这个功能是怎么实现完成的呢&#xff1f; 一、目标&#xff1a;实现切换画质功能 二、了解unity支持的画质 首先要了解unity中共支持多少种画质。 在代码中也可以进行打印。 方法如下&#xff1a; void Start …

NC61 两数之和

牛客网 NC61 两数之和 https://www.nowcoder.com/share/jump/7890810391698077140732 记录&#xff1a;维护哈希表&#xff0c;题目满足&#xff0c;numbers内必有两数相加为target&#xff0c;则可理解为&#xff0c;每次只需要判断target减去当前数&#xff0c;是否能在维护的…

zookeeper源码(02)源码编译启动及idea导入

本文介绍一下zookeeper-3.9.0源码下载、编译及本地启动。 下载源码 git clone https://gitee.com/apache/zookeeper.gitcd zookeeper git checkout release-3.9.0 git checkout -b release-3.9.0源码编译 README_packaging.md文件 该文件介绍了编译zookeeper需要的环境和命…

【JavaEE】UDP数据报套接字编程

一、UDP数据报套接字编程 1.1 DatagramSocket API DatagramSocket 是UDP Socket&#xff0c;用于发送和接收UDP数据报。 DatagramSocket 构造方法&#xff1a; DatagramSocket 方法&#xff1a; 1.2 DatagramPacket API DatagramPacket是UDP Socket发送和接收的数据报。…

NAS搭建指南三——私人云盘

一、私人云盘选择 我选择的是可道云进行私人云盘的搭建可道云官网地址可道云下载地址&#xff0c;下载服务器端和 Windows 客户端可道云官方文档 二、环境配置 PHP 与 MySQL 环境安装&#xff1a;XAMPP 官网地址 下载最新的 windows 版本 安装时只勾选 MySQL 与 PHP相关即可…

sklearn-6算法链与管道

思想类似于pipeline&#xff0c;将多个处理步骤连接起来。 看个例子&#xff0c;如果用MinMaxScaler和训练模型&#xff0c;需要反复执行fit和tranform方法&#xff0c;很繁琐&#xff0c;然后还要网格搜索&#xff0c;交叉验证 1 预处理进行参数选择 对于放缩的数据&#x…

工程化测试:Apollo的单元测试与集成测试指南

前言 「作者主页」&#xff1a;雪碧有白泡泡 「个人网站」&#xff1a;雪碧的个人网站 「推荐专栏」&#xff1a; ★java一站式服务 ★ ★ React从入门到精通★ ★前端炫酷代码分享 ★ ★ 从0到英雄&#xff0c;vue成神之路★ ★ uniapp-从构建到提升★ ★ 从0到英雄&#xff…

vue后台第二部步(布局和封装图标组件)

目录结构&#xff1b;根据需求修改、创建对应目录&#xff1b; src 应用部署目录├─api 接口├─assets 公共文件│ ├─theme.scss 主题样式…

一文了解AIGC与ChatGPT

一、AIGC简介 1.AIGC基础 (1)AIGC是什么 AIGC是人工智能图形计算的缩写&#xff0c;是一种基于图形处理器&#xff08;GPU&#xff09;的计算技术&#xff0c;可以加速各种计算任务&#xff0c;包括机器学习、深度学习、计算机视觉等。 AIGC是一种基于GPU的计算技术&#x…

MSQL系列(七) Mysql实战-SQL语句Join,exists,in的区别

Mysql实战-SQL语句Join&#xff0c;exists&#xff0c;in的区别 前面我们讲解了索引的存储结构&#xff0c;BTree的索引结构&#xff0c;以及索引最左侧匹配原则及讲解一下常用的SQL语句的优化建议&#xff0c;今天我们来详细讲解一下 我们经常使用的 join&#xff0c; exist&…

【机器学习】模型平移不变性/等变性归纳偏置Attention机制

Alphafold2具有旋转不变性吗——从图像识别到蛋白结构预测的旋转对称性实现 通过Alphafold2如何预测蛋白质结构&#xff0c;看有哪些机制或tricks可以利用&#xff1f; 一、等变Transformer 等变Transformer是Transformer众多变体的其中一种&#xff0c;其强调等变性。不变性…