C++之虚函数和多重继承

原文出处:http://blog.aaronballman.com/2011/10/virtual-methods-and-multiple-inheritance/#comment-85589,原文的作者是Aaron Ballman,voting member of the C++ standards committee.

本文介绍了在多重继承(MI)以及存在虚函数的场景下,派生类对象的内存布局及虚函数的访问机制,代码很简洁,见下:

class A {
private:
  int i;
public:
  A() : i( 42 ) {}
 
  virtual void Foo() {
  }
};
 
class B {
private:
  double d;
public:
  B() : d( 1.0 ) {}
 
  virtual void Bar() {
  }
};
 
class C : public A, public B {
private:
  char *s;
public:
  C() : s( "Hello World" ), A(), B() {}
 
  void Foo() {
  }
};
在这段代码中有3个类,类A拥有一个int型成员变量和一个虚函数Foo(),类B拥有一个double型成员变量和一个虚函数Bar(),类C继承于A和B,拥有一个指向字符(或字符串)的指针,且重新定义了从A中继承得到的Foo()。A、B、C的对象的内存布局如下:

可以看到C对象将包含A类子对象和B类子对象,其中A类子对象和B类子对象被分别插入了一个指向虚函数表的指针。假设现在我们有一个C类对象 C c; ,那么c中的内容可以用下图表示:


c的首地址是0x00045032,在该地址上有一个void*型的指针,该指针指向虚函数表,即它的内容就是虚函数表的地址,0x0004503A处也是一个指针,该指针也指向一个虚函数表(次虚函数表),即该指针的内容是次函数表的地址(可以看到主虚函数表和次虚函数表并不在一起,有的编译器会把它们放在一起),下面我们可以看看这两张虚函数表的内容:

主虚函数表中记录了C::Foo的地址,次虚函数表中记录了B::Bar的地址,同时这里可以看到,常量字符串也放在不远的位置,s指向了这个位置。趁热打铁,我们可以看看实际调用背后的汇编代码!

c->Foo();  // #1
c->Bar();  // #2
在x86系统上,调用C++函数一般遵循以下三个规则:

1)把this指针的值存在ECX寄存器中;

2)如果有返回值的话,把返回值存在EAX寄存器;

3)函数参数从右向左依次压入栈中;

这样,#1大概可以转换成如下的汇编指令:

mov eax, 1    ; Load the function pointer
mov ecx, c    ; Load the this pointer
call eax      ; Call the function

因为Foo()就在主虚函数表中,所以汇编语言相对简单,#2的汇编指令就要复杂一些了,里面涉及了给this指针调整offset,称为 thunk

mov edx, c        ; Get the instance pointer
add edx, 8        ; Advance by 8 bytes
mov eax, [edx]    ; Load the function pointer
mov ecx, edx      ; Load the this pointer
call eax          ; Call the function

前两条指令就是调整this指针,第4条指令将调整后的this指针放入ecx中。以下是几点结论:

1)类中如果有虚函数,则会有相应的虚函数表,虚函数表中的信息在编译时就会确定;

2)一个类可能有多个虚函数表(虚函数表指针);

3)编译器有时候需要插入thunk来保证this指针指向合适的位置;

4)使用虚函数会付出一些空间和复杂度的代价,使用多重继承下的虚函数付出的代价更多,如果效率很重要,可以考虑不依赖虚函数。



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值