前言

想深入了解一下(或者说验证)对象底层是如何工作的

重点是虚函数的调用, 虚指针的生成, 虚表中的数据

以及这些数据在多重继承, 虚继承, 多重虚继承环境下的表现

 

虚指针(virtual point)

考虑以下代码

输出和预期一致, 其中 func 是被 B 覆盖过的虚函数, 而 func2 则未被覆盖

以下是生成的汇编代码:

所以调用方式和书中一致, 从 vptr 索引虚表, 获得相应的 slot

这些信息全部都是在编译的时候由编译器生成

有一个点我忽略了: 单一继承对象只有一个虚表

后续我加上 C 对象后( C 继承自 A ), 让 B 继承 A, 这样A中就有 B C

但是依旧只有一个虚表, 仅当我让A再继承一个对象时, 这时产生了 2 个虚表

可能会问, 那么C对象是如何通过 pc->A::func() 这样的形式来调用 A 作用域的函数的呢?

答案是, 这会是一个单纯的函数调用, 并不会通过虚表或虚指针, 也没有任何的偏移

(emmm... 也就是说普通成员函数对于类来说, 可能更像是个陌生人, 即使它是成员)

(这也是为什么大多数情况下基类需要 virtual destruct 的原因)

 

虚析构是如何被调用的

在写标题的时候大概猜到了, 其实虚析构就是一个在子类中占了一个不能被重写的虚表槽

应该就是通过简单的偏移来调用的, 来试一下

(重点在于虚析构是不可能被覆盖的, 因为子类中不可能存在同名的函数)

汇编:

B的析构:

和预料中不同的是, A 的析构函数是直接调用的, 而并非偏移

想了想这在意料之中, 因为编译器将它优化成了普通函数

至于 _ZdlPv, 没有在汇编中找到这个符号 ...

(我试了一下, 没有什么好的方式让它像被虚函数一样调用, 很可惜...)

 

虚继承的析构函数调用

感觉非常奇怪, 所以本来是打算看看就好的, 因为之前花功夫去看了看虚继承的内存布局

不过实在太奇怪了, 所以打算好好分析一下, 顺便我在之前分析的时候好像忘了看虚继承的虚函数调用了

因为那时候好像已经晕了...

它会怎么被调用呢? 最无趣的情况就是像上面那样硬编码

不过在虚继承这种比较复杂的环境下, 编译器可能会做出其他反应也说不定?

源码就单纯的E被CD虚继承, B继承CD, 这里直接汇编:

B的析构函数:

虚函数是通过硬编码来调用的

不过有个非常不错的发现, 我在 _ZTT1B 中发现了 LTHUNK 的影子

在 _ZTT1B 中有 LTHUNK 相关的代码, 不过很可惜, 这些猜测无从证明...

来看看虚函数的调用吧:

普通虚函数的调用就是找到自己的虚表, 调用对应的槽

这个虚继承来的虚函数调用和普通继承的虚函数调用方式基本一致

多态是如何实现的呢?

每个构造函数, 都会覆写它的所有虚指针, 使他指向不同的地址(但是这些地址都不相同!!!)

如你所见, 当C指针去调用时, 发生了偏移, 虽然取到了同样的 slot, 但却不是同一张虚表

那么我能想到的就是有多张相同的虚表

(我只能如此猜测, 我想不到为什么这样的情况下还能调用同一个函数)

(但是为什么要有相同的呢? 这又是一个问题)

我尝试了看看这些虚表中是什么数据, 可惜失败了, 或者说看不懂

但是同一个类的虚表是完全相同的, 这可以肯定

不同的情况仅在于A类中C类和单独的C类, 这两个C类的虚表是不一样的

 

summary

虚函数机制和书中所说基本一致

  1. 找到对应的虚表
  2. 调用槽中的函数

 

review

我在 objdump 的输出中发现, 诸如 _ZTI1C 这样的东西在链接的时候会转化成一个数字常量

有些可能会是基于某个位置的偏移(比如 %rip)

编译器会利用它进行直接寻址, 有些会利用它进行间接寻址