C++进阶 多态原理

news2025/7/4 23:15:12

作者:@小萌新
专栏:@C++进阶
作者简介:大二学生 希望能和大家一起进步!
本篇博客简介:简单介绍C++中多态的概念

多态原理

  • 多态的原理
    • 虚函数表
    • 多态的原理
    • 为什么对象不能构成多态
    • 动态绑定和静态绑定
  • 继承多态面试题
    • 概念题
    • 问答题
  • 总结

多态的原理

虚函数表

我们先来看下面的一道题 :Base类实例化出对象的大小是多少?

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

有些同学可能看到这里会想到我们之前学习的类的大小计算

成员函数在公共区域 所以不算是类的大小 这里大小是四个字节

要是这样想 就被这个题目带进坑里面去了

我们先来看一下啊实际的大小是多少

在这里插入图片描述

实际大小是八 这就说明虚函数这里肯定是出问题了 那么问题出在哪里呢?

实际上 因为有虚函数的存在 它在内存中的分布应该是这样子的

在这里插入图片描述

指针的大小在32位的系统下是四个字节 因此我们最后算出来的结果才会是8

虚表指针指向一个虚函数表,简称虚表,每一个含有虚函数的类中都至少有一个虚表指针。

虚函数表中到底放的是什么?

我们写出下面的代码 设置三个对照组来对比一下

class person
{
public:
	virtual void func1()
	{
		cout << "func1" << endl;
	}

	virtual void func2()
	{
		cout << "func2" << endl;
	}

	void func3()
	{
		cout << "func3" << endl;
	}
private: 
	int _age = 1;
};


class child : public person
{
public:
	virtual void func1()
	{
		cout << "func1-child" << endl;
	}
private:
	int _name = 2;
};

在这里插入图片描述
我们可以发现 最后在内存中的结果变成了这样子 变成了一个指针 加上本来的成员变量

我们可以将它们抽象成下面的图

在这里插入图片描述
实际上_ptr指向的就是虚表的地址 虚表里面存放着虚函数的地址

由于child对于func2重写了 所以说两个虚表指针指向的虚表中 func2的地址不一样

func1的地址则相同(因为没有被重写)

而由于func3根本不是虚函数 所以说地址不会在虚表中

因为最后我们将func2的地址覆盖掉了 这也就是为什么我们原理层叫做覆盖 语法层叫做重写的原因

注意点: 我们一般会在虚表指针数组的最后放置一个空指针(nullptr)

那么我们在这里总结下 派生类虚表的生成步骤如下

  1. 先将基类中的虚表内容拷贝一份到派生类中的虚表
  2. 如果派生类中重写了某个虚函数 则使用重写后的虚函数地址覆盖之前的
  3. 派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后

那么接下来的问题又来了

虚表是什么阶段初始化的?虚函数存在哪里?虚表存在哪里?

  1. 虚表实际上是在构造函数初始化列表阶段进行初始化的

  2. 虚函数存放的位置和普通函数一样 都是存放在代码段

所以说我们虚表当中存放的地址并不是虚函数 而是指向虚函数的指针

  1. 虚表实际上也是存放在代码段的 我们可以使用下面的代码来证明
void test()
{
	person p;
	person* ptr = &p;
	printf("虚表地址:%p\n", *((int*)ptr));
	// 这里解释下上面这一步的原理
	// 我们首先将指针转化成int*类型 之后再解引用 就变成了int类型
	// 之后将这个int类型的数据再用地址的格式打印出来
	// 至于为什么要打印前面四个字节呢? 
	// 可以参考我们前面debug时候的内存图 前面四个字节就是虚表指针地址
	int i = 0;
	printf("栈上地址: %p\n", &i);
	printf("数据段地址: %p\n", &j);  // 全局变量再数据段

	int* k = new int;
	printf("堆上地址: %p\n", k);
	const char* cp = "hello world";
	printf("代码段地址: %p\n", cp);
}

