我的编程空间,编程开发者的网络收藏夹
学习永远不晚

C/C++多态原理实例分析

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

C/C++多态原理实例分析

本篇内容介绍了“C/C++多态原理实例分析”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!

多态

面向对象编程有三大特性:继承、封装和多态。

其中,多态又分为编译时多态和运行时多态。编译多态是通过重载函数体现的,运行多态是通过虚函数体现的。

多态是如何实现的呢?下面举个例子:

#include <iostream>using namespace std;class Base {public:virtual void fun() {cout << " Base::func()" << endl;}void fun1(int a) {cout << "Base::func1()" << endl;}void fun2(int a, int b) {cout << "Base::func2()" << endl;}};class Son1 : public Base {public:virtual void fun() override {cout << " Son1::func()" << endl;}};class Son2 : public Base {};int main(){cout << "编译时多态" << endl;Base* base1 = new Base;base1->fun1(1);base1->fun2(1,1);cout << "运行时多态" << endl;Base* base = new Son1;base->fun();base = new Son2;base->fun();delete base;base = NULL;return 0;}

结果:

C/C++多态原理实例分析

在例子中

  • 由于Base类中 fun1 和 fun2 函数签名不同(其中,函数后面是否有const 也是签名的一部分),从结果分析实现重载,体现了多态性。

  • Base为基类,其中的函数为虚函数。子类1继承并重写了基类的函数,子类2继承基类但没有重写基类的函数,从结果分析子类体现了多态性。

那么为什么会出现多态性,其底层的原理是什么?这里需要引出一些相关的概念来进行解释。

虚表和虚表指针

  • 虚表:虚函数表的缩写,类中含有virtual关键字修饰的方法时,编译器会自动生成虚表

  • 虚表指针:在含有虚函数的类实例化对象时,对象地址的前四个字节存储的指向虚表的指针

父类对象模型:

C/C++多态原理实例分析

子类对象模型:

C/C++多态原理实例分析

上图中展示了虚表和虚表指针在基类对象和派生类对象中的模型,下面阐述实现多态的过程:

(1)编译器在发现基类中有虚函数时,会自动为每个含有虚函数的类生成一份虚表,该表是一个一维数组,虚表里保存了虚函数的入口地址

(2)编译器会在每个对象的前四个字节中保存一个虚表指针,即vptr,指向对象所属类的虚表。在构造时,根据对象的类型去初始化虚指针vptr,从而让vptr指向正确的虚表,从而在调用虚函数时,能找到正确的函数

(3)所谓的合适时机,在派生类定义对象时,程序运行会自动调用构造函数,在构造函数中创建虚表并对虚表初始化。在构造子类对象时,会先调用父类的构造函数,此时,编译器只“看到了”父类,并为父类对象初始化虚表指针,令它指向父类的虚表;当调用子类的构造函数时,为子类对象初始化虚表指针,令它指向子类的虚表

(4)当派生类对基类的虚函数没有重写时,派生类的虚表指针指向的是基类的虚表;当派生类对基类的虚函数重写时,派生类的虚表指针指向的是自身的虚表;当派生类中有自己的虚函数时,在自己的虚表中将此虚函数地址添加在后面这样指向派生类的基类指针在运行时,就可以根据派生类对虚函数重写情况动态的进行调用,从而实现多态性。

下面在VS2019环境下,通过程序展现:

代码部分:

