C++:多态的底层实现原理 -- 虚函数表

这篇具有很好参考价值的文章主要介绍了C++:多态的底层实现原理 -- 虚函数表。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

目录

一. 多态的原理

1.1 虚函数表

1.2 多态的实现原理

1.3 动态绑定与静态绑定

二. 多继承中的虚函数表

2.1 虚函数表的打印

2.2 多继承中虚函数表中的内容存储情况


一. 多态的原理

1.1 虚函数表

对于一个含有虚函数的的类,在实例化出来对象以后,对象所存储的内容包含两部分:

  • 类的成员变量。
  • 一个指向虚函数表得虚函数表指针。

下段代码定义了一个Base类,其中包含虚函数func1以及一个int型数据,在main函数中,使用sizeof(Base)计算这个类实例化出来的对象大小为8bytes而不是4bytes,这正是因为虚函数表指针占了4bytes的存储空间(32位编译环境)。

class Base
{
public:
	virtual void func() { std::cout << "Base::func()" << std::endl; }

	int _b = 1;
};

int main()
{
	Base b;
	std::cout << sizeof(b) << std::endl;  //8
	return 0;
}

如果要调用Base中定义的虚函数func,那么程序会在运行时根据虚函数指针找到虚函数表,虚函数表中存有函数指针(函数所在地址),程序会根据虚函数表中存储的虚函数所在地址,找到对应的函数进行调用。

C++:多态的底层实现原理 -- 虚函数表
图1.1 虚函数指针和虚函数表

1.2 多态的实现原理

多态的实现,是通过虚函数的重写来实现的。对于一个包含虚函数的基类Base,设有一派生类Derive继承了基类Base,那么Derive会将Base的虚函数表一并继承下来。

  • 如果Derive中没有对Base中的虚函数进行重写,那么Derive和Base各自拥有不同的虚函数表,两者虚函数表中存储的内容相同。
  • 如果Derive对Base的虚函数完成了重写,那么虚函数表中的被重写的虚函数的地址会被覆盖,更新为派生类中对应的虚函数地址。

演示代码1.2中定义了一个基类Base和一个派生类Derive,Base中定义了两个虚函数func1和func2,在派生类Derive中,func2被重写了,func1没有被重写。运行代码,打开内存监视窗口,可以看到,在Derive对象的虚函数表中,func2的地址和Base对象中的不一样,而func1的地址一样,这证明了func2被重写后,其记录在虚函数表中的地址被覆盖了。

演示代码1.2:

#include<iostream>

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

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

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

	int _d = 1;
};

int main()
{
	Base b;
	Derive d;
	return 0;
}
C++:多态的底层实现原理 -- 虚函数表
图1.2 继承体系中的虚函数表及虚函数覆盖情况

我们知道,多态的条件之一,就是通过父类的指针或引用去调用,可以从这个调用条件为切入点,分析多态中函数调用的流程,来探索多态的底层原理,多态中函数调用流程为:

  1. 将父类或子类的对象(地址)赋值给父类的引用(指针)。
  2. 父类的对象或引用,根据其实际表示的对象或指向,拿到对应的虚表函数指针,在虚函数表中找到要调用的虚函数地址,来调用对应的函数。

正是由于子类对象中完成了对父类对象虚函数的重写,所以在子类对象完成虚函数操作时,会执行子类中定义的虚函数。多态中的虚函数调用,是通过获取虚函数表中的函数指针来确定具体调用哪个函数的,由于父类对象和子类对象的虚函数表中存储不同的虚函数指针,所以会调用不同的虚函数,从而实现了多态。

关于多态中虚函数表的生成和覆盖,总结出以下几点关键内容:

  • 虚函数表本质是一个存虚函数指针的指针数组,在VS编译环境下,这个数组最后面放了一个nullptr,但是在Linux gcc编译环境下,后面不会存有nullptr。
  • 派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中  b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数  c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
  • 虚函数表中存的是虚函数指针,而不是虚函数。虚函数和普通函数一样,存储在代码段,对象中存储的也不是虚函数表,而是虚函数表指针,其指向虚函数表所在的地址。

1.3 动态绑定与静态绑定

  • 静态绑定:静态绑定又称为前期绑定,表示在程序编译期间就可以确定程序完整的行为,函数的普通调用就是静态绑定,在编译时,就能明确哪个函数会被调用,动态绑定也可称为编译时决议。
  • 动态绑定:在多态调用的场景下,需要在程序运行时,根据虚函数表中的函数地址,来确定调用哪个函数,即:程序运行起来之后才能明确程序的具体行为,动态绑定也可称为运行时决议。
  • 函数普通调用为编译时决议,函数的多态调用为运行时决议。

二. 多继承中的虚函数表