运行结果如下

在这里插入图片描述
我们可以发现 虚表地址和代码段的地址是最相似的 所以说虚表在代码段当中

多态的原理

那到底多态的原理是什么?

还是一样 我们先来写代码

class person
{
public:
	virtual void buy_ticket()
	{
		cout << "买票 - 原价" << endl;
	}
private:
	int _a = 1;
};


class child : public person
{
public:
	virtual void buy_ticket()
	{
		cout << "买票 - 半价" << endl;
	}
private:
	int _b = 2;
};




int main()
{
	person p;
	child c;

	person* pptr = &p;
	person* cptr = &c;
	pptr->buy_ticket();
	cptr->buy_ticket();
	return 0;
}

在这里插入图片描述
这个时候我们再看看上面的图

学完了上面的知识我们大概就能明白

由pptr和cptr指针找到的函数地址(再虚表中) 是不一样的函数

因此 在不同对象去完成同一行为的时候发生了多态的现象

为什么对象不能构成多态

还记不记得我们之前在继承章节学过一个行为叫做切片

在这里插入图片描述

当我们使用指针或者引用切片的时候 我们本质上得到的是子类从父类派生过去的那一部分

而有了虚函数之后本质上就是前面多了一个虚表指针 也就是说

我们切片后的指针还有引用都还是使用的子类的虚表指针

但是如果是对象的切片呢?

在这里插入图片描述

这里实际上经历了一个拷贝构造的过程

构造出来的person对象 本质上是一个父类对象 所以说它的虚表指针也就是父类的虚表指针

自然无法构成多态

这里总结下:

  1. 构成多态 和对象有关
  2. 不构成多态 和类型有关

动态绑定和静态绑定

静态绑定: 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也成为静态多态,比如:函数重载。

动态绑定: 动态绑定又称为后期绑定(晚绑定),在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。

接下来我们通过汇编代码 来对其进行进一步的深入了解

首先 我们先验证下 是不是对象的确不能构成多态

class person
{
public:
	virtual void buy_ticket()
	{
		cout << "买票 - 原价" << endl;
	}
private:
	int _a = 1;
};


class child : public person
{
public:
	virtual void buy_ticket()
	{
		cout << "买票 - 半价" << endl;
	}
private:
	int _b = 2;
};




int main()
{
	person p;
	child c;

	person p1 = c;
	p1.buy_ticket();
	return 0;
}

我们可以看到它的汇编代码是这样子的

在这里插入图片描述
这里我们发现 就是一个普通的调用函数的过程

但是如果我们使用引用或者指针来调用

像这样

int main()
{
	person p;
	child c;

	person* p1 = &c;
	p1->buy_ticket();
	return 0;
}

我们再转到汇编代码

可以看到这样几行汇编

在这里插入图片描述
上面代码的意思实际上就是从虚基表中找到对应的函数地址然后调用的过程

这样就很好的体现了静态绑定是在编译时确定的,而动态绑定是在运行时确定的

继承多态面试题

概念题

1、下面哪种面向对象的方法可以让你变得富有()

A.继承 B.封装 C.多态 D.抽象

这个很显然 答案是A 继承 不用过多讲解

2、()是面向对象程序设计语言中的一种机制,这种机制实现了方法的定义与具体的对象无关,而方法的调用则可以关联于具体的对象。

A.继承 B.模板 C.对象的自身引用 D.动态绑定

本质上是多态 也就是动态绑定 这题选D

3、关于面向对象设计中的继承和组合,下面说法错误的是()

A.继承允许我们覆盖重写父类的实现细节,父类的实现对于子类是可见的,是一种静态复用,也称为白盒复用。
B.组合的对象不需要关系各自的实现细节,之间的关系是在运行时候才确定的,是一种动态复用,也称为黑盒复用。
C.优先使用继承,而不是组合,是面向对象设计的第二原则。
D.继承可以使子类能自动继承父类的接口,但在设计模式中认为这是一种破坏了父类的封装性的表现。

