java有虚函数表_理解 C++ 虚函数表

引言

虚表是 C++ 中一个十分重要的概念,面向对象编程的多态性在 C++ 中的实现全靠虚表来实现。在聊虚表之前我们先回顾一下什么事多态性。

多态实际上就是让一个父类指针,通过赋予子类对象的地址,可以呈现出多种形态和功能。如果这么说比较抽象的话,我们看一个例子就明白了:

class Base {

int m_tag;

public:

Base(int tag) : m_tag(tag) {}

void print() {

cout << "Base::print() called" << endl;

}

virtual void vPrint() {

cout << "Base::vPrint() called" << endl;

}

virtual void printTag() {

cout << "Base::m_tag of this instance is: " << m_tag << endl;

}

};

class Derived : public Base {

public:

Derived(int tag) : Base(tag) {}

void print() {

cout << "Derived1::print() called" << endl;

}

virtual void vPrint() {

cout << "Derived::vPrint() called" << endl;

}

};

在上面的代码中,我们声明了一个父类 Base,和它的一个派生类 Derived,其中 print() 实例方法是非虚函数,而其余两个实例方法被声明为了虚函数。并且在子类中我们重新了 print() 和 vPrint()。下面我们构造出一个 Derived 实例,并分别将其地址赋给 Base 指针和 Derived 指针:

int main(int argc, char *argv[]) {

Derived *foo = new Derived(1);

Base *bar = foo;

foo->print();

foo->vPrint();

bar->print();

bar->vPrint();

return 0;

}

我们看看程序运行的结果:

Derived1::print() called

Derived::vPrint() called

Base::print() called

Derived::vPrint() called

可以看到,对于 Derived 指针的操作正如它应该表现的样子,然而当我们把相同对象的地址赋给 Base 指针时,可以发现它的非虚函数竟然表现出了父类的行为,并没有被重写的样子。

这是什么原因呢?

C++ 类的实质是什么

首先我们要明白 C++ 中类的实质到底是什么。实际上,类在 C++ 中就是 struct (结构体)的一种扩展,允许了更高级的继承和虚函数。那么也就是说,结构体缺少的实际上就是虚函数。

对于一般的成员变量,它和结构体在内存布局上是完全一样的,不管是顺序还是内存对齐,完全一致。而一个类的方法地址并不会存储在一个实例的内存中。对于非虚函数,它们在内存中的地址是唯一的,你可以把它想象成普通函数,只不过第一个参数是 this 指针,在通过类对象指针调用时,编译器会根据类型找到相应非虚函数的地址,这个工作是编译时完成的。

64f3b9c22898

也就是说,什么指针指向什么函数这是固定的,反正指针如果是 Base *,那我就直接执行 Base::print() 函数。

揭开 vTable 的神秘面纱

既然非虚函数实现这么简单,那虚函数是不是会很复杂?其实并不是那么复杂。虚函数的地址被存储一张叫做虚表的东西里,我们其实很容易拿到这个虚表。下面我们通过 dump memory 的方式来揪出一个类的虚表:

64f3b9c22898

看到我选中的那个字节,那是我们的一个实例变量,在这个实例变量的前面有 8 个字节的内容,那实际就是虚表的地址了,我们尝试将这个地址所指向的内容拿出来:

64f3b9c22898

这就是虚表的内容了,什么?你不信,下面我就把虚表中第一个函数揪出来执行一下:

64f3b9c22898

可以看到,Derived 类中重写的 vPrint() 方法已经被执行。这就说明虚函数在执行时是一个动态的过程,并不是在编译时就确定下来要执行哪一个函数,而是运行时从虚表查到真正要执行的函数的地址,然后再将 this 指针传入执行。

到这里,我们已经大致了解了虚函数是怎样工作的了。下面我们看看 Base 类和 Derived 类的虚表有什么区别。我修改了源码,实例化了一个 Base 类对象 baz,然后分别 dump 出 Base 类和 Derived 类的内存:

64f3b9c22898

可以看出,两个对象的虚表指针是不同的。然后我们看看这两者虚表有什么不同:

64f3b9c22898

这两张虚表的第一个函数不同,因为 Derived 类重写了 vPrint() 方法,所以 Derived 的虚表第一个函数指针会有不同,而 printTag() 我并没有重写,所以两张表指向一个同一个函数。

所以每个类都会维护一张虚表,编译时,编译器根据类的声明创建出虚表,当对象被构造时,虚表的地址就会被写入这个对象内存的起始位置。这就是多态性在 C++ 中实现的方式,而像 Java、OC 这样的语言由于 Runtime 的存在,这些对象会有多余的内存空间记录类的信息(meta-object),在运行时根据这些信息解析出相应的函数去执行。虽然不同,但是异曲同工。

理解虚函数表有什么作用呢?

能让你更好地理解 C++

一些 hook 技术就是利用虚表来实现的

Wrap Up

这篇文章就简单地讲了一下多态和虚函数在 C++ 中的实现,我们说 C++ 非常 magical 就是因为它能用最简单的方式去实现各种面向对象编程的特性,十分值得我们终身学习。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值