【C++】详解多态的底层原理

news2024/11/19 13:16:54

文章目录

  • 前言
  • 1. 虚函数表指针与虚函数表
  • 2. 子类的虚函数表(单继承)
  • 3. 多态的实现原理
    • 3.1 多态是如何实现的
    • 3.2 多态调用与非多态调用的区别
    • 3.3 为什么父类的对象不能实现多态
  • 4. 其它多态相关问题的理解
    • 4.1 虚函数是存在哪里的?
    • 4.2 子类新增的虚函数地址是否进虚表
    • 4.3 打印虚函数表的程序
    • 4.4 虚表是什么时候生成的?虚表是存在哪的呢?
    • 4.6 对象中的虚表指针什么时候初始化的?
    • 4.7 静态多态和动态多态
  • 5. 多继承中的虚函数表
    • 5.1 多继承中子类几张虚表?
    • 5.2 子类新增的虚函数放在哪张虚表?
    • 5.3 子类重写的虚函数,为何在两张表的地址不同?
  • 6. 菱形虚拟继承下一些情况(了解)
  • 7. 用到的代码

上一篇文章我们学习了多态的语法,想必大家都会有很多疑问,这篇文章,我们就来带大家看看多态是如何实现的,它底层的原理是怎样的…

前言

需要声明的,本文中的代码及解释都是在vs2022下的x86程序中,涉及的指针都是4bytes。
如果要其他平台下,部分代码需要改动。比如:如果是x64程序,则需要考虑指针是8bytes问题等等

1. 虚函数表指针与虚函数表

首先,我们来一起看一道笔试题

现在有这样一个类:

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

大家来计算一下,sizeof(Base)是多少?

我们一起来分析一下:
之前类和对象的时候我们学过,计算一个类的大小其实只考虑成员变量就行了,因为类对象中只存储成员变量,成员函数是放在公共的代码段的。
那按照内存对齐规则,Base就应该是8个字节。
但是真的会这么简单吗?我们来看下结果是几
在这里插入图片描述
哦豁,答案是12字节。

怎么回事?为什么是12呢?

我们可以通过监视窗口观察一下Base都有哪些成员,是不是和我们想的一样
在这里插入图片描述
🆗,我们看到除了两个成员变量之外,还有一个_vfptr

那这个_vfptr是什么东西啊?

他其实是一个指针。
这里除了成员变量外,我们看到还多了一个名为_vfptr的指针放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function),简称虚表指针。

为什么会有这个东西?

是因为我们的Base类里面有虚函数,含有虚函数的类里面就会有虚函数表指针。
大家回忆一下,我们继承里面的学的虚拟继承,也是用的关键字virtual,它也是会给对象里面增加一个指针,那个指针叫做虚基表指针,它指向一张表(虚基表),里面存的是偏移量,用来去寻找公共的基类。
那这里的虚函数表指针也指向一张表,即虚函数表(简称虚表),虚函数表里面存的是虚函数的地址。
在这里插入图片描述
因为Base里面现在只有一个虚函数,所以我们看到它里面现在只有一个元素,就是虚函数Func1的地址。
另外我也能知道虚函数表其实就是一个虚函数指针数组,存放虚函数的地址,所以虚函数指针_vfptr其实就是一个数组指针(函数指针数组的指针)

那大家想,为什么要在对象里搞一个虚函数指针,去指向一个虚函数表,表里面存放虚函数的地址呢,为什么不直接把虚函数地址放在对象里面虚函数指针的位置?

🆗,因为虚函数是不是可能会有多个啊,所以虚函数表里面可能会放很多个虚函数地址。
另外,同一个类实例化出来多个对象,它们是共用一张虚函数表的,如果每个对象里面都放一张虚函数表,是不是有点浪费啊。
在这里插入图片描述
它们的虚函数指针都是一样的,指向同一张虚函数表。

2. 子类的虚函数表(单继承)

上面我们只有一个类,那如果是在继承体系中(当然是在多态的情况下,子类继承父类或重写父类的虚函数,这时子类才会有虚函数表),子类的虚函数表是怎么生成的?

首先大家思考一个问题,虚函数表会被继承吗?

其实不用多想,肯定会继承的,因为子类会继承父类的虚函数,那有了虚函数就会有虚函数指针,那就会有虚函数表。
我们可以来看一下:
在这里插入图片描述
下面通过调式窗口观察
在这里插入图片描述
我们发现子类虽然什么也没写,但是它里面有自己的虚函数指针(和父类的是相互独立的,看到它们地址是不一样的),并且我们看到子类虚表里面内容和父类是一样的。
所以可以认为子类会继承父类的虚表,子类的虚表是父类虚表的拷贝。

