C++虚函数表——套娃的指针

前言

面向对象的程序设计最重要的三个特点是:多态、继承、封装。在C++中,为了实现多态,使用了一种动态绑定的方法。这个方法的核心表现形式就是C++的虚函数表,本文主要是验证虚函数表的一些特点(PS:就是有个臭毛病,书上说的不太信,非要自己验证一番)。

类的虚函数表

所谓虚函数,就是在某基类中声明为virtual并在一个或多个派生类中被重新定义的成员函数。
用法格式:

virtual 函数返回类型 函数名(参数列表)
{函数体}

(PS:最明显的标志就是virtual,被virtual修饰的就是虚函数)。

虚函数表是什么呢?

所谓虚函数表,其本质是一个指针数组(PS:指针数组,就是一个数组,其每一个元素都是一个指针)。这个数组的每一个元素都是一个指针,指向基类或者派生类的虚函数。程序员可以通过这个指针来访问对应的虚函数。

每一个包含虚函数的类都有自己的虚函数表,无论这个类是基类还是派生类。虚函数表与类是一对一绑定关系,一个类对应一个虚函数表,一个虚函数表对应一个类。

虚函数表有什么好处呢?

实现面向对象的程序设计,无论是继承还是重写,让派生类的开发和管理更加方便、形象。

个别代码的含义解析

&a						//变量a的内存地址
(int64_t*)&a			//将a的地址转化为int64_t*类型的指针,该指针指向变量a
*(int64_t*)&a			//对变量a内存的第一块空间解引用(读取内存地址里的值),得到的还是一个内存地址。该内存地址是变量a类型的虚函数表的地址(也是虚函数表第一个元素的地址)。
(int64_t*)*(int64_t*)&a	//将虚函数表的地址转化为int64_t*类型的指针,该指针指向虚函数表的第一个元素。
*(int64_t*)*(int64_t*)&a//对虚函数表的第一个元素的内存地址解引用(读取该内存里的值),得到的依然是一个内存地址,该内存地址是变量a类型的虚函数的函数体的地址。

typedef void(*VTable)();
(VTable)*(int64_t*)*(int64_t*)&a
//将函数体的地址转化为VTable类型的函数指针,然后通过该指针执行函数体。

一、类的虚函数表

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

定义Base基类,该类有三个虚函数f()、g()、h()。执行这三个函数会分别在控制台输出Base::f、Base::g、Base::h。表示类名::函数名。

验证:每一个类都只有一个虚函数表。
int main(){
	Base a;
	Base b;

	cout << &a <<endl;
	cout << (int64_t*)&a << endl;
	cout << *(int64_t*)&a << endl;
	cout << (int64_t*)*(int64_t*)&a << endl;
	cout << *(int64_t*)*(int64_t*)&a << endl;
	//访问变量a函数体f
	VTable va_f = (VTable)*(int64_t*)*(int64_t *)&a;
    va_f();
    //访问变量a函数体g
	VTable va_g = (VTable)*((int64_t*)*(int64_t *)&a+1);
    va_g();
    //访问变量a函数体h
	VTable va_h = (VTable)*((int64_t*)*(int64_t *)&a+2);
    va_h();
	

	cout << &b <<endl;
	cout << (int64_t*)&b << endl;
	cout << *(int64_t*)&b << endl;
	cout << (int64_t*)*(int64_t*)&b << endl;
	cout << *(int64_t*)*(int64_t*)&b << endl;
	//访问变量b函数体f
	VTable vb_f = (VTable)*(int64_t*)*(int64_t *)&b;
    vb_f();
    //访问变量b函数体g
	VTable vb_g = (VTable)*((int64_t*)*(int64_t *)&b+1);
    vb_g();
    //访问变量b函数体h
	VTable vb_h = (VTable)*((int64_t*)*(int64_t *)&b+2);
    vb_h();
    
	return 0;
}

程序执行结果如图所示:
在这里插入图片描述

从上述结果可以看出,尽管变量a、b自身所在的内存地址是不一样的,但是其保存的第一个元素都是指向Base类的虚函数表的指针,无论是虚函数表的地址,还是虚函数表中指向f函数体的地址都是一样的。

二、派生类有自己的虚函数表(无重写单继承)

