从汇编看c++中的多态详解

2020-01-06 15:23:17王振洲

抽象基类

c++源码如下:


class X {
private:
  int i;
public:
  virtual void f() = 0;//纯虚函数
  X() {
    i = 1;
  }
};

class Y : public X {//Y继承自X
private:
  int j;
public:
  virtual void f() {
    j = 2;
  }
};




int main() {
 Y y;
}

只看父类X的构造函数和子类Y的构造函数的汇编码:

子类Y构造函数的汇编码:


??0Y@@QAE@XZ PROC          ; Y::Y, COMDAT
; _this$ = ecx
  push  ebp
  mov  ebp, esp
  push  ecx;为保存子对象首地址预留空间 
  mov  DWORD PTR _this$[ebp], ecx;将ecx(里面存放子对象首地址)的值放到刚才的空间
  mov  ecx, DWORD PTR _this$[ebp];将子对象首地址传给ecx,作为隐含参数(this指针)调用父对象的构造函数
  call  ??0X@@QAE@XZ        ; 调用父对象的构造函数
  mov  eax, DWORD PTR _this$[ebp];将子对象首地址给eax t
  mov  DWORD PTR [eax], OFFSET ??_7Y@@6B@;将子对象的vtable首地址存到子对象首地址所指向的内存,即初始化子对象的vptr
  mov  eax, DWORD PTR _this$[ebp];将子对象的首地址给eax,作为返回值。构造函数总是返回对象首地址
  mov  esp, ebp
  pop  ebp
  ret  0
??0Y@@QAE@XZ ENDP

父类X构造函数汇编码:


??0X@@QAE@XZ PROC          ; X::X, COMDAT
; _this$ = ecx

; 6  :   X() {

  push  ebp
  mov  ebp, esp
  push  ecx;压栈的目的就是为存储父对象首地址(即this指针)预留空间
  mov  DWORD PTR _this$[ebp], ecx;将父对象首地址存到刚才的空间
  mov  eax, DWORD PTR _this$[ebp];将父对象的首地址传给eax
  mov  DWORD PTR [eax], OFFSET ??_7X@@6B@;将父对象的vtable(由于父类为抽象类,其vtable不完全,即里面没有存放纯虚函数的地址,只为其保留了一个位置)首地址存到父对象首地址所指的内存 即初始化父对象的vptr指针

; 7  :    i = 1;

  mov  ecx, DWORD PTR _this$[ebp];将父对象的首地址给ecx
  mov  DWORD PTR [ecx+4], 1;将1存到偏移父对象首地址4byte处,即给父对象的成员变量i赋值

; 8  :   }

  mov  eax, DWORD PTR _this$[ebp];父对象的首地址给eax 作为返回值。构造函数总是返回对象首地址
  mov  esp, ebp
  pop  ebp
  ret  0
??0X@@QAE@XZ ENDP

从汇编码可以看出,在构造子类的过程中,依然调用了父类的构造函数,尽管父类是一个抽象类。但这只是为了初始化子对象中包含父对象的部分,如果直接想从父类实例化一个对象,编译器报错,这是因为父类的vtable不完全,编译器不能安全的创建一个抽象类对象。而在构造子对象的构成当中,虽然在构造子对象中所包含的的父对象部分,vptr暂时指向了父类的vtable,但是,当子对象构造完成时,vptr最终指向了子类的vtable。子类的vtable是一个完整的,因此编译器允许。

多态的晚捆绑机制只有在用地址或者引用调用虚函数的时候才有效,如果用对象本身直接调用虚函数,则不会出现晚捆绑,而是直接调用。