很明显 我们优先使用组合 而不是继承 所以C选项明显错误

4、以下关于纯虚函数的说法,正确的是()

A.声明纯虚函数的类不能实例化对象
B.声明纯虚函数的类是虚基类
C.子类必须实现基类的纯虚函数
D.纯虚函数必须是空函数

这里考察的是纯虚函数的概念 答案是A

5、关于虚函数的描述正确的是()

A.派生类的虚函数与基类的虚函数具有不同的参数个数和类型
B.内联函数不能是虚函数
C.派生类必须重新定义基类的虚函数
D.虚函数可以是一个static型的函数

这题很明显选B A C D都有明显的错误

6、关于虚表的说法正确的是()

A.一个类只能有一张虚表
B.基类中有虚函数,如果子类中没有重写基类的虚函数,此时子类与基类共用同一张虚表
C.虚表是在运行期间动态生成的
D.一个类的不同对象共享该类的虚表

A选项显然是错误的 父类既可以有父类的虚表也可以有子类的虚表

B显然也是错误的 子类基类不共用

C 虚表并不是动态产生的 它存在于代码段中

D D选项正确

7、假设A类中有虚函数,B继承自A,B重写A中的虚函数,也没有定义任何虚函数,则()

A.A类对象的前4个字节存储虚表地址,B类对象的前4个字节不是虚表地址
B.A类对象和B类对象前4个字节存储的都是虚基表的地址
C.A类对象和B类对象前4个字节存储的虚表地址相同
D.A类和B类虚表中虚函数个数相同,但A类和B类使用的不是同一张虚表

这一题考察的是虚表的概念 以及储存位置 答案是D

8、下面程序输出结果是什么?

#include <iostream>
using namespace std;
class A
{
public:
	A(char* s) { cout << s << endl; }
	~A() {};
};
class B : virtual public A
{
public:
	B(char* s1, char* s2)
		:A(s1)
	{
		cout << s2 << endl;
	}
};
class C : virtual public A
{
public:
	C(char* s1, char* s2)
		:A(s1)
	{
		cout << s2 << endl;
	}
};
class D : public B, public C
{
public:
	D(char* s1, char* s2, char* s3, char* s4)
		:B(s1, s2)
		, C(s1, s3)
		, A(s1)
	{
		cout << s4 << endl;
	}
};
int main()
{
	D* p = new D("class A", "class B", "class C", "class D");
	delete p;
	return 0;
}

A.class A class B class C class D
B.class D class B class C class A
C.class D class C class B class A
D.class A class C class C class D

这一题实际上是考察的继承的概念 先构造父类 再构造子类

和先析构子类再析构父类相反

所以说本题选A

9、下面说法正确的是?(多继承中指针的偏移问题)

class Base1
{
public:
	int _b1;
};
class Base2
{
public:
	int _b2;
};
class Derive : public Base1, public Base2
{
public:
	int _d;
};
int main()
{
	Derive d;
	Base1* p1 = &d;
	Base2* p2 = &d;
	Derive* p3 = &d;
	return 0;
}

A.p1 == p2 == p3
B.p1 < p2 < p3
C.p1 == p3 != p2
D.p1 != p2 != p3

答案是C 有关于指针偏移的问题 指针偏向于先继承的那个父类

10、以下程序输出结果是什么?

#include <iostream>
using namespace std;
class A
{
public:
	virtual void func(int val = 1)
	{
		cout << "A->" << val << endl;
	}
	virtual void test()
	{
		func();
	}
};
class B : public A
{
public:
	void func(int val = 0)
	{
		cout << "B->" << val << endl;
	}
};
int main()
{
	B* p = new B;
	p->test();
	return 0;
}

A.A->0 B.B->1 C.A->1 D.B->0
E.编译错误 F.以上都不正确

这一题考察的是缺省值的问题 记住结论就好 使用的是父类中的缺省值

