【C++学习手札】多态:掌握面向对象编程的动态绑定与继承机制(深入)

本文主要是介绍【C++学习手札】多态:掌握面向对象编程的动态绑定与继承机制(深入),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

                                               🎬慕斯主页修仙—别有洞天

                                              ♈️今日夜电波:世界上的另一个我

                                                                1:02━━━━━━️💟──────── 3:58
                                                                    🔄   ◀️   ⏸   ▶️    ☰  

                                      💗关注👍点赞🙌收藏您的每一次鼓励都是对我莫大的支持😍


目录

多态的原理

首先理解虚函数表

正式理解多态的原理

一些拓展

多态对于引用、指针和对象

虚表的拓展

多继承中的虚函数表

先了解如何打印虚函数表

然后理解多继承中的虚函数表

方法一:加上base1的大小

方法二:切片

结论


多态的原理

首先理解虚函数表

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

        请问sizeof(Base)的大小为多少?

        答案为:x64下16字节,x86下8字节

        解析如下:

        Base类中包涵着int类型的成员变量占4字节,而由于有虚函数,因此会有一个虚函表的指针vfptr,因此根据内存对齐,得到上述答案。

        这时就会有疑惑了?虚函数表指针和虚函数表是什么呢?

        如下通过监视窗口可以看到vfptr指向了一个数组(也就是虚函数表),而数组中存储着虚函数指针:

        继续分析,我们在上述代码的基础上增加代码:

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;
};
class Derive : public Base
{
public:virtual void Func1(){cout << "Derive::Func1()" << endl;}
private:int _d = 2;
};
int main()
{Base b;Derive d;return 0;
}

        通过观察和测试,我们发现了以下几点问题:

  1. 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分是自己的成员。
  2. 基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
  3. 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表。
  4. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。
  5. 总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
  6. 这里还有一个童鞋们很容易混淆的问题:虚函数存在哪的?虚表存在哪的? 答:虚函数存在虚表,虚表存在对象中。注意上面的回答的错的。但是很多童鞋都是这样深以为然的。注意虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。那么虚表存在哪的呢?实际我们去验证一下会发现vs下是存在代码段的,Linux g++下大家自己去验证?

正式理解多态的原理

        见以下代码:

class Person {
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }virtual void vv() { cout << "打折" << endl; }int a = 0;
};
class Student : public Person {
public:virtual void BuyTicket() { cout << "买票-半价" << endl; }int b = 1;
};
void Func(Person& p)
{p.BuyTicket();
}
int main()
{Person m;Func(m);Student s;Func(s);return 0;
}

        从监视中很明显的看到,子类继承了父类的虚函数表,但是很明显的看到虚函数表中我们的BuyTicket()的虚函数指针地址改变了,而vv()确没有改变,这就很明显了,因为BuyTicket()被重写了,而vv()没有,而重写也有另外一个名字:覆盖当我们重写了虚函数,那么就会覆盖对应虚函数在虚函数表中的指针

        内存方面观察:

        可以看到其中vfptr中存储的地址是发生了改变的,也就是说我们可以根据这个地址找到新的一张虚函数表,在前面我们学习过“切片”的概念,我们知道当以父类的类型去访问子类的类型会发生“切片”使得只访问父类的类型的空间,也就是说我们只访问上图中蓝色框内的内容,再结合上上张图监视中如果子类重写了虚函数则虚函数表中虚函数指针改变。当我们调用对应的虚函数时,就会调用子函数的虚函数而不是父类的虚函数!这就是多态实现的原理!因此,多态中指向父类调用父类,指向子类调用子类!

一些拓展

多态对于引用、指针和对象

        为什么多态只允许引用和指针呢?我们都知道引用的底层实现实际上还是指针,多态的实现就是指向子类对象中切割出来的那一部分!而对象只会拷贝子类对象中父类的那一部分,但是不会拷贝虚函数表指针。为什么呢?因为如果允许虚函数表指针的拷贝会造成二义性,如下:

int main()
{Person m;Student s;m=s;Func(m);Func(s);return 0;
}

        如果对象可以像引用和指针一样,那么当拷贝了虚函数表指针后,你会发现我们实现不了多态中指向父类调用父类,指向子类调用子类的场景。也会造成析构函数调用调错等等的错误。