2.1 虚函数表的打印

为了探索多继承中虚函数的行为,我们需要定义一个PrintVFTalbe函数,来打印虚函数所存储的地址,并通过虚函数表中存储的函数指针调用对应的函数,来观察虚函数表中的函数指针与子类和父类虚函数的指向关系。

因为VS编译器会在虚函数表末尾位置存储nullptr,所以使用Table[i] != nullptr作为循环结束的判断条件(Linux gcc编译器不会将在虚函数表最后放nullptr,必须显示地给定虚函数表中存储的函数指针的个数)。在每层循环内部,先打印函数指针(函数首条指令地址),然后将函数指针变量赋值给ptr,通过函数指针调用函数。

演示代码2.1:(虚函数表打印函数)

typedef void (*VFPTR)();  //将指向无参数、返回void的函数的函数指针类型重定义为VFPTR

void PrintfVFTable(VFPTR* table)
{
	for (size_t i = 0; table[i] != nullptr; ++i)
	{
		printf("第%d个虚函数的地址:%p -> ", i, table[i]);
		VFPTR ptr = table[i];   //获取函数指针
		ptr();   //通过函数指针调用函数
	}
	std::cout << std::endl;
}

2.2 多继承中虚函数表中的内容存储情况

编写演示代码2.2,其中定义了两个父类Base1和Base2,两个父类中都定义了func1和func2虚函数,并且,在子类Derive中,重写func1函数,并且定义了一个新的虚函数func3。

调试代码,打开监视窗口,我们可以发现,子类对象中包含的两个父类对象各有一张虚函数表,但是,VS的监视窗口并没有显示出虚函数func3的地址,这并不是说func3的地址没有进虚函数表,而是VS编译器没有将其显示出来,可以认为这是编译器的一个小BUG。

演示代码2.2:

class Base1 
{
public:
	virtual void func1() { std::cout << "Base1::func1" << std::endl; }
	virtual void func2() { std::cout << "Base1::func2" << std::endl; }
private:
	int _b1 = 1;
};

class Base2 
{
public:
	virtual void func1() { std::cout << "Base2::func1" << std::endl; }
	virtual void func2() { std::cout << "Base2::func2" << std::endl; }
private:
	int _b2 = 2;
};

class Derive : public Base1, public Base2 
{
public:
	virtual void func1() { std::cout << "Derive::func1" << std::endl; }
	virtual void func3() { std::cout << "Derive::func3" << std::endl; }
private:
	int _d1 = 3;
};

int main()
{
	Derive d;
	Base1* ptr1 = &d;
	Base2* ptr2 = &d;

	PrintfVFTable((VFPTR*)*(int*)ptr1);  //打印Base1的虚函数表
	PrintfVFTable((VFPTR*)*(int*)ptr2);  //打印Base2的虚函数表

	return 0;
}
C++:多态的底层实现原理 -- 虚函数表
图2.1 VS2019调试演示代码2.2的监视窗口

由于VS编译器的这个小“bug”,就要求我们显示的打印虚函数表,将虚函数指针作为参数,传给虚函数表打印函数。可见。Base1的虚函数表中存储了三个函数指针,从前到后依次为:子类定义的func1、Base1中定义的func2、func3,Base2的虚表中存储了3个函数指针,从前到后依次为:子类中定义的func1、Base2的func2。

C++:多态的底层实现原理 -- 虚函数表
图2.2  多继承体系中的虚表及虚表中存储的内容

根据图2.2所示的虚表打印情况,总结出多继承体系中如下的规律:文章来源地址https://www.toymoban.com/news/detail-419927.html

  1. 在多继承体系中,每一个基类都有一张虚表。
  2. 如果两个基类之中存在同名的虚函数,同时在派生类中对同名的虚函数重写,那么这两个派生类中的虚函数都会被覆盖。
  3. 派生类中未被重写的虚函数,会被存入第一个基类的虚表之中。
C++:多态的底层实现原理 -- 虚函数表
图2.3 多继承体系中的内存模型

到了这里,关于C++:多态的底层实现原理 -- 虚函数表的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包赞助服务器费用