问答题

1、什么是多态?

多态是指不同继承关系的类对象,去调用同一函数,产生了不同的行为。多态又分为静态的多态和动态的多态。

2、什么是重载、重写(覆盖)、重定义(隐藏)?

重载是指两个函数在同一作用域,这两个函数的函数名相同,参数不同。

重写(覆盖)是指两个函数分别在基类和派生类的作用域,这两个函数的函数名、参数、返回值都必须相同(协变例外),且这两个函数都是虚函数。

重定义(隐藏)是指两个函数分别在基类和派生类的作用域,这两个函数的函数名相同。若两个基类和派生类的同名函数不构成重写就是重定义。

3、多态的实现原理?

构成多态的父类对象和子类对象的成员当中都包含一个虚表指针,这个虚表指针指向一个虚表,虚表当中存储的是该类对应的虚函数地址。因此,当父类指针指向父类对象时,通过父类指针找到虚表指针,然后在虚表当中找到的就是父类当中对应的虚函数;当父类指针指向子类对象时,通过父类指针找到虚表指针,然后在虚表当中找到的就是子类当中对应的虚函数。

4、inline函数可以是虚函数吗?

我们知道内联函数是会在调用的地方展开的,也就是说内联函数是没有地址的,但是内联函数是可以定义成虚函数的,当我们把内联函数定义虚函数后,编译器就忽略了该函数的内联属性,这个函数就不再是内联函数了,因为需要将虚函数的地址放到虚表中去。

6、构造函数可以是虚函数吗?

构造函数不能是虚函数,因为对象中的虚表指针是在构造函数初始化列表阶段才初始化的

7、析构函数可以是虚函数吗?什么场景下析构函数是虚函数?

析构函数可以是虚函数,并且最后把基类的析构函数定义成虚函数。若是我们分别new一个父类对象和一个子类对象,并均用父类指针指向它们,当我们使用delete调用析构函数并释放对象空间时,只有当父类的析构函数是虚函数的情况下,才能正确调用父类和子类的析构函数分别对父类和子类对象进行析构,否则当我们使用父类指针delete对象时,只能调用到父类的析构函数

8、对象访问普通函数快还是虚函数更快?

对象访问普通函数比访问虚函数更快,若我们访问的是一个普通函数,那直接访问就行了,但当我们访问的是虚函数时,我们需要先找到虚表指针,然后在虚表当中找到对应的虚函数,最后才能调用到虚函数。

9、虚函数表是在什么阶段生成的?存在哪的?

虚表是在构造函数初始化列表阶段进行初始化的,虚表一般情况下是存在代码段(常量区)的。

10、C++菱形继承的问题?虚继承的原理?

菱形虚拟继承因为子类对象当中会有两份父类的成员,因此会导致数据冗余和二义性的问题。
虚继承对于相同的虚基类在对象当中只会存储一份,若要访问虚基类的成员需要通过虚基表获取到偏移量,进而找到对应的虚基类成员,从而解决了数据冗余和二义性的问题。

11、什么是抽象类?抽象类的作用?

抽象类很好的体现了虚函数的继承是一种接口继承,强制子类去抽象纯虚函数,因为子类若是不抽象从父类继承下来的纯虚函数,那么子类也是抽象类也不能实例化出对象。

其次,抽象类可以很好的去表示现实世界中没有示例对象对应的抽象类型,比如:植物、人、动物等。

总结

在这里插入图片描述
本篇博客简单介绍了C++中多态的原理

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

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

相关文章

【SpringBoot+Redis】实现多端登录+token自动续期和定期刷新+自定义注解和拦截器实现鉴权(角色和权限校验)

目录前言思路1、登录、token相关2、鉴权相关实现一、登录1、先定义一个Component组件2、登录、退出二、鉴权、token相关1、自定义注解2、拦截器鉴权、token续期和定期刷新3、新增/更新角色时&#xff0c;更新redis中角色对应的权限4、更新菜单权限标识时&#xff0c;更新redis中…