#include <iostream>using namespace std;class A {public:virtual void vfunc1() {cout << "A::vfunc1() -> ";}virtual void vfunc2() {cout << "A::vfunc2() -> " ;}void func1() {cout << "A::func1() -> " ;}void func2() {cout << "A::func2() -> " ;}int m_data1, m_data2;};class B : public A {public:virtual void vfunc1() {cout << "B::vfunc1() -> " ;}void func2() {cout << "B::func2() -> " ;}int m_data3;};class C : public B {public:virtual void vfunc1() {cout << "C::vfunc1() -> " ;}void func2() {cout << "C::func2() -> " ;}int m_data1, m_data4;};int main(){//  这里指针操作比较混乱,在此稍微解析下://  *****printf("虚表地址:%p\n", *(int *)&b); 解析*****://  1.&b代表对象b的起始地址//  2.(int *)&b 强转成int *类型,为了后面取b对象的前四个字节,前四个字节是虚表指针//  3.*(int *)&b 取前四个字节,即vptr虚表地址////  *****printf("第一个虚函数地址:%p\n", *(int *)*(int *)&b);*****://  根据上面的解析我们知道*(int *)&b是vptr,即虚表指针.并且虚表是存放虚函数指针的//  所以虚表中每个元素(虚函数指针)在32位编译器下是4个字节,因此(int *)*(int *)&b//  这样强转后为了后面的取四个字节.所以*(int *)*(int *)&b就是虚表的第一个元素.//  即f()的地址.//  那么接下来的取第二个虚函数地址也就依次类推.  始终记着vptr指向的是一块内存,//  这块内存存放着虚函数地址,这块内存就是我们所说的虚表.cout << "class A 成员函数、成员变量的地址::" << endl;A a;cout << "A::vptr 地址 :" << *(int*)&a << endl;cout << "A::vtbl 地址 :" << *(int*)*(int*)&a << endl;cout << "A::vtbl 地址 :" << *((int*)*(int*)(&a) + 1) << endl;union {void* pv;void(A::* pfn)();} u;u.pfn = &A::vfunc1;(a.*u.pfn)();cout << u.pv << endl;u.pfn = &A::vfunc2;(a.*u.pfn)();cout << u.pv << endl;u.pfn = &A::func1;(a.*u.pfn)();cout << u.pv << endl;u.pfn = &A::func2;(a.*u.pfn)();cout << u.pv << endl;cout << "class B 成员函数、成员变量的地址::" << endl;B b;cout << "B::vptr 地址 :" << *(int*)&b << endl;cout << "B::vtbl 地址 :" << *(int*)*(int*)&b << endl;cout << "B::vtbl 地址 :" << *((int*)*(int*)(&b) + 1) << endl;union {void* pv;void(B::* pfn)();} m;m.pfn = &B::vfunc1;(b.*m.pfn)();cout << m.pv << endl;m.pfn = &B::vfunc2;(b.*m.pfn)();cout << m.pv << endl;m.pfn = &B::func1;(b.*m.pfn)();cout << m.pv << endl;m.pfn = &B::func2;(b.*m.pfn)();cout << m.pv << endl;cout << "class C 成员函数、成员变量的地址::" << endl;C c;cout << "C::vptr 地址 :" << *(int*)&c << endl;cout << "C::vtbl 地址 :" << *(int*)*(int*)&c << endl;cout << "C::vtbl 地址 :" << *((int*)*(int*)(&c) + 1) << endl;union {void* pv;void(C::* pfn)();} n;n.pfn = &C::vfunc1;(c.*n.pfn)();cout << n.pv << endl;n.pfn = &C::vfunc2;(c.*n.pfn)();cout << n.pv << endl;n.pfn = &C::func1;(c.*n.pfn)();cout << n.pv << endl;n.pfn = &C::func2;(c.*n.pfn)();cout << n.pv << endl;}

运行结果:

C/C++多态原理实例分析

整个程序图示:

C/C++多态原理实例分析

通过图示我们可以看出,函数在构造后,通过vptr寻找到vtbl,进而得到所对应的成员函数。而它是怎么做到寻找到所需要的是父类还是子类的成员函数呢?

这里就要提到另一个隐藏的指针,this指针。

this指针是隐藏在类里面的一个指针,它指向当前对象,通过它可以访问当前对象的所有成员。

如程序中如果出现:

    C c;
    c.vfunc1();

其实编译器会对其进行处理,从直观上可以将 vfunc1() 看作是下面形式(不知编译器是否这样转换):

    c.A::vfunc1(&c);

其中,&c就是隐藏的this指针,通过this指针,进而得到c对象需要的成员函数。

同时,这里面还包括另一个C++语法:动态绑定和静态绑定

  • 静态绑定:绑定的是静态类型,所对应的函数或属性依赖于对象的静态类型,发生在编译期;

  • 动态绑定:绑定的是动态类型,所对应的函数或属性依赖于对象的动态类型,发生在运行期;

从上面的定义也可以看出,非虚函数一般都是静态绑定,而虚函数都是动态绑定(如此才可实现多态性)。

所以,我们在上面代码中加入一些代码如下:

    B bb;
    A aa = (A)bb;
    aa.vfunc1();

同时,加入断点,进行调试,通过vs2019窗口查看反汇编代码,我们得到如下代码:

    B bb;
00B63237  lea         ecx,[bb]  
00B6323D  call        B::B (0B6129Eh)  
    A aa = (A)bb;
00B63242  lea         eax,[bb]  
00B63248  push        eax  
00B63249  lea         ecx,[aa]  
00B6324F  call        A::A (0B6128Ah)  
    aa.vfunc1();
00B63254  lea         ecx,[aa]  
00B6325A  call        A::vfunc1 (0B6111Dh)  

由于,aa是一个A的对象而非指针,即使a内容是B对象强制转换而来,aa.vfunc1()调用的是静态绑定的A::vfunc1()。同时,在汇编中我们得到,在调用时,直接call xxxx,call后面是一个固定的地址,从这里依旧可以看出是静态绑定。

同时,我们继续运行下面代码:

    A* pa = new B;
    pa->vfunc1();

    pa = &b;
    pa->vfunc1();

得到如下反汇编:

    A* pa = new B;
00B6325F  push        10h  
00B63261  call        operator new (0B6114Fh)  
00B63266  add         esp,4  
00B63269  mov         dword ptr [ebp-174h],eax  
00B6326F  cmp         dword ptr [ebp-174h],0  
00B63276  je          __$EncStackInitStart+68Fh (0B6328Bh)  
00B63278  mov         ecx,dword ptr [ebp-174h]  
00B6327E  call        B::B (0B6129Eh)  
00B63283  mov         dword ptr [ebp-17Ch],eax  
00B63289  jmp         __$EncStackInitStart+699h (0B63295h)  
00B6328B  mov         dword ptr [ebp-17Ch],0  
00B63295  mov         eax,dword ptr [ebp-17Ch]  
00B6329B  mov         dword ptr [pa],eax  
    pa->vfunc1();
