C 语言

关注公众号 jb51net

关闭
首页 > 软件编程 > C 语言 > C++虚表&多态实现原理

C++虚表&多态的实现原理分析

作者:爱上小公举

文章介绍了C++虚函数的实现机制,通过虚函数指针(vfptr)和虚函数表(vftable)实现多态,对象包含vfptr指针,虚表存函数地址并以空指针结尾,继承时虚表继承并扩展,多态需基类指针引用和虚函数重写,利用虚表实现动态绑定

C++虚表&多态实现原理

这里我们只看继承中的多态 ,本文程序调试在VS2017

先看一个小问题,下面的类A实例化出的对象占几个字节呢?

#include<iostream>
using namespace std;
class A {
	int m_a;
public:
	void func(){
		cout << "类A的func" << endl;
	}
};
int main() {
        A a;
	cout << sizeof(a) << endl;
	system("pause");
	return 0;
}

答案是4字节,这是因为成员函数存放在公共的代码段, 所以只计算成员变量m_a所占字节的大小

调试一下来看, 

我们如果将成员函数定义成虚函数又会如何呢?

我们来看:

#include<iostream>
using namespace std;
class A {
	int m_a;
public:
	virtual void func(){
		cout << "类A的func" << endl;
	}
	virtual int func1() {
		cout << "类A的func1" << endl;
		return 0;
	}
};
int main() {
	A a,b;
	cout << sizeof(A) << endl;
	system("pause");
	return 0;
}

可以看到结果是8字节,emm.... 事出反常必有妖,我们调试一下,看看到底多出来了个什么东西

我们可以看到,在实例化出的对象a和b中多了一个_vfptr,它的类型时void**,是一个二级指针,指针在32位平台中占4字节,所以这里的结果是8(m_a的4字节+_vfptr的4字节),那么_vfptr到底是个什么东西? 

类中有了虚函数之后才有了_vfptr,它们之间到底有着什么关系?

其实,_vfptr,其实就是虚函数指针 (virtual function pointer)

可以看到_vfptr 指向了一个 vftable(virtual function table) 虚函数表(也叫虚表),虚表中元素是void*类型 ,第一个元素是指向了虚函数func(),第二个元素指向了fun1()

虚函数指针(_vfptr) 和 虚函数表(vftable)

虚函数指针和虚表是什么

通过上面的调试,我们已经看到了,_vfptr是指向虚表的一个指针,那么我们也可以叫 _vfptr 为虚函数表指针

虚表本质上是一个在编译时就已经确定好了的void* 类型的指针数组 .

注意 :  虚函数表为了标志结尾,会在虚表最后一个元素位置保存一个空指针.所以看到的虚表元素个数比实际虚函数个数多一个

C++中的虚函数的实现一般是通过虚函数表 (C++规范并没有规定具体用哪种方法,但大部分的编译器都用虚函数表的方法)  大多数编译器(如本文用的VS)中虚函数表指针都在对象的最前面位置,意味着能通过对象的地址就能遍历虚函数表(能够在多层继承或多重继承中保持较高性能)

虚函数是为了继承时的多态才有的概念,上面简单了解了一下虚表,我们再来看继承关系中的虚表

继承中的虚表

在有虚函数的类(有虚表的类)被继承后,  虚表也会被拷贝给派生类. 注意,编译器会给派生类新分配一片空间来拷贝基类的虚表,将这个虚表的指针给派生类, 而并不是沿用基类的虚表,在发生虚函数的重写时,重写的是派生类为了拷贝基类虚表新创建的这虚表中的虚函数地址

虚表为所有这个类的对象所共享.  注意,是通过给每个对象一个虚表指针_vfptr共享到的虚表.

单继承中的虚表

1. 单继承未重写虚函数: 会继承派生类的虚表,如果派生类中新增了虚函数,则会加继承的虚表后面

2. 单继承重写虚函数: 继承的虚表中被重写的虚函数地址会在继承虚表时被修改为派生类函数的地址(如下面例子中把A::func()修改成了B::func()的地址)(注意: 此时基类的虚表并没有被修改,修改的是派生类自己的虚表)

所以, 重写实际上就是在继承基类虚表时,把基类的虚函数地址修改为派生类虚函数的地址

举个栗子

#include<iostream>
using namespace std;
class A {
	int m_a;
public:
	virtual void func(){
		cout << "类A的func" << endl;
	}
	virtual int func1() {
		cout << "类A的func1" << endl;
		return 0;
	}
};
class B :public A {
public:
	virtual void func() {
		cout << "类B的func" << endl;
	}
	virtual void func2() {
		cout << "类B的func2" << endl;
	}
};
int main() {
        A a1;
        A a2;
	B b;
	system("pause");
	return 0;
}

调试如下:      

可以看到对象b中的_vfptr所指向的虚表继承了类A的虚表 ,但是地址却和a1,a2的_vfptr不一样,也印证了前面所说,新分配了一片空间来拷贝基类的虚表. 

还可以看到a1和a2的虚表地址相同,也印证了前面所说,类的虚表被所有对象所共享. 

但我们却发现,B中也有虚函数,怎么没有了,讲道理这是不科学的,那么B类的虚函数的地址放到底哪去了呢?

思考一下,它肯定是存在的,要么另外建一张虚表放里面,要么放在继承A的虚表里.

实际上,在单继承中,派生类的虚函数地址会放在继承来的基类的虚表后面,只是VS这里没有显示出来.

我们也可以看到图中红色圈出来的vftable[4],虚表中也已经有了四个元素,既然VS不给力,只能自己想办法,可以在监视窗口,通过地址看到,B类中的虚函数指针指向的虚表,其实是这样的,如下 

还可以看到,继承的A中的虚函数func()被重写之后,虚表中就放的是重写后的B中的func()的地址 

多继承中的虚表

举个栗子

#include<iostream>
using namespace std;
class A {
	int m_a;
public:
	virtual void funcA() {
		cout << "类A的funcA" << endl;
	}
	virtual void func() {
		cout << "类A的func" << endl;
	}
};
class B {
public:
	virtual void funcB() {
		cout << "类B的funcB" << endl;
	}
	virtual void func() {
		cout << "类B的func" << endl;
	}
};
class C :public A,public B {
public:
	virtual void func() {
		cout << "类C的func" << endl;
	}
	virtual void funcC() {
		cout << "类C的funcC" << endl;
	}
};
int main() {
	C c;
	A a;
	system("pause");
	return 0;
}

调试如下 :

可以看到,派生类继承了两张虚表,A::func()和B::func()的地址修改为了C:::func()的地址

我们再次通过_vfptr的地址,在监视窗口可以看到,派生类中新增的虚函数,虚函数地址被加在了派生类拷贝基类的第一张虚表的后面. 

多态的原理

我们回忆一下多态的两个构成条件

简单来说就是,利用了虚函数可以重写的特性,当一个有虚函数的基类有多个派生类时,通过各个派生类对基类虚函数的不同重写,实现通过指向派生类对象基类指针基类引用调用同一个虚函数,去实现不同功能的特性. 抽象来说就是,为了完成某个行为,不同的对象去完成时会产生多种不同的状态

总结

派生类的虚表生成:

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

您可能感兴趣的文章:
阅读全文