优优聚:美团成立机器人研究院!

美团成立机器人研究院 不用出门走路购买生活必须品&#xff0c;也不用等待几天的快递时间&#xff0c;现在的消费者越来越习惯“外卖点一切”、半小时送达的购物方式。 在即时零售市场中&#xff0c;美团&#xff0c;无疑是当下的焦点。 万万没想到的是&#xff0c;“外卖送一…

Java+MySQL基于SSM的二手玩具交换网站

本二手玩具交换网站主要包括系统用户管理模块、商品信息管理模块、所有购买记录、订单信息、登录模块、和退出模块等多个模块。它帮助二手玩具交换实现了信息化、网络化,通过测试,实现了系统设计目标,相比传统的管理模式,本系统合理的利用了二手玩具交换数据资源,有效的减少了二…

转行IT,女生学编程有前途吗?

一直以来&#xff0c;IT行业对技术的高要求让人们把这个行业标签为男生专属&#xff0c;从前只有个别女生顶着强大的压力、身边人的不理解坚守在IT岗位。 近些年随着互联网科技的发展与普及&#xff0c;很多女孩子发现原来IT技术没有自己想象中难&#xff0c;而且还可以毕业拿高…

Java项目:springboot课程自动排课系统

作者主页&#xff1a;源码空间站2022 简介&#xff1a;Java领域优质创作者、Java项目、学习资料、技术互助 文末获取源码 项目介绍 课程自动排课系统&#xff0c;该系统分两种角色&#xff1a;管理员与普通用户&#xff1b; 主要功能包括&#xff1a; 首页&#xff1a;查看分…

【运维有小邓】AD域权限报表

企业需要每天都警惕内部攻击和间谍等重大安全威胁。防范潜在的攻击者对保护组织的网络和数据大有裨益。但是&#xff0c;要实现此目标&#xff0c;还需满足几点。您必须完全了解分配给Windows Active Directory (AD)中用户和组的权限&#xff0c;它们可访问的帐户、资源和数据&…

java学习day64(乐友商城)Elasticsearch

1.Elasticsearch介绍和安装 用户访问我们的首页&#xff0c;一般都会直接搜索来寻找自己想要购买的商品。 而商品的数量非常多&#xff0c;而且分类繁杂。如何能正确的显示出用户想要的商品&#xff0c;并进行合理的过滤&#xff0c;尽快促成交易&#xff0c;是搜索系统要研究…

自己可以学习python吗?

现在还有很多小伙伴们在怀疑自学Python编程语言行不行&#xff0c;我想说Python自学是肯定行的&#xff0c;最重要的是要看自己的努力程度&#xff0c;Python是一个简单的编程语言&#xff0c;任何懂python语法规则的人都可以操作部署&#xff0c;更重要的是它是免费开源的&…

【学习笔记】前端HTML+CSS部分必懂基础内容(面试考察重点)

一、HTML 1. 什么是语义化&#xff1f;为什么要语义化&#xff1f;语义化标签有哪些 语义化&#xff1a;根据内容的结构化&#xff08;内容语义化&#xff09;&#xff0c;选择合适的标签&#xff08;代码语义化&#xff09;便于开发者阅读和写出更优雅的代码的同时让浏览器的…

试卷的安全方案

摘 要 随着互联网的飞速发展,传统的人工试卷保密措施已渐渐形成智能互联网加密保护。巨大的变革大大减少工作量&#xff0c;提升试卷质量&#xff0c;但随之而来的试卷拟定到发放以及回收的安全问题日益凸显。为了保护智能互联网试卷的保密性,认证授权加密已经成为了互联网传输…

MIUI10国际版系统自定义字体设置办法

国际版系统主题商店中没有字体设置。只有主题和壁纸 需要用到第三方主题安装工具mythemer和miui字体打包主题工具mifont maker 首先在网络上下载ttf格式的字体。 打开mifont maker&#xff0c;点击create custom font 点击pick font选择下载的字体 点击create miui font 制…

