C++虚函数实现模拟

前言

虚函数的实现包含两个重要部分,虚函数表和虚函数表指针。

  • 虚函数表:一个类如果有虚函数,则针对这个类会产生一个虚函数表;
    虚函数表属于类。
  • 虚函数表指针:生成这个类的对象时,对象里就包含了一个指针(虚函数指针),用来指向这个虚函数表的起始地址;
    虚函数表指针属于对象,一般位于对象内存的起始位置。

虚函数表分析

看如下示例代码(代码1),父类Base包含三个虚函数f、g、h,子类Derive重写了其中两个虚函数f、g:

class Base
{
    
    
public:
	Base() {
    
    }

	virtual void f() {
    
     cout << "Base::f()" << endl; }
	virtual void g(int n) {
    
     cout << "Base::g(" << n << ")" << endl; }
	virtual void h() {
    
     cout << "Base::h()" << endl; }
};


class Derive : public Base
{
    
    
public:
	Derive() {
    
      }
	void f() {
    
     cout << "Derive::f()" << endl; }
	void g(int n) {
    
     cout << "Derive::g(" << n << ")" << endl; }
};

int main()
{
    
    
	Base* pb = new Base;
	Base* pd = new Derive;

	pd->f();
	pd->g(5);
	pd->h();

	return 0;
}

其运行结果如下所示:
在这里插入图片描述
在调试时将断点设置在return 0;处,即可获得如下调试信息:
在这里插入图片描述

根据以上代码和调试信息,绘制出父类对象(pb)和子类对象(pd)内存布局如下所示:
虚函数内存布局

  • 虚函数的地址在编译器就确定了;
  • 虚函数表中存放的是此类对应的虚函数的地址,若子类未重写父类虚函数,则虚函数的地址为父类对应函数的地址,如Derive::h指向Base::h。虚函数表在编译期就行确定。
  • 类的虚函数表指针指向虚函数表的首地址。对象的虚函数表指针的值在运行期就行确定,即虚函数表指针的值在运行期会发生变化,故虚函数的调用在运行期才能确定。

虚函数模拟实现

针对以上代码,模拟实现虚函数,实现代码(代码2)如下:

class Base
{
    
    
public:
	Base() {
    
     
		__vfptr = vftable;/*模拟编译时插入的代码*/
		//... 用户代码
	 }

	void f() {
    
     cout << "Base::f()" << endl; }//virtual void f()
	void g(int n) {
    
     cout << "Base::g(" << n << ")" << endl; }//virtual void g()
	void h() {
    
     cout << "Base::h()" << endl; }//virtual void h()

	void** __vfptr;//虚表指针,跟随对象,模拟编译时插入的代码
	static void* vftable[3]; //虚函数表,跟随类
};

void* Base::vftable[3];//定义虚函数表


class Derive : public Base
{
    
    
public:
	Derive() {
    
    
		__vfptr = vftable;/*模拟编译时插入的代码*/
		//... 用户代码
	}
	void f() {
    
     cout << "Derive::f()" << endl; }
	void g(int n) {
    
     cout << "Derive::g(" << n << ")" << endl; }

	static void* vftable[3];
};

void* Derive::vftable[3];//定义虚函数表

int main()
{
    
    
	/* 模拟编译期确定虚函数表中每一项的值,编译器实现在main函数执行之前
	   Base::vftable = { &Base::f, &Base::g, &Base::h } */
	void(Base:: * b_pf)() = &Base::f;
	memcpy(Base::vftable + 0, &b_pf, sizeof(void*));
	void(Base:: * b_pg)(int) = &Base::g;
	memcpy(Base::vftable + 1, &b_pg, sizeof(void*));
	void(Base:: * b_ph)() = &Base::h;
	memcpy(Base::vftable + 2, &b_ph, sizeof(void*));

	/* Derive::vftable = { &Derive::f, &Derive::g, &Base::h } */
	void(Derive:: * d_pf)() = &Derive::f;
	memcpy(Derive::vftable + 0, &d_pf, sizeof(void*));
	void(Derive:: * d_pg)(int) = &Derive::g;
	memcpy(Derive::vftable + 1, &d_pg, sizeof(void*));
	void(Derive:: * d_ph)() = &Base::h;
	memcpy(Derive::vftable + 2, &d_ph, sizeof(void*));


	//运行期
	Base* pb = new Base;
	Base* pd = new Derive;

	//pd->f();  (pd->__vfptr[0])()
	void(Base:: * vf)();
	memcpy(&vf, pd->__vfptr + 0, sizeof(char*));
	(pd->*vf)();
	//pd->g(5); (pd->__vfptr[1])(5);
	void(Base:: * vg)(int);
	memcpy(&vg, pd->__vfptr + 1, sizeof(char*));
	(pd->*vg)(5);
	//pd->h();  (pd->__vfptr[2])();
	void(Base:: * vh)();
	memcpy(&vh, pd->__vfptr + 2, sizeof(char*));
	(pd->*vh)();

	return 0;
}

运行上述代码,得到和代码1一样的结果。

虚函数原理分析

  1. 虚函数表指针__vfptr是编译时插入的代码,其赋值在构造函数执行时。由于其作为成员变量,故发生复制时,会发生值拷贝。如:
    Base* p = new Base;// p->__vfptr指向Base的虚函数表
    p = new Derive;    // p->__vfptr指向Derive的虚函数表
    
    在构造Base的对象时,将对象的虚函数表地址设置为Base::vftable的首地址;当构造Derive对象时,构造的对象的虚函数表指针指向Derive::vftable。复制操作时,成员变量进行复制,此时p的虚函数表地址从Base::vftable变为Derive::vftable。
  2. 虚函数表的初始化发生在编译期,即在运行期不会发生变化。
  3. 虚函数的执行,先从虚函数表中找到虚函数的地址,然后调用此地址的函数。

猜你喜欢

转载自blog.csdn.net/kpengk/article/details/109709865