00B632A1  mov         eax,dword ptr [pa]  
00B632A7  mov         edx,dword ptr [eax]  
00B632A9  mov         esi,esp  
00B632AB  mov         ecx,dword ptr [pa]  
00B632B1  mov         eax,dword ptr [edx]  
00B632B3  call        eax  
00B632B5  cmp         esi,esp  
00B632B7  call        __RTC_CheckEsp (0B61316h)    //并非固定地址

    pa = &b;
00B632BC  lea         eax,[b]  
00B632BF  mov         dword ptr [pa],eax  
    pa->vfunc1();
00B632C5  mov         eax,dword ptr [pa]  
00B632CB  mov         edx,dword ptr [eax]  
00B632CD  mov         esi,esp  
00B632CF  mov         ecx,dword ptr [pa]  
00B632D5  mov         eax,dword ptr [edx]  
00B632D7  call        eax  
00B632D9  cmp         esi,esp  
00B632DB  call        __RTC_CheckEsp (0B61316h)  

在下面这段程序中,我们可以看到,指针pa指向一个B对象,有一个向上转型操作,可以确定,这应该是动态绑定。同时,在汇编代码中,call后面并不是一个固定的地址,从这里我们也可以看出pa调用了B::vfunc1()。

“C/C++多态原理实例分析”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识可以关注编程网网站,小编将为大家输出更多高质量的实用文章!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

C/C++多态原理实例分析

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

C/C++多态原理实例分析

本篇内容介绍了“C/C++多态原理实例分析”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!多态面向对象编程有三大特性:继承、封装和多态。其中,
2023-07-02

C++多态的实现与原理及抽象类实例分析

这篇文章主要讲解了“C++多态的实现与原理及抽象类实例分析”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“C++多态的实现与原理及抽象类实例分析”吧!多态的概念多态: 从字面意思来看,就是事物
2023-06-29

C++中多态的示例分析

小编给大家分享一下C++中多态的示例分析,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!1. 多态概念1.1 概念多态的概念:通俗来说,就是多种形态,具体点就是去完
2023-06-15

C++的多态和虚函数实例分析

这篇文章主要介绍了C++的多态和虚函数实例分析的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇C++的多态和虚函数实例分析文章都会有所收获,下面我们一起来看看吧。一、C++的面试常考点阿里虽然是国内Java的第一
2023-06-29

C语言排序的原理实例分析

这篇文章主要介绍“C语言排序的原理实例分析”,在日常操作中,相信很多人在C语言排序的原理实例分析问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”C语言排序的原理实例分析”的疑惑有所帮助!接下来,请跟着小编一起来
2023-06-17

C++虚函数表与多态实例代码分析

这篇文章主要介绍“C++虚函数表与多态实例代码分析”,在日常操作中,相信很多人在C++虚函数表与多态实例代码分析问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”C++虚函数表与多态实例代码分析”的疑惑有所帮助!
2023-07-05

C#原子操作实例分析

这篇文章主要讲解了“C#原子操作实例分析”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“C#原子操作实例分析”吧!知识点竞争条件当两个或两个以上的线程访问共享数据,并且尝试同时改变它时,就发生
2023-06-29

java中多态原理的示例分析

这篇文章将为大家详细讲解有关java中多态原理的示例分析,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。Java可以用来干什么Java主要应用于:1. web开发;2. Android开发;3. 客户端开发
2023-06-14

C语言中动态内存管理实例分析

今天小编给大家分享一下C语言中动态内存管理实例分析的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。1.动态内存开辟的原因常见的
2023-07-02

【C++】多态的实现及其底层原理

个人主页:🍝在肯德基吃麻辣烫 我的gitee:gitee仓库 分享一句喜欢的话:热烈的火焰,冰封在最沉默的火山深处。 文章目录 前言一、什么是多态?二、多态的构成条件2.1什么是虚函数?2.2虚函数的重
2023-08-17

C语言动态内存管理实例代码分析

这篇文章主要介绍了C语言动态内存管理实例代码分析的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇C语言动态内存管理实例代码分析文章都会有所收获,下面我们一起来看看吧。1.动态内存开辟的原因常见的内存开辟方式int
2023-07-02

C/C++字节序实例分析

这篇文章主要讲解了“C/C++字节序实例分析”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“C/C++字节序实例分析”吧!字节序 最近在看 redis 的内存编码,里面涉及到字节
2023-06-29

C# XML实例分析

本篇内容介绍了“C# XML实例分析”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!C# XML解析的实现是如何的呢?下面我们来看看实现的方法
2023-06-17

C#多线程举例分析

这篇文章主要讲解了“C#多线程举例分析”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“C#多线程举例分析”吧!线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中
2023-06-22

C++中string底层原理的示例分析

小编给大家分享一下C++中string底层原理的示例分析,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!一、深浅拷贝浅拷贝:在实现string时要是不实先strin
2023-06-25

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录