AlertDialog6种使用方法

AlertDialog 1.AlertDialog的6种创建模式 1.1setMessage 1&#xff09;Java代码 //1.创建构造器AlertDialog.Builder buildernew AlertDialog.Builder(this);//2.设置参数builder.setTitle("弹窗提示").setIcon(R.mipmap.boy).setMessage("选择你的性别&#xf…

刚毕业1年,做Python挣了60W!”网友:吹的不多..

现状揭秘 &#xff1a; Python岗位大厂50K起&#xff1f; 程序员&#xff1a; 心态崩了&#xff01; 屠杀各种榜单&#xff0c;拿下语言排行榜的Python&#xff0c;薪酬真的如同网传开挂了吗&#xff1f; 从上图看&#xff0c;Python薪酬普遍集中在 25-35k &#xff0c;也就是…

构建系列之新一代利器Esbuild(上)

What is Esbuild&#xff1f; Esbuild 是由 Figma 的 CTO 「Evan Wallace」基于 Golang 开发的一款打包工具&#xff0c;相比传统的打包工具&#xff0c;主打性能优势&#xff0c;在构建速度上可以快10~100 倍。 为什么会这么快&#xff1f; go实现&#xff0c;编译为本地代码…

Linux:阿里云服务器购买数据盘并挂载流程

1.进入ECS实例详情&#xff0c;找到‘创建云盘’按钮 2.进入购买界面并配置 3.确认订单等待自动挂载 4.返回实例&#xff0c;就能看到刚刚够买的数据盘 5.查看已挂载数据盘情况 df -lh6.查看所有数据盘包括未挂在的数据盘 fdisk -l 7.对数据盘进行分区。 fdisk /de…

maven中的pom

maven中的pompom的最低要求配置pom的默认行为packaging有哪些关于dependencytypescope关于 Dependency Management构建maven聚合工程&#xff0c;父子工程maven官方文档 !!!pom的最低要求配置 总共5个 project-根元素modelVersion -设置为4.0.0即可groupId-项目分组的idartif…

Polygon zkEVM发布公开测试网2.0

1. 引言 Polygon zkEVM发布公开测试网2.0&#xff0c;相比于10月份发布的公开测试网1.0版本&#xff0c;做了如下改进&#xff1a; 支持递归证明&#xff08;testnet1.0采用的是one batch of transactions对应one proof&#xff09;&#xff1a;从而支持多个provers并行工作&…

【架构师(第五十三篇)】 性能优化之 HTTP 缓存

ETag ETagHTTP 响应头是资源的特定版本的标识符&#xff0c;这可以让缓存更高效&#xff0c;并节省带宽&#xff0c;因为如果内容没有改变&#xff0c;web 服务器不需要发送完整的响应。 第二次请求的时候会添加一个 If-None-Match 请求头&#xff0c;去判断文件是否发生过变化…

[思维模式-11]:《如何系统思考》-7- 认识篇 - 克服片面、局部思维,转向全面思考 =》 UML

目录 第1章 全面思考概述&#xff08;空间&#xff09; 1.1 什么是全面思考&#xff08;整体思考&#xff09; 1.2 全面思考的含义 1.3 程序的局部性原理 第2章 如何做到全面思考 2.1 本位思考 》 全局思考 2.2 大局观&#xff0c;既是一种格局&#xff0c;也是一种能力…

【SpringMVC】下篇,拦截器(一步到位学会它)

✅作者简介&#xff1a;热爱Java后端开发的一名学习者&#xff0c;大家可以跟我一起讨论各种问题喔。 &#x1f34e;个人主页&#xff1a;Hhzzy99 &#x1f34a;个人信条&#xff1a;坚持就是胜利&#xff01; &#x1f49e;当前专栏&#xff1a;【Spring】 &#x1f96d;本文内…