虚表的拓展

        如果子类与父类中不重写虚函数,子类与父类的续虚函数表一样吗?不一样!他们存在不同的位置!虽然他们的内容是一样的!同类对象的虚函数表一样吗?一样!

        总结,不同的类不会共用虚函数表,只有相同的类才会共用虚函数表!

多继承中的虚函数表

先了解如何打印虚函数表

        我们都知道虚函数表是一个函数指针数组,并且数组最后一位是以nullptr结尾的。因此,我们可以根据该特性打印虚函数表:

typedef void(*VFPTR) ();
void PrintVTable(VFPTR* vTable)
{// 依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数cout << " 虚表地址>" << vTable << endl;for (size_t i = 0; vTable[i] != nullptr; ++i){printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);VFPTR f = vTable[i];f();}cout << endl;
}

        例子,打印单继承的虚函数表:

class Base {
public:virtual void func1() { cout << "Base::func1" << endl; }virtual void func2() { cout << "Base::func2" << endl; }
private:int a;
};
class Derive :public Base {
public:virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }virtual void func4() { cout << "Derive::func4" << endl; }
private:int b;
};typedef void(*VFPTR) ();
void PrintVTable(VFPTR* vTable)
{// 依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数cout << " 虚表地址>" << vTable << endl;for (size_t i = 0; vTable[i] != nullptr; ++i){printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);VFPTR f = vTable[i];f();}cout << endl;
}int main()
{Base b;Derive d;VFPTR * vTableb = (VFPTR*)(*((int*)&b));PrintVTable(vTableb);VFPTR* vTabled = (VFPTR*)(*((int*)&d));PrintVTable(vTabled);return 0;
}

        思路:取出b、d对象的头4bytes,就是虚表的指针,前面我们说了虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr。需要注意的是这是在x86的运行环境下的,如果是x64则需强转为long long:

        1.先取b的地址,强转成一个int*的指针

        2.再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针

        3.再强转成VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。

        4.虚表指针传递给PrintVTable进行打印虚表

        5.需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最后面没有放nullptr,导致越界,这是编译器的问题。我们只需要点目录栏的 - 生成 - 清理解决方案,再编译就好了。

然后理解多继承中的虚函数表

        概念:

C++中的多继承是指一个派生类可以同时从多个基类派生,从而继承它们的属性和行为

        多继承是面向对象编程中一个重要的概念,它允许一个类继承多个其他类的成员。这样做有几个目的:

  • 代码重用:多继承可以提高代码的重用性,因为派生类可以访 问所有基类的公有成员和保护成员。
  • 功能组合:通过继承多个类,派生类可以将不同基类的功能组合在一起,形成更复杂的功能。

        然而,多继承也可能带来一些问题,如菱形继承问题,这可能导致二义性。为了解决这个问题,C++引入了虚基类的概念。

        如下为一段多继承的代码,可以看到drive继承了base1和base2:

class base1 {
public:virtual void func1() { cout << "base1::func1" << endl; }virtual void func2() { cout << "base1::func2" << endl; }
private:int b1;
};class base2 {
public:virtual void func1() { cout << "base2::func1" << endl; }virtual void func2() { cout << "base2::func2" << endl; }
private:int b2;
};class derive : public base1, public base2 {
public:virtual void func1() { cout << "derive::func1" << endl;}virtual void func3() { cout << "derive::func3" << endl; }
private:int d1;
};

        那么他的虚函数表又是什么样的呢?如下:

        可以看到正如我们猜测的那样,它包含着两张虚函数表!然而,我们在derive中重写了func1()函数,以及额外添加了一个func3()函数,但是并没有在监视中显示,这是因为编译器并没有让你实际的看到,也就是说编译器在骗人,实际上就是在其中的一张表当中,可以理解为监视的一个bug。我们通过上述打印虚函数表可以看到具体的效果(注意此为x64环境下):

