IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> C++多态 -> 正文阅读

[C++知识库]C++多态

前言:

共识

  1. 基类对象的指针或者引用调用虚函数时,都以多态的流程执行代码。可以说,虚函数就是为实现多态做准备的。

  2. 同一个类的不同对象,共用同一个虚函数表,且虚函数表最可能存放在常量区/代码段

重载

  1. 要求重载的函数在同一作用域,一般是全局作用域
  2. 函数名,参数相同

重定义

  1. 2个函数分别在基类和子类作用域
  2. 重定义也叫做隐藏,基类与子类的同名函数就构成隐藏
  3. 重定义关注的地方是:函数的声明和实现
  4. 同名成员变量也构造隐藏

重写

  1. 2个函数分别在基类和子类的作用域
  2. 要求三同:函数名/参数/返回值必须相同
  3. 2个函数必须是虚函数
  4. 重写就是一种接口继承(会继续基类的属性,这也就是为什么子类可以不写virtual),关注的是函数的实现。
  5. 当基类指针或者引用调用重写函数,可能构造多态,当子类对象调用重写函数,构成隐藏/重定义

重写的2个特殊情况

  1. 协变(基类与派生类虚函数返回值类型不同)派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。
  2. 析构函数的重写(基类与派生类析构函数的名字不同)如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor

重写的一个特殊点

  1. 重写如果在多态调用中就是

C++11 override 和 final

final

  1. 修饰函数:表明该函数不能被重写。

  2. 修饰类:表明该类不能被继承

override

检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错

一个例题

class A
{
public:
virtual void func(int val = 1){ std::cout<<"A->"<< val <<std::endl;}
virtual void test(){ func();}
};
class B : public A
{
public:
void func(int val=0){ std::cout<<"B->"<< val <<std::endl; }
};
int main(int argc ,char* argv[])
{
B*p = new B;
p->test();
return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-g5kphQjE-1660721146365)(./%E5%A4%9A%E6%80%81.assets/image-20220817095135938-16607010973161.png)]

多态

概念

不同的子类对象,通过基类指针与引用,调用相同的函数,产生不同的效果。

条件

  1. . 必须通过基类的指针或者引用调用虚函数,即保证调用函数的this指针是基类地址
  2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
  3. 当子类对象赋值给基类对象时,编译器只会将成员变量赋值给基类对象,子类的虚函数不会拷贝给基类对象

应用层分析

在这里插入图片描述

通过基类对象的指针或者引用,调用虚函数,且该虚函数完成重写时,构成多态。

原理层分析

通过设置一个虚函数表,实现多态

在这里插入图片描述

虚函数表

  1. 虚函数指针是在对象构造函数初始化队列中初始化的。
  2. 本质是一个数组,存放的是虚函数的地址

虚函数表打印

class Base {
public:
	virtual void func1() { cout << "Base1::func1" << endl; }
	virtual void func2() { cout << "Base1::func2" << endl; }

private:
	int b1;
};
class Derive : public Base {
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }
private:
	int d1;
};

//typedef void(*) () VFPTR;
//本来重命名函数指针类型时是这样,但是C++语言规定这种是不行的,需要将VFPTR放到内部
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
	cout << " 虚表地址>" << vTable << endl;
	for (int i = 0; vTable[i] != nullptr;++i)
	{
		printf("第%d个虚函数地址 :%p,->", i, vTable[i]);
		//通过this调用函数是在编译阶段的事情,一旦到内存就没有这种限制
		VFPTR f = vTable[i];
		f();
	}
	cout << endl;
}
int main()
{

	Derive d;
	printf("Derive::func1地址%p\n", &(Derive::func1));
	printf("Derive::func2地址%p\n", &(Derive::func2));
	printf("Derive::func3地址%p\n", &(Derive::func3));

	VFPTR* vTableb1 = (VFPTR*)(*((void**)&d));
	//通过强制转换来获得d中前一个指针大小的字节内容。
	//考虑到不同平台指针大小不同,
	//我们指针指针解引用后会得到对应地址后的指向对象大小的内容
	// 即int *p,p解引用后,得到从指向对象地址也就是变量p中的值
	// 开始的sizeof(int)字节大小的内容
	// 
	// 因此将d的地址强转为二级指针,当引用后,
	// 就可以的到对应地址的sizeof(void*)大小的内容
	// 就可以满足不同平台完成同样的需求
	//最后再强制转换即可
	PrintVTable(vTableb1);
	
}

在这里插入图片描述

可以发现,主动打印的函数地址和虚函数表中的地址是不同?

这是因为VS进行了封装,无论是主动打印的还是虚函数表中的地址都不是真正的函数地址,而是一个指令地址。证明如下

请添加图片描述

多继承关系的虚函数表

  1. 多继承中,子类会将自己独有的虚函数,放到继承声明中的第一个基类的虚函数表中
  2. 基类各自拥有着自己的虚函数表

菱形继承、菱形虚拟继承

实际中我们不建议设计出菱形继承及菱形虚拟继承,一方面太复杂容易出问题,另一方面这样的模型,访问基类成员有一定得性能损耗。所以菱形继承、菱形虚拟继承我们的虚表我们就不看了,一般我们也不需要研究清楚,因为实际中很少用。如果好奇心比较强的宝宝,可以去看下面的两篇链接文章。

C++ 虚函数表解析

C++ 对象的内存布局

菱形继承

在发生代码冗余与二义性的地方,通过virtual继承,建立一个虚基表,通过相对偏移量实现基类的共享

菱形虚拟继承

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nxFYnksm-1660721146387)(./%E5%A4%9A%E6%80%81.assets/image-20220817151052587-166072025699917.png)]

  1. 菱形虚拟继承中,因为BC共享同一个基类A,但是A的虚函数表只有一个,那么A中的虚函数表该存放那个是没法确定的。
  2. 通过在D中重写func1来解决该问题。同时BC中的虚函数表存放D的func1

问答题

  1. inline函数可以是虚函数吗?答:可以,不过当进行多态调用时编译器就忽略inline属性,这个函数就不再是inline,因为虚函数要放到虚表中去。
  2. 静态成员可以是虚函数吗?答:不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式,是无法访问虚函数表,所以静态成员函数无法放进虚函数表。
  3. 构造函数可以是虚函数吗?答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。
  4. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?答:可以,并且最好把基类的析构函数定义成虚函数。
  5. 对象访问普通函数快还是虚函数更快?答:首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函
    数表中去查找。
  6. 虚函数表是在什么阶段生成的,存在哪的?答:虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的。
  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2022-08-19 18:45:50  更:2022-08-19 18:47:43 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/11 4:04:18-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码