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++知识库]多态的原理和面试问题

以前学习多态部分的时候总觉得囫囵吞枣,感觉懂了但是又觉得没太理解,今天研究了一下,索性就记录一下吧。

多态需要发生的条件:

  1. 父类的指针或者引用指向子类对象
  2. 父类的函数必须要是虚函数
  3. 子类必须要对父类的虚函数进行重写。

需要注意的是第3点:这里子类中对应的函数和父类中的函数必须长得一模一样,也意味着除了函数内部的实现机制不一样,函数的外壳(名称,返回值,参数类型大小)必须要一模一样。
为什么要有如此严格的限制呢,多态机制就是依靠对象中的虚函数表来实现的。我们一点点来分析。
如果一个对象中的成员函数不是虚函数时,为了节省空间,我们知道函数是被保存在公共代码区的。对象a的大小为4个字节,代码如下所示:

class Base
{
public:
	void Func1()
	{
		cout << "Base::Func1()" << endl;
	}
	void Func2()
	{
		cout << "Base::Func2()" << endl;
	}
	void Func3()
	{
		cout << "Base::Func3()" << endl;
	}
private:
	int _b = 1;
};
Base a;
cout<<sizeof(a)<<endl;//4

当我们尝试使得Func1和Func2变为虚函数时,再来看看a的大小,此时对象a的大小已经变成了8个字节。

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;
};
Base a;
cout<<sizeof(a)<<endl;

父类的虚函数表:

编译器会为具有虚函数的类创建一个虚函数表,该虚函数表被该类的所有对象共享,虚函数表中存在的是每一个虚函数的地址,如下图所示。
在这里插入图片描述

虚表指针:

所有的对象共用1张虚函数表,为了指定对象的虚表,对象内部包含一个虚表的指针,来指向自己所使用的虚表。因此编译器在类中添加了一个指针_vfptr,用来指向虚函数表,它是一个二级指针。
由于对象a和对象b都属于Base类,因此_vfptr的值是相同的。

打印虚函数表的地址:

int main()
{
	Base a;
	Base b;
	cout << "a:_vsfptr虚表地址" << (int*)(*(int*)(&a)) << endl;;
	cout << "b:_vsfptr虚表地址" << (int*)(*(int*)(&b)) << endl;
}

在这里插入图片描述

子类的虚函数表:

下面我们看如果子类对父类进行继承,同时对父类的虚函数重写之后会发生什么现象。
子类定义如下:对父类的Func1()进行了重写,而且自身还有1个虚函数Func4()

class Derive : public Base
{
public:
	virtual void Func1()
	{
		cout << "Derive::Func1()" << endl;
	}
	virtual void Func4()
	{
		cout << "Derive::Func4()" << endl;
	}
private:
	int _d = 2;
};
int main()
{
	Base a;
	Derive b;
	cout << "父类虚表地址" << (int*)(*(int*)(&a)) << endl;;
	cout << "子类虚表地址" << (int*)(*(int*)(&b)) << endl;
}

在这里插入图片描述
可以看出来父类和子类虚函数表的地址是不一样的,因此子类和父类有两张不同的虚函数表,对应的内存模型如下:
在这里插入图片描述
注意,使用visual studio查看b的时候,由于b是继承自Base类的,因此通过编译器查看的时候:我们只能看到b下面的Base类我们只能看到Fun1()和Fun2(),而无法看到b中的Fun4()。
在这里插入图片描述
通过以下代码可以查看到Fun4()是真实存在的,注意这里强转为int类型的指针是因为函数的地址也是4个字节大小,方便操作,具体的注释如下所示:

int main()
{
	函数指针重新定义为Fun类型
	typedef void(*Fun)(void);
	Fun pfun = NULL;
	Base a;
	Derive b;
	虚函数表第一行的地址
	int* vt_one = (int*)(*(int*)(&b));
	虚函数表第二行的地址
	int* vt_two = (int*)(*(int*)(&b)) + 1;
	虚函数表的第三行的地址
	int* vt_three = (int*)(*(int*)(&b)) + 2;
	对首地址解引用得到的第一个虚函数的地址,强转为函数指针类型
	pfun = (Fun) * (vt_one);
	调用函数
	pfun();
	对第二行的地址解引用得到的第二个虚函数的地址
	pfun = (Fun) * (vt_two);
	pfun();
	pfun = (Fun) * (vt_three);
	pfun();
}

结果如下所示:可以看到Derive:Func4()函数的存在
在这里插入图片描述

多态的原理解释:

当父类的指针指向不同的子类对象时,调用父类对应的函数,会触发不同的功能,看起来很玄学,但其实正是虚函数表作祟的原因。
由于父类的指针指向子类的对象,因此实际上访问的仍然是子类的对象。但是由于该指针是父类类型的,所以你只能看到子类中父类的那一部分,子类中有父类中没有的那部分当然就看不到了,就像上面提到的Func4函数一样,站在父类的角度上,是看不到该函数的。
因为子类函数因为和父类函数长得一模一样,因此子类虚函数的地址覆盖掉原本父类中相对应函数的地址,所以通过父类指针调用时,才可以调用子类的函数。

int main()
{
	typedef void(*Fun)(void);
	Fun pfun = NULL;
	Base a;
	Derive b;
	Base *c = &b;
	a.Func1();
	c->Func1();
	c->Func4()//会报错
	b.Func4();//正常
}

在这里插入图片描述
对应的内存模型如下:注意这里通过c是访问不了Func4()了,大家可以和上面的图对比。
在这里插入图片描述
那么如果不是指针或者引用,仅仅通过拷贝赋值的话,是无法达到这个效果的,因为a所指向的地址和b所指向的地址是不一样的。故c的虚函数表还是最初的父类的虚函数表,和子类的虚表无关。

Base a;
Derive b;
// 拷贝赋值,子类可以转化为父类(子类的东西多于父类哦),反之不可以的。
Base c = b;
a.Func1();
c.Func1();

在这里插入图片描述

常见的面试题目总结:

1.inline, static, constructor三种函数都不能带有virtual关键字
inline函数没有地址,无法将地址放入虚函数表中,它是直接插入到运行的地方中。它在编译时被确定,而虚函数只有运行的时候才可以确定。
static函数没有this指针,virtual函数一定要通过对象来调用。
构造函数也不能是虚函数,因为对象中的虚表指针是在构造函数初始化列表中才初始化的。

2. 析构函数可以是虚函数吗?
最好定义为虚函数,而且是必须的。否则有可能造成内存泄漏,如果子类对象中包含指针成员,由于析构函数没有进行重写,因此只会调用父类的析构函数,而不会调用子类的析构函数。

3.什么是抽象类,抽象类的作用是什么?
在虚函数后面写上=0,则这个函数是纯虚函数。包含纯虚函数的类称为抽象类,抽象类无法实例化对象,派生类继承之后必须要重写虚函数,才可以实例化出对象。
虚函数的继承体现的是一种接口继承,继承的是基类虚函数的接口,目的是为了重写,达成多态。

  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2022-05-09 12:22:52  更:2022-05-09 12:25:59 
 
开发: 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/20 21:17:53-

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