typedef void(*VFPTR) ();
void PrintVTable(VFPTR* vTable)
{// 依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数cout << " 虚表地址>" << vTable << endl;for (size_t i = 0; vTable[i] != nullptr; ++i){printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);VFPTR f = vTable[i];f();}cout << endl;
}class base1 {
public:virtual void func1() { cout << "base1::func1" << endl; }virtual void func2() { cout << "base1::func2" << endl; }
private:int b1;
};class base2 {
public:virtual void func1() { cout << "base2::func1" << endl; }virtual void func2() { cout << "base2::func2" << endl; }
private:int b2;
};class derive : public base1, public base2 {
public:virtual void func1() { cout << "derive::func1" << endl;}virtual void func3() { cout << "derive::func3" << endl; }
private:int d1;
};int main()
{cout << "base1:" << endl;base1 b;PrintVTable((VFPTR*)(*(long long*)&b));cout << "base2:" << endl;base2 c;PrintVTable((VFPTR*)(*(long long*)&c));cout << "derive 表1:" << endl;derive d;PrintVTable((VFPTR*)(*(long long*)&d));//printvft((vfunc*)(*(int*)((char*)&d+sizeof(base1))));cout << "derive 表2:" << endl;base2* ptr = &d;PrintVTable((VFPTR*)(*(long long*)ptr));
}

        得到第一个虚基表的方法很简单,因为第一个虚基表的指针正好处在前8个字节处,只需要向上面一样进行强转即可,如果要找到第二个虚基表则有如下两种方法:

方法一:加上base1的大小
	printvft((vfunc*)(*(int*)((char*)&d+sizeof(base1))));

        也就是加上sizeof(base1)即可,但是!需要注意的是d的类型是Derive,在&d后变为Derive* 的一个指针,+1 跳转的是Derive类型的字节大小!而我们想要的是每次+1跳转1个字节,所以需要强制转换char* !

方法二:切片
	base2* ptr = &d;PrintVTable((VFPTR*)(*(long long*)ptr));

        把d利用切片的原理给到ptr,然后再按照上面强转的原理找到虚基表即可!

结论

        如下为上面代码的运行结果:

        可以看到上面的图示,我们可以得出相应的结论:多继承中重写的虚函数以及新增的虚函数都是在第一个虚基表当中进行修改以及增加的!如果重写的虚函数在其他基类中也有对应的虚函数,那么继承下来的虚基表也需要重写。

        更加详细的图解如下:

​        这里又引申出来一个问题,为什么其derive继承的两个虚基表中func1()的地址不同呢?这里就需要从汇编的角度进行理解了:

        在以上的代码的基础上调试下面这段代码(x86环境下),通过反汇编可得结果如下:

	derive d;base1* p1 = &d;p1->func1();base2* p2 = &d;p2->func1();

        ​从上图的图示可以看到p1只经过了一次jmp就找到了derive中的func1()的地址,而p2则是经过了多次的jmp才找到func1()地址。这是因为:p1的调用的地址恰好与derive* 类型的this指针的地址是重叠的,因此不需要去找这个地址,而p2要经过蓝框中的“8字节的偏移”才能找到this指针(可以看到有ecx标识(ecx是存储this指针的)),才能指向derive对象的开始,才可以调用derive的func1()(毕竟fun1也可能调用成员函数、成员变量等等)。

        总结:这里是为了修正this指针指向derive对象,这里调用的是derive重写的func1()。

 


                      感谢你耐心的看到这里ღ( ´・ᴗ・` )比心,如有哪里有错误请踢一脚作者o(╥﹏╥)o! 

                                       

                                                                        给个三连再走嘛~  

 

这篇关于【C++学习手札】多态:掌握面向对象编程的动态绑定与继承机制(深入)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/723135

相关文章

HarmonyOS学习(七)——UI(五)常用布局总结

自适应布局 1.1、线性布局(LinearLayout) 通过线性容器Row和Column实现线性布局。Column容器内的子组件按照垂直方向排列,Row组件中的子组件按照水平方向排列。 属性说明space通过space参数设置主轴上子组件的间距,达到各子组件在排列上的等间距效果alignItems设置子组件在交叉轴上的对齐方式,且在各类尺寸屏幕上表现一致,其中交叉轴为垂直时,取值为Vert

Ilya-AI分享的他在OpenAI学习到的15个提示工程技巧

Ilya(不是本人,claude AI)在社交媒体上分享了他在OpenAI学习到的15个Prompt撰写技巧。 以下是详细的内容: 提示精确化:在编写提示时,力求表达清晰准确。清楚地阐述任务需求和概念定义至关重要。例:不用"分析文本",而用"判断这段话的情感倾向:积极、消极还是中性"。 快速迭代:善于快速连续调整提示。熟练的提示工程师能够灵活地进行多轮优化。例:从"总结文章"到"用

JVM 的类初始化机制

前言 当你在 Java 程序中new对象时,有没有考虑过 JVM 是如何把静态的字节码(byte code)转化为运行时对象的呢,这个问题看似简单,但清楚的同学相信也不会太多,这篇文章首先介绍 JVM 类初始化的机制,然后给出几个易出错的实例来分析,帮助大家更好理解这个知识点。 JVM 将字节码转化为运行时对象分为三个阶段,分别是:loading 、Linking、initialization

【前端学习】AntV G6-08 深入图形与图形分组、自定义节点、节点动画(下)

【课程链接】 AntV G6:深入图形与图形分组、自定义节点、节点动画(下)_哔哩哔哩_bilibili 本章十吾老师讲解了一个复杂的自定义节点中,应该怎样去计算和绘制图形,如何给一个图形制作不间断的动画,以及在鼠标事件之后产生动画。(有点难,需要好好理解) <!DOCTYPE html><html><head><meta charset="UTF-8"><title>06

学习hash总结

2014/1/29/   最近刚开始学hash,名字很陌生,但是hash的思想却很熟悉,以前早就做过此类的题,但是不知道这就是hash思想而已,说白了hash就是一个映射,往往灵活利用数组的下标来实现算法,hash的作用:1、判重;2、统计次数;

【C++ Primer Plus习题】13.4

大家好,这里是国中之林! ❥前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站。有兴趣的可以点点进去看看← 问题: 解答: main.cpp #include <iostream>#include "port.h"int main() {Port p1;Port p2("Abc", "Bcc", 30);std::cout <<

第10章 中断和动态时钟显示

第10章 中断和动态时钟显示 从本章开始,按照书籍的划分,第10章开始就进入保护模式(Protected Mode)部分了,感觉从这里开始难度突然就增加了。 书中介绍了为什么有中断(Interrupt)的设计,中断的几种方式:外部硬件中断、内部中断和软中断。通过中断做了一个会走的时钟和屏幕上输入字符的程序。 我自己理解中断的一些作用: 为了更好的利用处理器的性能。协同快速和慢速设备一起工作

深入探索协同过滤:从原理到推荐模块案例

文章目录 前言一、协同过滤1. 基于用户的协同过滤(UserCF)2. 基于物品的协同过滤(ItemCF)3. 相似度计算方法 二、相似度计算方法1. 欧氏距离2. 皮尔逊相关系数3. 杰卡德相似系数4. 余弦相似度 三、推荐模块案例1.基于文章的协同过滤推荐功能2.基于用户的协同过滤推荐功能 前言     在信息过载的时代,推荐系统成为连接用户与内容的桥梁。本文聚焦于

C++包装器

包装器 在 C++ 中,“包装器”通常指的是一种设计模式或编程技巧,用于封装其他代码或对象,使其更易于使用、管理或扩展。包装器的概念在编程中非常普遍,可以用于函数、类、库等多个方面。下面是几个常见的 “包装器” 类型: 1. 函数包装器 函数包装器用于封装一个或多个函数,使其接口更统一或更便于调用。例如,std::function 是一个通用的函数包装器,它可以存储任意可调用对象(函数、函数

C++11第三弹:lambda表达式 | 新的类功能 | 模板的可变参数

🌈个人主页: 南桥几晴秋 🌈C++专栏: 南桥谈C++ 🌈C语言专栏: C语言学习系列 🌈Linux学习专栏: 南桥谈Linux 🌈数据结构学习专栏: 数据结构杂谈 🌈数据库学习专栏: 南桥谈MySQL 🌈Qt学习专栏: 南桥谈Qt 🌈菜鸡代码练习: 练习随想记录 🌈git学习: 南桥谈Git 🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈�