虚函数详细(剖析)看这一篇就够了

      

        类只要包含了虚函数,那么这个类就会产生一个虚函数表,但是我们光知道了虚函数表的存在,想没想过他在对象的内存布局中的位置呢?这是个非常重要的问题!!!下面我们就来分析虚函数在对象的内存布局的位置。

         我使用的开发工具是 vs2019


        首先先创建一个类:添加一个虚函数

#include <iostream>
using namespace std;

class A {
public:
	int i;	//四个字节
	virtual void func1() {}	//虚函数

};
int main()
{
	A a;
	int len = sizeof(a);
	cout << "对象a的大小为: " << len << endl;
    
    return 0;
}

        执行结果如下: 

 

        我们会发现,除了我们自己定义的成员变量 int  i ,就没别的了,正常来说这个对象会占用4个字节才对,为什么是8字节呢? 这就代表了该对象内产生了虚函数表指针,我们继续探索。

 添加如下代码:

    char* p1 = (char*)&a;
	char* p2 = (char*)&a.i;

	if (p1 == p2) {//如果相等就代表i的地址在虚函数的上边,也就是对象的起始地址(跟this指针一个位置)
		cout << "相等" << endl;
	}
	else {
		cout << "不相等" << endl;
	}

        看到结果会发现,成员变量 i 不在对象的起始地址上,那么 i 就代表(5-8)字节上,(1-4)字节是虚函数指针,这么说肯定不能够说服你,接下来让我们看看内存里边是什么样的。

这里我们给 成员变量 i 进行一次赋值

a.i = 8;

shift + F9 进入快速监视,找到 对象a 的地址

        我们找到从该地址起始的八个字节

        我们会发现给 i 赋值 8 后,后边的四个字节竟然变为红色,那是因为我们已经改变了它的值, 但是这里的08 00 00 00 也许有人就看不懂了,我们不是赋值为 8 嘛 为啥是 这么一串东西,因为这里是按照16进制来存储的(不懂的小伙伴可以去复习一下二进制和十六进制),这里是小端存储,所以他的高地址在地位,低地址在低位,我们把它调整一下 00 00 00 08 这样看是不是就是我们最熟悉的呢。

补充:        00 00 00 08  每一对都是一个字节

二进制:       00000000  00000000  00000000  00001000    

         这也就证明了前边四个字节是虚函数指针了。因为我们对成员变量  i 赋值只影响了后边四个字节,前面四个也就被证明为 虚函数指针所占用的地址了。结论在下图

        知识补充(32位系统 所有指针类型都占用4个字节,虚基类指针会有特殊情况,我们之后再谈,64位系统 指针类型 占用8 个字节)

         你以为到这里结束了? 不不不,这才刚刚开始

        单继承下的虚函数(指针不好的小伙伴,一定要恶补一下指针哦,太重要了)

我们让子类 Child 重写了基类 Base 的 b2虚函数

#include <iostream>
using namespace std;

class Base {
public:
	virtual void b1(){ cout << "Base::b1 " << endl; }
	virtual void b2(){ cout << "Base::b2 " << endl; }
	virtual void b3(){ cout << "Base::b3 " << endl; }

};

class Child:public Base {
public:
	virtual void b2(){ cout << "Child::b2 " << endl; }	//这里的virtual修饰可以不加,因为他们本来就是虚函数
};
int main()
{
	cout << sizeof(Base) << endl;	//4
	cout << sizeof(Child) << endl;	//4

	Child d;    //我们使用子类对象来进行测试,看看子类的虚函数表内容是什么

	long* vp = (long*)&d;//转换当前对象d的地址为int*类型,vp指向的是d的地址
	long* vptr = (long*)(*vp);//vptr现在指向的是虚函数表指针
	
	typedef void(*Func)(void);	//类型定义一个函数指针
	
	Func f1 = (Func)vptr[0];
	Func f2 = (Func)vptr[1];
	Func f3 = (Func)vptr[2];

	f1();
	f2();
	f3();
	
}

运行结果如下,

        我们在调用了子类 Child 中的 虚函数表里面的虚函数,会发现 虚函数表里面的b2虚函数已经被子类Child 重写了,这也就证明了父类的b2虚函数并不在 子类的虚函数表中;

        这里注意一点!!! 如果继续让vptr[下标] 让 【下标值】 继续增加会出现不寻常的后果,因为我们只定义了三个虚函数,后边的数据就无从得知了,都是未知的,会引发别的效果,有兴趣的小伙伴可以试一试

        我再把父类的虚函数表的内容还有地址打印出来再次论证

         我们会看到,b2的地址 父类和子类是不同的,证明了子类重写了b2虚函数,其他两个是相同的,这就有小伙伴要问了,那既然b1 和 b3 地址相等是不是就是同一张虚函数表呢?为啥子类重写了虚函数,父类的虚函数表不会改变呢?让我们继续论证。

         使用断点调式,用上面讲过的方法来找到对象d的地址;这里边的内容我就不多说了,因为没有成员变量,所以只有虚函数指针,让我们重新排列一下这个地址

        得到的结果为:0x00649cfc      这个就是虚函数指针指向的地址,也就是虚函数表的起始地址(虚函数表内部存放着若干个指针,分别指向虚函数)

        进入到虚函数指针,我们发现了什么!!! 这窜数字好像刚才在哪里见过,对了就是打印虚函数表的地址的时候(上面的子类基类的 控制台输出结果)我们给他排序一下

