C++——多态底层原理

虚函数表

先来看这个问题:

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

sizeof(Base)是多少?

答案是:8

因为Base中除了成员变量_b,还有一个虚函数表_vfptr(当类中有虚函数就会生成),虚函数表的本质是函数指针数组,用来存储虚函数的指针


cpp 复制代码
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 _a = 1;
};

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

void test1()
{
	Base bs;
	Derive de;
}


基类虚函数表

派生类基函数表

派生类虚函数表是这样生成的:先将基类的虚函数表内容拷贝过来,在把派生类中重写的虚函数的地址覆盖到被重写的虚函数地址上,最后将派生类自己增加的虚函数依次增加到虚表的末尾。

上面这个例子中,派生类和基类的虚函数表都存储有两个函数地址,即Func1和Func2,但是派生类只对Func1重写,没有对Func2重写,编译器会将派生类中的Func1的地址进行覆盖,所以Func1的地址不同,Func2的地址相同(0x009f1370)

注意:虚函数存在哪,虚函数表存在哪?

虚函数和普通的成员函数一样都存在公共代码段,虚函数表只是存了虚函数的指针;虚函数表存在哪,这个要看编译器,感兴趣可以自己验证。

我的编译器是将虚函数表存储在类对象的开始位置,使用32位系统虚表大小4byte

cpp 复制代码
void test()
{
	int i = 0;
	printf("栈区:%p\n", &i);

	char* ch = new char;
	printf("堆区:%p\n", ch);

	static int ci = 1;
	printf("数据段:%p\n", &ci);

	const char* s = "hello";
	printf("代码段:%p\n", s);

	Derive de;
	printf("虚表:%p\n", *((int*)&de));

}

虚表地址和代码段地址相差48byte,基本可以认为虚表存在代码段。大家可以参考这个方法,根据自己的编译器和环境来测试


多态原理

说了这么多,虚函数表在多态中有什么用?

当我们有基类的指针或引用调用派生类的虚函数时,第一步是将派生类赋值给基类,这时派生类只会将属于基类的那部分交给基类的指针或引用,而属于基类的那部分中包含了派生类的虚函数表。这样造成的结果就是,调用虚函数时调用了派生类中重写过的虚函数,实现了多态。

为什么直接将派生类赋值给基类就不能实现多态呢?因为这种写法会在编译器编译时,直接从符号表确定函数地址,程序运行时直接调用。而多态调用在编译期间不会确定,只在程序运行时到对象中寻找函数。其实编译器并不知道这个对象是基类还是派生类,只是无脑从虚函数表中找。


下面这个测试程序,通过观察汇编代码,就可以直观看出普通调用和多态调用的区别:

cpp 复制代码
void test()
{
	Base bs;
	Derive de;
	bs = de;
	Base& b = de;
	
	bs.Func1();

	b.Func1();
}

多继承的虚函数表

前面都是以单继承为例讲虚函数表和多态原理,下面我们看多继承的虚函数表

cpp 复制代码
class Base1
{
public:
	virtual void Func1()
	{
		cout << "Base1::Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "Base1::Func2()" << endl;
	}
private:
	int _a = 1;
};

class Base2
{
public:
	virtual void Func3()
	{
		cout << "Base2::Func3()" << endl;
	}

	virtual void Func4()
	{
		cout << "Base2::Func4()" << endl;
	}
};
class Derive :public Base1 ,public Base2
{
public:
	void Func1()
	{
		cout << "Derive::Func1()" << endl;
	}
	void Func3()
	{
		cout << "Derive::Func3()" << endl;
	}
	virtual void Func5()
	{
		cout << "Derive::Func5()" << endl;
	}
private:
	int _b = 2;
};

void test()
{
	Base1 b1;
	Base2  b2;
	Derive d;
}

调试:

多继承后的派生类有两个虚函数表,Func2和Func4没有重写,与基类函数地址相同。Func1和Func3重写后进行覆盖。派生类新增加的虚函数地址存到了最先继承的基类Base1的虚表中(第一张图的监视窗口未显示,有bug,在内存窗口可以看到)

重写是对虚函数函数体部分的重写

看下面程序的运行结果是什么:

cpp 复制代码
 class A
 {
 public:
   virtual void func(int val = 1){ std::cout<<"A->"<< val <<std::endl;}
   virtual void test(){ func();}
 };
 class B : public A
 {
 public:
   void func(int val=0){ std::cout<<"B->"<< val <<std::endl; }
 };
 int main(int argc ,char* argv[])
 {
   B*p = new B;
   p->test();
   return 0;
 }

test函数没有重写,直接调用A::test();func函数被重写,虚表中是B中func的函数地址,又因为重写只是对函数体的重写,val缺省值是0,结果是B->0.

相关推荐
Zfox_38 分钟前
【Linux】进程信号全攻略(二)
linux·运维·c语言·c++
起名字真南1 小时前
【OJ题解】C++实现字符串大数相乘:无BigInteger库的字符串乘积解决方案
开发语言·c++·leetcode
少年负剑去1 小时前
第十五届蓝桥杯C/C++B组题解——数字接龙
c语言·c++·蓝桥杯
cleveryuoyuo1 小时前
AVL树的旋转
c++
神仙别闹1 小时前
基于MFC实现的赛车游戏
c++·游戏·mfc
小c君tt1 小时前
MFC中 error C2440错误分析及解决方法
c++·mfc
木向2 小时前
leetcode92:反转链表||
数据结构·c++·算法·leetcode·链表
阿阿越2 小时前
算法每日练 -- 双指针篇(持续更新中)
数据结构·c++·算法
hunandede2 小时前
FFmpeg存放压缩后的音视频数据的结构体:AVPacket简介,结构体,函数
c++
hunandede3 小时前
FFmpeg 4.3 音视频-多路H265监控录放C++开发十三:将AVFrame转换成AVPacket。视频编码,AVPacket 重要函数,结构体成员学习
c++·ffmpeg·音视频