c++虚函数和虚函数表

故事扮演 提交于 2020-02-28 09:55:14

前言

(1)虚基表与虚函数表是两个完全不同的概念

  • 虚基表用来解决继承的二义性(虚基类可以解决)。
  • 虚函数用来实现泛型编程,运行时多态。

(2)虚函数是在基类普通函数前加virtual关键字,是实现多态的基础
(3)虚函数表其实不用我们管这个编译器会帮我们做好
注:无特别说明本文的虚表均指虚函数表

(一) 什么是虚函数表?

虚函数(Virtual Function)是通过一张虚函数表(VirtualTable)来实现的。简称为V-Table。虚表(virtual table),编译器为每个拥有虚函数的类都建有一张虚函数表,主是要一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其容真实反应实际的函数。这样,在有虚函数的类的实例中这个表被分配在了这个实例的内存中,所以,当我们用父类的指针来操作一个子类的时候,这张虚函数表就显得由为重要了,它就像一个地图一样,指明了实际所应该调用的函数。

C++的编译器应该是保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证取到虚函数表的有最高的性能——如果有多层继承或是多重继承的情况下)

(二)含虚函数的单继承

单继承时,派生类中仅有一个虚函数表。这个虚函数表和基类的虚函数表不是一个表(无论派生类有没有重写基类的虚函数),但是如果派生类没有重写基类的虚函数的话,基类和派生类的虚函数表指向的函数地址都是相同的。

#include <iostream>
using namespace std;

class A
{
public :
	A(int a)
	{
		this->a = a;
	}
	virtual void show()
	{
		cout << "a=" << a << endl;
	}
protected:
	int a;
};

class B:public A
{
public:
	B(int a, int b) :A(a)
	{
		this->b = b;
	}
/*	void show()
	{
		cout << "a= " << a << " b = " << b << endl;
	}*/
protected:
	int b;
};

int main()
{
	A a(2);
	a.show();
	B b(1, 3);
	b.show();
	return 0;
}

此时类B,没有重写类A的show方法,仅仅是继承了父类
mei
可以看出,两个类的__vfptr的值不同,但是每个槽内部的函数地址都是相同的。
下面在类B中重写类A的show方法:

#include <iostream>
using namespace std;

class A
{
public :
	A(int a)  {this->a = a;}
	virtual void show()   {cout << "a=" << a << endl;}
protected:
	int a;
};

class B:public A
{
public:
	B(int a, int b) :A(a)  {this->b = b;}
	void show()  {cout << "a= " << a << " b = " << b << endl;}
protected:
	int b;
};

int main()
{
	A a(2);
	a.show();
	B b(1, 3);
	b.show();
	return 0;
}

重写
通过上面可以总结:派生类内存布局,先是复制一份基类内存布局,然后是自己的布局(注意内存对齐)。虚表指针指向自己的虚表,派生类虚函数地址如果自己未覆盖,那么就是基类的,否则是自己的函数地址,并且可以看到派生类一旦重写父类的虚函数就会覆盖原来继承的,
关于这一点的讲解,我认为这个大佬讲的不错:虚函数表解析

(三)含虚函数的多继承

多继承情况下,派生类中有多个虚函数表,虚函数的排列方式和继承的顺序一致。派生类重写函数将会覆盖所有虚函数表的同名内容,派生类自定义新的虚函数将会在第一个类的虚函数表的后面进行扩充。

#include<iostream>
using namespace std;
class Base
{
public:
	Base(int base){	this->base = base;}
	virtual void show()  {cout << base << endl;}
	virtual void print() { cout << "test  Base " << endl; }
protected:
	int base;
};
class BaseA
{
public:
	BaseA(int basea){this->basea = basea;}
	virtual void show(){cout << basea << endl;}
	virtual void watch() { cout << "test  BaseA" << endl; }
protected:
	int basea;
};
class BaseB :public Base, public BaseA
{
public:
	BaseB(int base, int basea, int baseb) :Base(base), BaseA(basea)
	{
		this->baseb = baseb;
	}
	void show(){cout << base << basea << baseb << endl;}
	virtual void print() { cout << "test  B " << endl; }//重写base的print方法,没有重写BASEA的watch方法
private:
	int baseb;
};
int main()
{
    Base base(1);
	BaseA baseA(2);
	BaseB baseB(3,3, 3);
	return 0;
}

2
这里通过编译器的部分可以看出来,未被重写的虚函数指针将和基类指向同一个位置,一旦被重写,函数指针就指向新的位置。先按照继承顺序,从左到右排布基类的布局包括虚标指针,然后排布自己的指针和数据;派生类虚表排布形式是按照继承顺序,是继承来的虚函数,如果有覆盖则换成自己的函数地址;然后是下一个基类,直至基类排布完毕。继承来的多张表是独立的(从内存布局中的多个虚表指针可以看出),且使用首地址+偏移量的形式来访问。
注意:如果派生类有自己的虚函数则会加在第一个基类的虚表末尾
以前关于虚函数表不太明白,今天网上搜集了些资料,在这里总结一下,如果有错误欢迎讨论啊~

附录

这里介绍一种查看内存布局的方法:
1.点击图中红圈打开“开发人员命令提示符
图一
2.通过dos命令进入代码所在目录
3.使用cl命令的"/d1 reportAllClassLayout或reportSingleClassLayoutXXX"选项。这里的reportAllClassLayout选项会打印大量相关类的信息,一般用处不大。而reportSingleClassLayoutXXX选项的XXX代表要编译的代码中类的名字(这里XXX类),打印XXX类的内存布局和虚函数表(如果代码中没有对应的类,则选项无效)。
例如我的:
在这里插入图片描述
在vs中查看变量的另一种方法是:
在调试模式下,点击窗口==>自动窗口就可以了
在这里插入图片描述
注意一定是调试模式,否则没有此按钮,同时注意设置断点。

参考文章:
虚函数表解析
C++虚函数和虚函数表原理

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!