C++和C#中的vtable是如何实现的?

6
让我们来看一个情况(在C++中,在C#中,类A、B是接口):
class A { virtual void func() = 0; };
class B { virtual void func() = 0; };
class X: public A, public B { virtual void func(){ var = 1; } int var;};

X * x = new X; // from what I know, x have 2 vtables, is this the same in c#?
A * a = (A*)x; // a == x
B * b = (B*)x; // here b != x, so when calling b->func(), how is the address of var correct?

c#编译器是否总是创建一个vtable?在进行类型转换时,它是否会进行任何指针修复?


2
我想你的意思是 a == xb != x。由于指针在堆栈上一个接一个地分配,它们都有不同的地址...尽管它们应该都指向同一个对象。 - PypeBros
当然,我的错误 :) 现在已经修复了,我忘记了 a == b 比较的是地址。 - chris
4个回答

3

如果我使用g++学习这个派生版本

class X: public A, public B { 
   unsigned magic;
 public:
   X() : magic(0xcafebabe) {};
   virtual void func(){ var = 1; } int var;
};

extern "C" int main() 
{
   X * x = new X; // from what I know, x have 2 vtables, is this the same in c#?
   A * a = (A*)x; // &a == &x
   B * b = (B*)x; // here &b != &x, so when calling b->func(), how is the address of var correct?
   printf("%p -- %p -- %p\n", x, a, b);

   unsigned* p = (unsigned*)((void*) x);
   unsigned *q = (unsigned*)(p[1]);
   printf("x=[%x %x %x %x]\n",p[0],p[1],p[2],p[3]);
   p = (unsigned*)(p[0]);
   printf("a=[%x %x %x %x]\n",p[0],p[1],p[2],p[3]);
   printf("b=[%x %x %x %x]\n",q[0],q[1],q[2],q[3]);

}

原来,在C++中b == a+1,因此X的结构为[vtable-X+A][vtable-B][magic][var]。进一步检查(nm ./a.out),vtable-X+a包含指向X::func的引用(正如人们所期望的那样)。当您将X强制转换为B时,它会调整指针,以使B函数的VTBL出现在代码所期望的位置。
您真的打算“隐藏”B::func()吗?
B的vtbl看起来像是持有一个指向X的“跳板”,在调用X+A vtbl所持有的“常规”X::func之前,它会将对象指针恢复到完整的X。
080487ea <_ZThn8_N1X4funcEv>:   # in "X-B vtbl"
_ZThn8_N1X4funcEv():
 80487ea:       83 44 24 04 f8          addl   $0xfffffff8,0x4(%esp)
 80487ef:       eb 01                   jmp    80487f2 <_ZN1X4funcEv>
 80487f1:       90                      nop

080487f2 <_ZN1X4funcEv>:        # in X-A vtbl
_ZN1X4funcEv():
 80487f2:       55                      push   %ebp
 80487f3:       89 e5                   mov    %esp,%ebp
 80487f5:       8b 45 08                mov    0x8(%ebp),%eax
 80487f8:       c7 40 14 01 00 00 00    movl   $0x1,0x14(%eax)
 80487ff:       5d                      pop    %ebp
 8048800:       c3                      ret    

2
你真的只是在 extern "C" 主函数吗? - Cole Tobin
2
嗯...不知道。看起来这不是我在其他C++工具中通常做的事情。那只影响名称混淆和外部可见性,对吧? - PypeBros
这里真是太有趣了)) - rostamn739

3
不想太过于学究,但是C#编译器在这个层面上并不涉及。整个类型模型、继承、接口实现等都由CLR处理,更具体地说是由CTS(公共类型系统)处理。.NET编译器主要生成代表意图的IL代码,稍后由CLR执行,其中所有的Vtable处理等都得到处理。
有关CLR如何创建和管理运行时类型的一些详细信息,请参见以下链接。最后解释了MethodTable和Interface Maps。
链接:http://web.archive.org/web/20150515023057/https://msdn.microsoft.com/en-us/magazine/cc163791.aspx

1
这个链接现在已经失效了 - 它只会跳转到 MSDN 杂志的往期索引。要查看原始文章,请下载 2005 年 5 月的 .CHM 文件,解除其阻止,然后转到文章“JIT and Run: Drill Into .NET Framework Internals to See How the CLR Creates Runtime Objects”。(或者,谷歌搜索标题。)我想编辑一个链接,但似乎没有“官方”的链接。 - Daniel McLaury
1
谢谢,我已经用Wayback时间机器将链接替换为原始文章的链接。 - Chris Taylor

2
是的,在托管语言中只有一个虚函数表,CLR不支持多重继承。当你转换为实现的接口时,会进行指针修复。
当试图声明一个从IUnknown之外的另一个接口声明的COM接口时,这是一个值得注意的问题。这是本文作者没有完全理解的问题。COM要求每个接口都有一个单独的虚函数表,正是支持MI的编译器所需要的。

如果只有一个vtable,C#如何支持“菱形”接口继承? - chris
1
@Chris,如果你看一下我在答案中提供的链接,你应该能够基本了解这是如何工作的。这里有一句话:“复制插槽是必要的,以创建一个假象,即每个接口都有自己的迷你vtable。然而,复制的插槽指向相同的物理实现。” - Chris Taylor
2
钻石继承问题的麻烦在于不清楚要使用哪个实现。接口没有实现。只能有一个基类。 - Hans Passant

1

vtables是一种实现细节。没有官方/必需/预期的实现。不同的编译器供应商可以以不同的方式实现继承。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接