我们再针对上面的代码做一些改造:

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;
};
  1. 我们增加一个派生类Derive去继承Base
  2. Derive中重写Func1
  3. Base再增加一个虚函数Func2和一个普通函数Func3

然后我们来观察对比一下子类和父类的虚函数表:

在这里插入图片描述
首先现在父类里面两个虚函数,那子类继承父类的虚表,另外他没有自己增加虚函数(子类只是重写了其中一个虚函数Func1),所以子类虚表里面也是两个虚函数的地址,这没问题(没有Func3是因为它不是虚函数,所以不会进虚表)。
但是,我们仔细观察会发现,它们表里面第一个函数地址是不一样的。
为什么呢?

🆗,因为子类重写了父类的虚函数Func1。
如果没有重写,子类继承父类的虚表,它们虚表的内容是一样的,这个我们上面看了。
但是现在子类重写了父类的虚函数,那就会在虚表里覆盖上重写之后的虚函数地址。
所以,为什么当时讲重写的时候我们说还可以叫覆盖。
其实覆盖就是指这里的虚函数地址的覆盖。
重写是语法层的叫法,而覆盖是原理层的叫法。

总结一下子类的虚表生成:

先将基类中的虚表内容拷贝一份到派生类虚表中,如果派生类重写了基类中某个虚函数,则用派生类自己重写后的虚函数的地址覆盖虚表中原来存的继承下来的基类的虚函数地址。
派生类自己新增加的虚函数的地址按其在派生类中的声明次序增加到派生类虚表的最后,但这个在监视窗口可能看不到(这个后面给大家验证)。

3. 多态的实现原理

我们再来看上一篇文章举过的这个多态的例子:

class Person 
{
public:
	virtual void BuyTicket() 
	{ 
		cout << "买票-全价" << endl; 
	}
};
class Student : public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票-半价" << endl;
	}
};
void Func(Person& p)
{
	p.BuyTicket();
}

3.1 多态是如何实现的

了解了上面的内容,那多态到底是怎么实现的呢?它底层的原理是怎么样的?

在这里插入图片描述
它怎么根据不同的对象就能调到不同的函数呢?
在这里插入图片描述

我们来分析一下:

其实了解了上面的内容,相信大家已经差不多能猜出来了。
我们来看一下
在这里插入图片描述
相信大家看这张图就应该明白怎么回事了。
我们说如果实现多态的话,去调用的时候还跟引用或指针的类型有关吗?
是不是无关啊,而是跟它指向的对象的具体类型有关,它指向的对象是什么类型的,就去调该类对应的虚函数,从而就实现了多态——不同的对象调用同一个函数,产生不同的结果。
所以他这里是怎么实现的?
🆗,其实就是通过对象里的虚函数指针,去找到其对应的虚函数表,那子类对象的虚指针就指向子类的虚函数表,父类对象的虚指针就指向父类的虚函数表,那这样它们就能调到不同的虚函数,进而实现多态(不同对象去完成同一行为时,展现出不同的形态)。

3.2 多态调用与非多态调用的区别

那对于编译器来说,多态调用和非多态调用有什么不同呢?

首先来看非多态调用的:

我把父类虚函数的virtual去掉就不构成多态了
在这里插入图片描述
🆗,那不是多态的话其实就是普通函数的调用了,它在编译期间就可以根据这里引用或指针的类型确定要调用那个函数

然后我们来观察一下多态调用时的汇编:

在这里插入图片描述
不过汇编我们可能看不太懂。
但是我们知道它这里做的事情其实就是通过对象头4/8个字节里面存的虚函数指针去找到虚函数表,然后调用对应的虚函数。
所以多态时的函数调用就不是在编译期间就能确定要调用的函数了,而是在运行期间通对象里的虚函数指针找到虚表,然后确定要调用的具体函数。

那现在我们回过头来看多态的两个条件?为什么是它们两个?

首先是虚函数重写,为什么要有虚函数的重写?
因为只有子类对父类的虚函数进行了重写,子类的虚函数表里面才会有自己重写后的地址,这样通过对象找到虚表的时候才能调到不同的函数,表面上子类对象和父类对象调的是同一个函数,但实际父类对象调父类的虚函数,子类对象调自己重写之后的,进而实现多态。
那第二个为什么必须是父类的指针或引用去调用虚函数呢?
因为父类的指针和引用是不是既可以指向子类对象,也可以指向父类对象啊,我们之前学过,它支持赋值转换(切片)嘛。

那现在就有一个问题值得我们去思考:

3.3 为什么父类的对象不能实现多态

父类的对象也支持把子类的对象赋给它啊,那为什么父类的对象去调用虚函数不能实现多态呢?

回忆一下我们之前讲的内容,我们说指针和引用的切片是不是可以理解成:

子类对象的地址赋给父类的指针,就可以认为是把子类对象中切出来的父类的那一部分的地址赋给父类的指针。
子类对象赋给父类对象的引用就可以认为是给子类对象中切出来的父类的那一部分起一个别名。
那这样的话,父类的指针和引用指向的是不是还是一个子类对象啊,只不过可能是子类对象的一部分嘛,那它通过虚表指针找到的不就还是子类对象的虚函数表嘛。

那就可以实现多态啊。

那如果是子类对象赋给父类对象呢?

我们还可以按照切片理解,但是我们是不是说了把子类对象赋给父类对象其实是调用父类的拷贝构造完成的
那这里就涉及一个问题:子类对象的虚表会不会拷贝过去
如果没有拷贝虚表,那父类对象的虚表就还是自己的,那就不能实现多态了。
如果敢拷贝的话,就可以实现多态。

那大家想,子类的虚表会拷贝吗?拷贝过去可行吗?

那要告诉大家的是:将子类对象赋给父类对象时,并不会拷贝虚函数表,父类的虚表是不会变的,也因此父类的对象不能实现多态。
因为如果拷贝的话是不合理的,不敢随便拷这个。
因为一个父类对象,那它的虚表肯定就是父类自己的虚表,如果它里面的虚表指针指向子类的虚表,这就乱套了,这是不合理的。
子类对象里面虚表指针就指向子类的虚表,父类对象里面虚表指针就指向父类的虚表,这样才是合理的。

4. 其它多态相关问题的理解

4.1 虚函数是存在哪里的?

要注意虚函数不是存在虚函数表里面的:

虚函数和普通函数一样的,都是存在代码段的。只是虚函数的地址会被放进虚函数表里面,另外注意虚函数表不是放在对象中的,对象中放的是虚函数表指针。

4.2 子类新增的虚函数地址是否进虚表

那如果是子类自己定义的虚函数(不是重写父类的),那么它的地址会进虚表吗?
这也是我们上面遗留的一个问题。

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;
	}
	virtual void myfunc()
	{
		cout << "void myfunc()" << endl;
	}
private:
	int _d = 2;
};

我们就在上面那个代码里给Drive类里面再增加一个新的虚函数myfunc,但这个虚函数并不是对父类虚函数的重写
在这里插入图片描述
那它的地址会进虚函数表吗?
我们来看一下
在这里插入图片描述
我们通过监视窗口看到myfunc的地址好像没有进虚表。
但是我们说过,监视窗口看到的并不一定是真实的,有可能被处理过。
所以我们再通过内存窗口看一下:
在这里插入图片描述
我们看到前两个地址和监视窗口显示的是一样的,第三个地址和前两个很相近,所以我们猜测第三个就是myfunc的地址。
这里如果我们直接打印虚函数地址去验证的话,可能会发现打印出来的跟虚表里的地址不一样,可以理解为虚函数表内的地址是虚函数实际地址的一种间接表示形式,这可能与C++中的多态性、动态绑定和继承机制所导致的。

4.3 打印虚函数表的程序

所以,我们可以写一个打印虚函数表的程序验证一下。

那其实就是去打印虚指针指向的那个虚表嘛,它里面放的不就是虚函数的地址嘛!

那这个虚表其实是一个函数指针数组,那我们拿到这个数组打印里面的指针就行了。
那首先我们做一件事情,把我们这里虚函数对应的函数指针类型typedef一下,我们这里的几个虚函数它们的函数指针都是void (*) ()类型的,当然都写成这样其实也是为了我们方便演示和理解。
那我们typedef一下:typedef void(*VF_PTR)();
那我们函数里面打印这个数组就行了
在这里插入图片描述
但是现在有一个问题,这个数组的大小是多大呢?我们怎么判断循环结束呢?
🆗,在vs系列的编译器上,它在虚表这个指针数组的最后放了一个空指针(nullptr),在其他平台上可能就需要我们自己根据实际个数写了。
所以,我们直接这样写就可以
在这里插入图片描述

那我们怎么调用它呢?

是不是得拿到对象里面得虚函数表指针啊,然后解引用不是调用函数要传的实参嘛。
那虚指针怎么拿到?
是不是就在对象的前4/8个字节里面存啊。(我们当前环境是4字节)
那如何拿到对象的前4个字节的内容?
大家回忆一下之前C语言的文章里有讲过大小端的问题,在那里我们要取出一个整数变量的第一个字节的内容,怎么做的?
🆗,是不是把把该变量的地址强转为char*,然后解引用,就拿到第一个字节的内容了。
那这里也可以用同样的方法:
这里我们把对象的地址强转成int*,然后解引用,不是就拿到前四个字节的内容了嘛。
但是int*解引用是个int,而形参本质是个函数指针,所以我们可以再强转一个
在这里插入图片描述
这样。

那我们运行试一下:

在这里插入图片描述
可以,我们看到Derive类对象里面就是有三个虚函数地址的。
在这里插入图片描述
当然我们也可以调用一下函数看看,因为上面每个虚函数我都加了一些打印信息
在这里插入图片描述
所以第三个就是虚函数myfunc的地址,这证实了我们上面的猜想。
所以子类新增的虚函数也会进虚表。
派生类自己新增加的虚函数的地址按其在派生类中的声明次序增加到派生类虚表的最后,但这个在监视窗口可能看不到。

需要说明的是

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

4.4 虚表是什么时候生成的?虚表是存在哪的呢?

虚表什么时候生成?

虚表是由编译器在编译阶段生成的,因为编译过程中的汇编阶段会生成符号表,此时就可以确定函数的地址,那就可以生成虚函数表了。

那虚表存在哪里呢?

我们可以通过程序验证一下
在这里插入图片描述
看虚表的地址和那个挨得近,那基本上就在哪个区了
在这里插入图片描述
我们看到虚表的地址是不是和常量区的地址很接近啊。
当然其实我们都不用借助这个看
在这里插入图片描述
看,虚表的地址跟里面存的函数的地址是不是也很接近啊,而函数就是存在代码段的。

所以,是的

一般情况下,虚函数表通常被放置在代码段或常量区(只读数据段)中。这是因为虚函数表在运行时是只读的,不会被修改。
当然不同的编译器或平台也可能会不同。

4.6 对象中的虚表指针什么时候初始化的?

虚表指针其实是在构造函数的初始化列表阶段初始化的。

我们可以来验证一下:

给Base类加一个构造函数在这里插入图片描述
我们创建一个对象调式看一下
在这里插入图片描述
此时进行初始化列表,还没执行
在这里插入图片描述
此时初始化列表走完,我们看到虚表指针就初始化好了

4.7 静态多态和动态多态

静态多态(编译时多态、早期绑定/静态绑定):

静态多态是指在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载、模板

动态多态(运行时多态、晚期绑定/动态绑定):

动态多态是指是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。

5. 多继承中的虚函数表

接下来我们来看一个多继承的例子:

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;
};

有一个Base1,一个Base2,Derive继承了Base1和Base2,Base1和Base2里面都有两个虚函数func1、func2,Derive重写了func1,自己增加了一个func3。

那在这样一个多继承的体系中,子类的虚表又是怎么样的呢?

5.1 多继承中子类几张虚表?

首先大家思考一下,Derive应该有几张虚表?

猜测应该会有两张,因为子类会继承父类的虚表,而现在Derive继承了两个类,所以他应该有两张虚表。
我们可以先用监视窗口看一下
在这里插入图片描述
确实有两张表,Derive重写了func1,所以我们看到表里面进行了覆盖(两张表都覆盖了),func2是继承下来的,没有重写。

5.2 子类新增的虚函数放在哪张虚表?

那另一个问题,通过上面的学习我们知道子类自己增加的虚函数也会进函数表的,不过监视窗口看不到,那对于当前的继承体系来说,子类增加的虚函数func3会放在那一张虚表里呢?

那我们看一下呗,借助上面写好的打印虚表的函数。
那这里有两张虚表,那我们就要拿到两个虚指针,第一个肯定还在对象的前4个字节,那第二个应该在哪个位置?
在这里插入图片描述
🆗,是不是应该在子类对象里面第二个父类部分的前4个字节啊。
那起始位置的指针+sizeof(Base1)是不是就拿到Base2的地址了,然后从得到的位置取4个字节是不是就行了。
所以可以这样写:
在这里插入图片描述
当然其实找第二个虚指针我们可以借助指针偏移获取,之前多继承那里我们不是讲过嘛(大家不了解可以去看菱形继承那篇文章)
在这里插入图片描述
所以我们看到func3放的第一个虚表里面了
多继承中派生类自己新增的虚函数(不是重写父类的虚函数)放在第一个继承基类部分的虚函数表中
在这里插入图片描述

5.3 子类重写的虚函数,为何在两张表的地址不同?

但是,大家有没有发现一个问题:我们对func1重写了,但它在两个虚表里覆盖的地址不一样!!!
在这里插入图片描述

这是怎么回事呢?

我们来看一段代码
在这里插入图片描述
大家看,这两个函数调用调的是同一个吗?
当然是,因为这里是满足多态的,首先func1完成了重写,然后也是父类的指针去调用,所以这里调用的都是子类重写的func1函数。
但是这里父类的指针一个是Base1*的,一个是Base2*的。
所以它们应该一个去找第一个虚表的func1,另一个找第二个虚表里面的。
当然它们调的还是同一个,因为子类重写后在两张表里都进行了覆盖。

但是为啥两张表里面func1的地址不一样呢?

我们对比一下两个指针去调用func1的汇编:
在这里插入图片描述
汇编大家看不太懂也没关系。
通过汇编我们可以看出来它们一开始call的地址是不一样的,但是最终还是调到了同一个函数。可以理解成目的地是一样的,只是走的路线不同。
ptr1调用的过程其实是比较正常的一个函数调用的过程,但是ptr2好像多走了几步
在这里插入图片描述
多了一个sub和两次jmp的操作,那它为什么要这样呢,为啥要绕绕路呢?
其实这里面比较关键的一步是sub这条指令,ptr1那边是没有这个的,那它是什么作用呢?
大家知道这里的ecx里面存的是啥吗?
这个其实之前第一篇讲解类和对象的文章里我们就提过,调用成员函数的时候,vs上面会把this指针存到ecx寄存器里面
在这里插入图片描述
所以这句指令是什么作用呢?
我们再来看一下这个对象模型
在这里插入图片描述
现在两个指针调用的都是子类重写的函数,因为多态看的是指针指向的对象的类型。
调用成员函数的时候this存的是谁的地址,是不是当然调用函数的对象的地址啊(在这里其实就是ptr指针里面的地址),所以应该是子类对象的地址。
所以this指针应该指向子类对象的起始地址,那现在ptr1刚好就指向子类对象的起始,所以它可以直接去正常的调,而ptr2的指向是不是不对啊,他现在指向子类对象中父类Base2部分的起始位置。
所以要对ptr2的指向进行修正。
上面的sub ecx ,8这句汇编其实就是在修正this指针的位置。
sub这个指令用于执行减法操作,所以它的意思是给this指针-8,而ptr2现在执行Base2,它前面有一个base1,Base1这个类的大小刚好就是8,这样一减,刚好就指向子类对象的起始位置了。
所以当前减这个值跟Base1的大小有关系。
当然如果先继承Base2,那就是ptr1需要修正了。

6. 菱形虚拟继承下一些情况(了解)

大家还记不记得在之前菱形虚拟继承那篇文章,我们遗留一个问题:

就是我们当时看那个虚基表,里面的偏移量是放在虚基表的第二个位置的,第一个位置空了出来
在这里插入图片描述
我们当时说第一个位置空出来是跟多态有关系的,那我们今天讲到多态了,就来解释一下。

我们还把当时那个菱形虚拟继承继承的代码拿过来,给他们加一些虚函数:

class A
{
public:
	virtual void func1()
	{}
public:
	int _a;
};

class B : virtual public A
{
public:
	virtual void func1()
	{}
public:
	int _b;
};

class C : virtual public A
{
public:
	virtual void func1()
	{}
public:
	int _c;
};

class D : public B, public C
{
public:
	int _d;
};

大家看一下代码,现在还是这个菱形继承
在这里插入图片描述
现在A里面有一个虚函数func1,BC都对它进行了重写,D没有重写

我们之前讲过这个继承体系的对象模型:

原本BC里面都有一个A,但是虚继承后只保留一份A,放在公共的区域,原本BC里面放A的位置放的是虚基表指针,指向虚基表,虚基表里存的是偏移量,通过偏移量可以找到公共的A。
那现在这种情况,如果没有虚继承的话,BC里面都有一个继承A的虚表,B重写会覆盖自己里面继承A的虚表,C重写也会。
但是!!!
现在虚继承之后A只有一份,那BC都覆盖的话,就会有二义性,继承下来的A的虚表里面放B重写的还是C重写的?
在这里插入图片描述
那这样D最后继承的也不明确

所以可以怎么解决呢?

那既然不明确,那D就自己重写呗。
在这里插入图片描述
在这里插入图片描述
我们看到此时看起来虽然有三张虚表,但是它们的地址是一样的,里面存的虚函数地址也是一样的,都是D重写的那个,所以可以认为只有一张虚表。

那我们再修改修改代码:

给BC里面分别再增加一个虚函数,其他地方不变
在这里插入图片描述
然后我们再看监视窗口
在这里插入图片描述
我们看的此时是真的有三张虚表,D重写的放在公共的A部分的虚表里面,BC自己新增的虚函数就放在了BC部分的虚表里面了。

再通过内存窗口看一下当前的模型

在这里插入图片描述
现在是3张虚函数表,两张虚基表,大家分不清哪个指针是什么指针,可以用内存窗口看他们里面放的什么,大家见了这么多了,应该能看出来的。

那我们看看现在的虚基表里面,之前空的哪个位置存的啥?

在这里插入图片描述
我们看到,现在第一个位置确实不是之前的0了,大家看这个值转化成10进制是几?
fffffffc,内存中是补码,转换为10进制是-4。
那这个数是干嘛的呢?
大家看,当前这两个虚基表指针的地址,-4是不是正好得到虚函数表指针的地址啊,因为现在一行4字节。
所以虚基表里面的第一个数应该是用来帮助寻找虚函数表指针的。

实际中我们不建议设计出菱形继承及菱形虚拟继承,一方面太复杂容易出问题,另一方面这样的模型,访问基类成员有一定得性能损耗。所以菱形继承、菱形虚拟继承我们的虚表我们就不看了,一般我们也不需要研究清楚,因为实际中很少用。

7. 用到的代码

//class Base
//{
//public:
//	virtual void Func1()
//	{
//		cout << "Func1()" << endl;
//	}
//};
//class Derive: public Base
//{};
//int main()
//{
//	Base b;
//	Derive d;
//	return 0;
//}

//class Base
//{
//public:
//	Base()
//		:_b(10)
//	{}
//	virtual void Func1()
//	{
//		cout << "Base::Func1()" << endl;
//	}
//	virtual void Func2()
//	{
//		cout << "Base::Func2()" << endl;
//	}
//	void Func3()
//	{
//		cout << "Base::Func3()" << endl;
//	}
//private:
//	int _b;
//};
//class Derive : public Base
//{
//public:
//	virtual void Func1()
//	{
//		cout << "Derive::Func1()" << endl;
//	}
//	virtual void myfunc()
//	{
//		cout << "void myfunc()" << endl;
//	}
//private:
//	int _d = 2;
//};
打印虚表
//typedef void(*VF_PTR)();
//void printVFTable(VF_PTR table[])
//{
//	for (int i = 0; table[i] != nullptr; ++i)
//	{
//		printf("[%d]:%p->", i, table[i]);
//		table[i]();
//	}
//}
//int main()
//{
//	Base b;
//
//	int x = 0;
//	static int y = 0;
//	int* z = new int;
//	const char* p = "xxxxxxxxxxxxxxxxxx";
//
//	printf("栈对象:%p\n", &x);
//	printf("堆对象:%p\n", z);
//	printf("静态区对象:%p\n", &y);
//	printf("常量区对象:%p\n", p);
//	printf("b对象虚表:%p\n", *((int*)&b));
//	return 0;
//}
//int main()
//{
//	Derive d;
//
//	printVFTable((VF_PTR*)(*(int*)&d));
//	printf("%p", &Derive::myfunc);
//	//Base b;
//
//	return 0;
//}


//class Person 
//{
//public:
//	virtual void BuyTicket() 
//	{ 
//		cout << "买票-全价" << endl; 
//	}
//};
//class Student : public Person
//{
//public:
//	virtual void BuyTicket()
//	{
//		cout << "买票-半价" << endl;
//	}
//};
//void Func(Person& p)
//{
//	p.BuyTicket();
//}
//int main()
//{
//	Person ps;
//	Student st;
//
//	Func(ps);
//	Func(st);
//
//	return 0;
//}


//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;
//};
打印虚表
//typedef void(*VF_PTR)();
//void printVFTable(VF_PTR table[])
//{
//	for (int i = 0; table[i] != nullptr; ++i)
//	{
//		printf("[%d]:%p->", i, table[i]);
//		table[i]();
//	}
//	cout << endl << endl;
//}
//int main()
//{
//	Derive d;
//	Base1* ptr1 = &d;
//	Base2* ptr2 = &d;
//	ptr1->func1();
//	ptr2->func1();
//
//	return 0;
//}
//int main()
//{
//	Derive d;
//	printVFTable((VF_PTR*)(*(int*)&d));
//	//printVFTable((VF_PTR*)(*(int*)((char*)&d + sizeof(Base1))));
//	Base2* ptr = &d;
//	printVFTable((VF_PTR*)(*(int*)ptr));
//
//	return 0;
//}

//class A
//{
//public:
//	virtual void func1()
//	{}
//public:
//	int _a;
//};
//
//class B : virtual public A
//{
//public:
//	virtual void func1()
//	{}
//	virtual void func2()
//	{}
//public:
//	int _b;
//};
//
//class C : virtual public A
//{
//public:
//	virtual void func1()
//	{}
//	virtual void func3()
//	{}
//public:
//	int _c;
//};
//
//class D : public B, public C
//{
//public:
//	virtual void func1()
//	{
//
//	}
//public:
//	int _d;
//};
//
//int main()
//{
//	D d;
//	d.B::_a = 1;
//	d.C::_a = 2;
//	d._b = 3;
//	d._c = 4;
//	d._d = 5;
//
//	return 0;
//}

在这里插入图片描述

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

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

相关文章

手机照片误删除?无需担忧,点击这里,即可轻松恢复

手机照片误删除&#xff1f;无需担忧&#xff0c;点击这里&#xff0c;即可轻松恢复 开头&#xff1a;在数字化时代&#xff0c;手机已成为我们生活中不可或缺的伙伴。随着手机摄影的普及&#xff0c;我们记录了许多珍贵的瞬间和回忆。然而&#xff0c;有时候我们不小心误删除…

项目经理必备的5种管理能力

作为中层管理者&#xff0c;需要同时完成上级的任务安排和照顾下属的情绪&#xff0c;这是职场中最具挑战性的管理能力。项目经理必备能力中&#xff0c;计划制定、有效授权、高效沟通、化解冲突、项目跟踪是至关重要的。 1、计划制定是项目管理的关键。 作为项目经理&#…

Tribon二次开发- tbbatchjob

在Tribon安装目录下C:\Tribon\M3\Bin里面有许多未知用途的exe,有的双击后时一个DOS终端,有的一闪而过,有的需要按照提示输入信息,有的需要提前在指定的目录配置文件,该如何使用呢? 这些exe大多可以在Tribon以外通过.NET来使用,有的可以通过添加.NET项目引用来使用,有的…

聊聊spring中bean的作用域

前言 今天分享一下spring bean的作用域&#xff0c;理解bean的作用域能够在使用过程中避免一些问题&#xff0c;bean的作用域也是spring bean创建过程中一个重要的点。 Spring bean的作用域类型 singleton&#xff08;单例模式&#xff09;&#xff1a;在整个应用程序的生命周…

成都爱尔蔡裕:泡在“糖”里的脆弱血管,暴露在眼睛深处

糖尿病是一组由多病因引起的以慢性高血糖为特征的终身性代谢性疾病。长期血糖增高&#xff0c;大血管、微血管受损并危及心、脑、肾、周围神经、眼睛、足等。医生临床数据显示&#xff0c;糖尿病发病后10年左右&#xff0c;将有30%&#xff5e;40%的患者至少会发生一种并发症&a…

【TypeScript】对函数类型的约束定义

导读 函数是JavaScript 中的 一等公民 概念&#xff1a;函数类型的概念是指给函数添加类型注解&#xff0c;本质上就是给函数的参数和返回值添加类型约束 文章目录 声明式函数:表达式函数&#xff1a;箭头函数可选参数和默认参数&#xff1a;参数默认值&#xff1a;过剩参数的处…

脚本 打开 cmd 跳转到某个文件夹并执行某些命令

很多时候我们需要启动windows安装的redis、nacos等。 通常我们可以打开安装软件的目录&#xff0c;在文件夹目录那一栏输入cmd,再执行相关启动命令但是这样比较麻烦&#xff0c;现在我们写一个bat脚本&#xff0c;直接启动脚本就可以实现启动程序了。 例如&#xff0c; 1&…

docker入门讲解

目录 第 1 章 Docker核心概念与安装 为什么使用容器&#xff1f; Docker是什么 Docker设计目标 Docker基本组成 容器 vs 虚拟机 Docker应用场景 Linux 安装 Docker 第 2 章 Docker镜像管理 Docker 容器管理 Docker 容器数据持久化 Docker 容器网络 Dockerfile 定制…

JAVA的数据类型与变量

目录 1. 字面常量 2. 数据类型 3. 变量 3.2 长整型变量 3.3 短整型变量 3.4 字节型变量 3.5双精度浮点型 3.6 单精度浮点型 3.7字符型变量 3.8布尔型变量 4.类型转换 4.1自动类型转换(隐式) 4.2强制类型转换(显式) 5.字符串类型 1. 字面常量 字面常量的分类&am…

深度学习之梯度下降算法

0.1 学习视频源于&#xff1a;b站&#xff1a;刘二大人《PyTorch深度学习实践》 0.2 本章内容为自主学习总结内容&#xff0c;若有错误欢迎指正&#xff01; 1 线性模型 1.1 通过简单的线性模型来举例&#xff1a; 1.2 如图&#xff0c;简单的一个权重的线性模型&#xff0c…

透明屏的应用范围广吗?

透明屏是一种新型的显示技术&#xff0c;它可以使屏幕显示的内容透明&#xff0c;让用户可以同时看到屏幕上的图像和背后的物体。 透明屏的应用领域非常广泛&#xff0c;可以用于商业广告、展览展示、智能家居等多个领域。 透明屏的原理是利用透明材料和光学技术&#xff0c;…

通过el-tab切换Echarts图表显示不全问题

一、背景 在让日常开发中很多时候会通过el-tab选项卡方式去分类统计数据&#xff0c;本文我们主要是针对统计中用到了echarts图表&#xff0c;在刚接触时可能会遇到默认选项卡可以正常显示图表数据&#xff0c;但是切换选项卡以后会出现图表大小出现问题&#xff0c;当然原因就…

第2集丨webpack 江湖 —— 创建一个简单的webpack工程demo

目录 一、创建webpack工程1.1 新建 webpack工程目录1.2 项目初始化1.3 新建src目录和文件1.4 安装jQuery1.5 安装webpack1.6 配置webpack1.6.1 创建配置文件&#xff1a;webpack.config.js1.6.2 配置dev脚本1.7 运行dev脚本 1.8 查看效果1.9 附件1.9.1 package.json1.9.2 webpa…

MyBatisPlus之DML编程控制

MyBatisPlus之DML编程控制 1. id生成策略控制&#xff08;Insert&#xff09;1.1 id生成策略控制&#xff08;TableId注解&#xff09;1.2 全局策略配置id生成策略全局配置表名前缀全局配置 2. 多记录操作&#xff08;批量Delete/Select&#xff09;2.1 按照主键删除多条记录2.…

【java实习评审】对小说更新时间点的并发压力的短链接接口实现比较到位

大家好&#xff0c;本篇文章分享一下【校招VIP】免费商业项目“推推”第一期书籍详情模块java同学的代码周最佳作品。该同学来自西安邮电大学通信工程专业。本项目亮点难点&#xff1a;1 热门书籍在更新点的访问压力&#xff0c;2 书籍更新通知的及时性和有效性&#xff0c;3 书…

GlobalProtect-点击连接按钮无响应

GlobalProtect-点击连接按钮无响应 解决方案 重启PanGPS服务 点击连接

C++输入字符串函数cin.getline()

1.函数作用 接受一个字符串&#xff0c;可以接收空格并输出。 2.函数的完整形式 cin.getline(字符数组名,字符个数,结束标志) 第三个参数可以省略&#xff0c;当第三个参数省略之后&#xff0c;系统默认为’\0’。 若指定参数“字符个数”为n&#xff0c;则利用cout函数输出…

LiveGBS流媒体平台GB/T28181常见问题-TOKEN有效期是多久如何设置token有效期StreamToken和URLToken

LiveGBS中TOKEN有效期是多久如何设置token有效期StreamToken和URLToken 1、获取TOKEN2、TOKEN有效期3、默认token有效期3、自定义token加密key3.1、token_key3.2、stream_token_key 4、如何配置一直有效的token4.1、URLToken4.2、StreamToken 5、动态有效期6、流地址鉴权开启后…

《微SaaS创富周刊》第9期:如何把创业者访谈,变成年收入100万+美元的生意

导读 大家好&#xff01;第9期《微SaaS创富周刊》面世啦&#xff08;点击这里阅读第1期&#xff09;&#xff0c;感谢大家的关注和阅读&#xff01;本周刊面向独立开发者、早期创业团队&#xff0c;报道他们主要的产品形态——微SaaS如何变现的最新资讯和经验分享等。所谓微Sa…