相关文章

  • C++:多态的内容和底层原理

    C++:多态的内容和底层原理

    本篇总结 C++ 中多态的基本内容和原理实现和一些边角内容 首先要清楚多态是什么,是用来做什么的? 多态从字面意思来讲,就是多种形态, 完成一个事情,不同的人去完成会有不同的结果和状态 ,这样的情况就叫做多态 多态是不同继承关系的类对象,在调用一个函数的时

    2024年02月08日
    浏览(11)
  • C++修炼之路之多态---多态的原理(虚函数表)

    C++修炼之路之多态---多态的原理(虚函数表)

    目录 一:多态的原理  1.虚函数表  2.原理分析 3.对于虚表存在哪里的探讨 4.对于是不是所有的虚函数都要存进虚函数表的探讨 二:多继承中的虚函数表 三:常见的问答题  接下来的日子会顺顺利利,万事胜意,生活明朗-----------林辞忧  接上篇的多态的介绍后,接下来介绍

    2024年04月26日
    浏览(14)
  • C++中菱形继承中的多态在底层是如何实现的。

    C++中菱形继承中的多态在底层是如何实现的。

    如果还不了解菱形继承和多态的底层可以看这两篇文章: C++中多态的底层实现_Qianxueban的博客-CSDN博客 C++的继承以及virtual的底层实现_Qianxueban的博客-CSDN博客

    2024年02月09日
    浏览(11)
  • 【C++】虚函数表 & 多态的原理 & 动态绑定和静态绑定

    【C++】虚函数表 & 多态的原理 & 动态绑定和静态绑定

    梳理虚函数表、多态原理、动静态绑定的知识 目录 一、虚函数表 二、多态的原理 三、动态绑定和静态绑定 在学习多态原理之前,我们需要了解一下虚函数表的概念  我们先一起来看下下面这段代码 通过测试我们发现b对象是8bytes, 除了_b成员,还多一个__vfptr指针放在对象

    2024年02月03日
    浏览(11)
  • C++类和对象-多态->多态的基本语法、多态的原理剖析、纯虚函数和抽象类、虚析构和纯虚析构

    C++类和对象-多态->多态的基本语法、多态的原理剖析、纯虚函数和抽象类、虚析构和纯虚析构

    #includeiostream using namespace std; //多态 //动物类 class Animal { public:     //Speak函数就是虚函数     //函数前面加上virtual,变成虚函数,那么编译器在编译的时候就不能确定函数调用了。     virtual void speak()     {         cout \\\"动物在说话\\\" endl;     } }; //猫类 class Cat

    2024年02月20日
    浏览(13)
  • [C++] C++入门第一篇 -- 命名空间,输入输出,缺省函数,函数重载底层原理

    [C++] C++入门第一篇 -- 命名空间,输入输出,缺省函数,函数重载底层原理

      目录 1、  2、命名空间 2.1 命名空间的定义 2.2 命名空间的使用方式 2.2.1 加命名空间名称及作用域限定符 2.2.2 使用using将命名空间中某个成员引入 2.2.3 使用using namespace 命名空间名称引入 3、C++输入与输出 4、缺省参数 4.1 缺省参数的概念 4.2 缺省参数分类 4.2.1 全缺省参

    2024年02月15日
    浏览(10)
  • 【C++庖丁解牛】面向对象的三大特性之一多态 | 抽象类 | 多态的原理 | 单继承和多继承关系中的虚函数表

    【C++庖丁解牛】面向对象的三大特性之一多态 | 抽象类 | 多态的原理 | 单继承和多继承关系中的虚函数表

    🍁你好,我是 RO-BERRY 📗 致力于C、C++、数据结构、TCP/IP、数据库等等一系列知识 🎄感谢你的陪伴与支持 ,故事既有了开头,就要画上一个完美的句号,让我们一起加油 需要声明的,本节课件中的代码及解释都是在vs2013下的x86程序中,涉及的指针都是4bytes。如果要其他平台

    2024年04月10日
    浏览(14)
  • [C++]:万字超详细讲解多态以及多态的实现原理(面试的必考的c++考点)

    [C++]:万字超详细讲解多态以及多态的实现原理(面试的必考的c++考点)

    文章目录 前言 一、多态的定义及实现 1.多态的构成条件 2.c++11的override和final 3.重载,重写,重定义的比较 4.抽象类 5.多态的原理 6.多继承中的虚函数表 7.动态绑定和静态绑定 总结 多态的概念: 多态的概念:通俗来说,就是多种形态, 具体点就是去完成某个行为,当不同的

    2023年04月22日
    浏览(12)
  • C++中的多态是什么?如何实现多态?解释一下C++中的虚函数和纯虚函数,它们的作用是什么?

    C++中的多态是什么?如何实现多态? 在C++中,多态(Polymorphism)是面向对象编程的三大特性之一,另外两个是封装(Encapsulation)和继承(Inheritance)。多态指的是允许一个接口(或一个父类引用)在多种数据类型上被实现,或者一个接口被多个不同的类以不同的方式实现。

    2024年02月19日
    浏览(19)
  • 【C++学习】第六章多态与虚函数案例实现

    虚函数的作用就是为了实现多态,和php的延时绑定是一样的。 函数重载是静态的,在横向上的功能, 虚函数是类继承上的功能,是动态的。

    2024年02月09日
    浏览(11)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包