class Base{

public:
    virtual void f() { cout << "Base::f" << endl; }
    virtual void g() { cout << "Base::g" << endl; }
    virtual void h() { cout << "Base::h" << endl; }
};
class Drive :public Base {
};
验证:派生类的虚函数表和基类的虚函数表不是一个。
int main(){
	Base a;
	Drive c;

	cout << &a <<endl;
	cout << (int64_t*)&a << endl;
	cout << *(int64_t*)&a << endl;
	cout << (int64_t*)*(int64_t*)&a << endl;
	cout << *(int64_t*)*(int64_t*)&a << endl;
	//访问变量a函数体f
	VTable va_f = (VTable)*(int64_t*)*(int64_t *)&a;
    va_f();
    //访问变量a函数体g
	VTable va_g = (VTable)*((int64_t*)*(int64_t *)&a+1);
    va_g();
    //访问变量a函数体h
	VTable va_h = (VTable)*((int64_t*)*(int64_t *)&a+2);
    va_h();

	cout << &c <<endl;
	cout << (int64_t*)&c << endl;
	cout << *(int64_t*)&c << endl;
	cout << (int64_t*)*(int64_t*)&c << endl;
	cout << *(int64_t*)*(int64_t*)&c << endl;
	//访问变量c函数体f
	VTable vc_f = (VTable)*(int64_t*)*(int64_t *)&c;
    vc_f();
    //访问变量c函数体g
	VTable vc_g = (VTable)*((int64_t*)*(int64_t *)&c+1);
    vc_g();
    //访问变量b函数体h
	VTable vc_h = (VTable)*((int64_t*)*(int64_t *)&c+2);
    vc_h();
	return 0;
}

程序执行结果:
在这里插入图片描述

从上述结果可以看出,变量Base::a、Drive::c自身所在的内存地址是不一样,其指向的虚函数表的地址也不一样,但是虚函数表中指向f函数体的地址都是一样的。

三、派生类重写虚函数

public:
    virtual void f() { cout << "Base::f" << endl; }
    virtual void g() { cout << "Base::g" << endl; }
    virtual void h() { cout << "Base::h" << endl; }
};
class Drive1 :public Base {
    virtual void h() {cout << "Drive :: h" << endl;}
};
验证:重写的虚函数其地址应该是新的,不应该是在基类虚函数的地址上覆盖。
int main(){
	Base a;
	Drive1 d;

	cout << &a <<endl;
	cout << (int64_t*)&a << endl;
	cout << *(int64_t*)&a << endl;
	cout << (int64_t*)*(int64_t*)&a << endl;
	cout << *(int64_t*)*(int64_t*)&a << endl;
	cout << *((int64_t*)*(int64_t *)&a +2)<<endl;
	//访问变量a函数体f
	VTable va_f = (VTable)*(int64_t*)*(int64_t *)&a;
    va_f();
    //访问变量a函数体g
	VTable va_g = (VTable)*((int64_t*)*(int64_t *)&a+1);
    va_g();
    //访问变量a函数体h
	VTable va_h = (VTable)*((int64_t*)*(int64_t *)&a+2);
    va_h();

	cout << &d <<endl;
	cout << (int64_t*)&d << endl;
	cout << *(int64_t*)&d << endl;
	cout << (int64_t*)*(int64_t*)&d << endl;
	cout << *(int64_t*)*(int64_t*)&d << endl;
	cout << *((int64_t*)*(int64_t *)&d +2)<<endl;

	//访问变量d函数体f
	VTable vd_f = (VTable)*(int64_t*)*(int64_t *)&d;
    vd_f();
    //访问变量c函数体g
	VTable vd_g = (VTable)*((int64_t*)*(int64_t *)&d+1);
    vd_g();
    //访问变量b函数体h
	VTable vd_h = (VTable)*((int64_t*)*(int64_t *)&d+2);
    vd_h();
	return 0;
}

程序执行结果:
在这里插入图片描述
从上述结果可以看出,变量Base::a、Drive1::d自身所在的内存地址是不一样,其指向的虚函数表的地址也不一样,虚函数表中指向没有重写的虚函数的地址是一样的。但是重写后的虚函数的地址是不一样的,是一个新的地址,并不是在基类该虚函数的地址上覆盖。

参考博客:

[1]https://blog.csdn.net/lihao21/article/details/50688337
[2]https://blog.csdn.net/haoel/article/details/1948051
[3]https://blog.csdn.net/u012630961/article/details/81226351?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromBaidu-3.control&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromBaidu-3.control
[4]https://blog.twofei.com/496/
[5]https://www.cnblogs.com/rollenholt/articles/2023364.html
[6]https://blog.csdn.net/a13602955218/article/details/104743492

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值