0x0064147e

0x00641465

0x00641474

这三个就是虚函数表里边的指针指向的地址(虚函数地址)

 再让我们看看基类b的内容  

第一步找到对象b的地址

         进入0x00649bfc

 地址如下:   基类:    0x0064147e             子类:       0x0064147e                                   

                                0x0064146f                             0x00641465

                                0x00641474                            0x00641474

细心的小伙伴会发现,基类和派生类的虚函数表头地址竟然不一样!!!

           基类  0x00649bfc                    派生类:    0x00649cfc

得到结论,虽然子类继承了基类的虚函数表,派生类如果不重写的话,他们仅仅是内容相同的两张表,在内存中的存放位置是不同的,只有虚函数表里面的指针指向的虚函数地址是相同的。

        留下一个问题,那么同一个类的 多个对象之间他们的虚函数表是否是同一张表呢?想知道的小伙伴可以根据上边的方法自己去验证,这样才能记忆深刻。

        让我们再来看看这样的内存布局有什么不同

红色的线,代表是同一个虚函数

        懂了上边的各种调试,在看这个图是不是突然豁然开朗,清晰明了

        单继承聊完了,再来让我们看看多继承是怎么样的

        因为是多继承,所以会有两个虚函数表指针!!!

        不要着急,先来看看他们在派生类内存中的位置

        我添加了如下内容

        基类1 有成员变量 i, 基类2 有成员变量 j ,共同的派生类有成员变量  k,派生类还有自己的新创建的虚函数 d1(注意不是重写从父类继承的,而是子类自己的虚函数)

根据这个虚函数指针的指向的地址,就能够判断这是谁的虚函数指针,记住这张图,后边还要用

        分别给他们赋值为8,观察地址变化

         得到派生类的内存布局,小知识(多继承的虚函数指针在派生类中的位置是根据派生类的继承顺序来决定的) 下边证明

        注意! 每次运行的地址都会有所不同

第一个基类b的地址

 

 第二个基类b2的地址

         还记得我让你们记住的那张图嘛,大家比对一下他们的地址的内容,你就明白了派生类的内存布局是怎么回事了,我把图再放到这里方便你们观看。

        小尝试(你可以把继承的关系换一下,先继承b2,在继承b,你看看会有什么变化)

        回到正题,那他们的虚函数表是怎么样的呢?是不是三张表呢?基类b一张,基类b2一张,派生类自己一张,那这样的话为啥是两个虚函数指针,而不是三个呢?继续探索

先来看看结果图

         进入到派生类的地址

 进入第一个虚函数表,跟基类 b 的虚函数表地址是一样的

进入第二个虚函数表, 跟基类 b2 的虚函数表地址是一样的

 细心的小伙伴会发现,后边有三个地址跟谁的地址也对不上,也不是第二个虚函数的地址,那么他们都是谁呢,,其实上边五个地址是第一张虚函数表的内容加上派生类的新的虚函数,所以我们知道了第一个继承的基类的虚函数表会和派生类的合并并且称为新的派生类的虚函数表,红色箭头指向的是前边我说过的未知的地址,他们不是很稳定,因为他们并不是我们要找的虚函数地址,那问题来了,那么第二张虚函数表呢?他在哪里呢?

 先来看看两个基类的布局,大家都很熟悉了

        再来看看子类的布局,这里要注意的是,这两张虚函数表跟俩个基类也是 仅仅是内容相同,在内存中的存放位置是不同的两张表;所以我们想要让子类来访问第二个虚函数表,就要调整this指针的偏移值,偏移到第二个虚函数指针的位置才可以;

        我的代码里用了,Base& b2 = d;        这句话的意思是让b2的对象指向子类,这里用到了多态,所以b2的位置就是在派生类d内存中的b2虚函数指针开始的位置,不要惊讶,编译器会根据代码自动调整this指针的;

        注意(我图中用b的虚函数指针和b2的虚函数指针,并不是b和b2对象的虚函数指针,而是他们Base1类和 Base2类的虚函数指针,这里不要被扰乱)

希望这篇文章能对大家有所帮助

猜你喜欢

转载自blog.csdn.net/weixin_45428525/article/details/120597984