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.

相关推荐
爱上纯净的蓝天6 小时前
迁移面试题
java·网络·c++·pdf·c#
老赵的博客6 小时前
c++ 常用接口设计
开发语言·c++
JCBP_9 小时前
QT(1)
开发语言·汇编·c++·qt
郝学胜-神的一滴9 小时前
C++ Core Guidelines 核心理念
开发语言·c++·设计模式·代码规范
YLCHUP10 小时前
【联通分量】题解:P13823 「Diligent-OI R2 C」所谓伊人_连通分量_最短路_01bfs_图论_C++算法竞赛
c语言·数据结构·c++·算法·图论·广度优先·图搜索算法
minji...10 小时前
C++ string自定义类的实现
开发语言·c++
汤永红10 小时前
week4-[二维数组]平面上的点
c++·算法·平面·信睡奥赛
特立独行的猫a11 小时前
C/C++三方库移植到HarmonyOS平台详细教程
c语言·c++·harmonyos·napi·三方库·aki
谱写秋天12 小时前
VSCode+Qt+CMake详细地讲解
c++·ide·vscode·qt·编辑器
A7bert77712 小时前
【YOLOv5部署至RK3588】模型训练→转换RKNN→开发板部署
c++·人工智能·python·深度学习·yolo·目标检测·机器学习