从汇编码可以看出,a,b虚函数在子类vtable和父类table中的位置是一样的(从它们相对于自己所在vtable的偏移量可以看出)。这保证了不论对象实际的类型是什么,编译器总能使用同样的偏移量来调用虚函数。假如不这么做,也是说虚函数a,b在子类Y的vtable中的位置和在父类X的vtable中的位置不一样,由于向上转型,编译器只针对父类工作,也是对虚函数a,b的调用只会根据父类X的vtable来确定偏移量,那么在实际运行的时候会出错,实际的子对象根本调用不到正确的函数,多态失效。

  在上面的例子中,如果将yp转为实际的类型调用c,我们会看到编译器形成的偏移量为8byte,汇编代码如下:

; 32   :     yp->c();

    mov    ecx, DWORD PTR _yp$[ebp];将yp所指向的堆对象的首地址给ecx
    mov    edx, DWORD PTR [ecx];将堆对象首地址的内容给edx,即将子类vptr指向的vtable首地址给edx
    mov    ecx, DWORD PTR _yp$[ebp];将yp所指向的堆对象首地址给ecx,作为隐含参数传递给虚成员函数c
    mov    eax, DWORD PTR [edx+8];将偏移子类vtable首地址8byte处内存的内容给eax,即将虚函数c的地址给eax(这里,虚函数b的地址同样位于偏移子类Y的vtable首地址8byte处)
    call    eax;调用虚成员函数c

  对象切片

  如果进行向上转型的时候不是用传地址或者引用,而是用传值,那么会发生对象切片,即派生类对象中原有的部分被切除,只保留了基类的部分。

  下面是c++源码:

class X {
private:
    int i;
public:
    virtual void a() {
        i = 1;
    }
    virtual void b() {
        i = 2;
    }
};

class Y : public X {
private:
    int i;
public:
    virtual void c() {//新定义的虚函数
        i = 3;
    }
    void b() {//重写父类中的虚函数
        i = 4;
    }
};
void f(X x) {//用传值的形式进行向上转换
    x.b();
}

int main() {
    Y y;
    f(y);
}

  下面是main函数的汇编码:

; 28   : int main() {

    push    ebp
    mov    ebp, esp
    sub    esp, 16                    ; 为对象y预留16byte的空间

; 29   :     Y y;

    lea    ecx, DWORD PTR _y$[ebp];将y的首地址给ecx,转为隐含参数传递给y的构造函数
    call   ??0Y@@QAE@XZ;调用y的构造函数

; 30   :     f(y);

    sub    esp, 8;//由于对象传值,要进行拷贝,产生临时对象,这里为临时对象预留8byte的空间(类X的大小)
    mov    ecx, esp;//将临时对象的首地址给ecx,作为隐含参数传递给拷贝函数
    lea    eax, DWORD PTR _y$[ebp];将对象y的首地址给eax,作为参数给拷贝函数
    push    eax;压栈,传递参数
    call    0X@@QAE@ABV0@@Z;调用类X的拷贝函数
    call    f@@YAXVX@@@Z                ; 调用函数f
    add    esp, 8;释放刚才的临时对象占用的8byte空间
; 31   : }

    xor    eax, eax
    mov    esp, ebp
    pop    ebp
    ret    0

  从汇编吗中可以看出,临时对象的大小为父类X的大小,调用的拷贝函数也是父类X的拷贝函数。

  下面是父类X的拷贝函数汇编码:

??0X@@QAE@ABV0@@Z PROC                    ; X::X, COMDAT
; _this$ = ecx
    push    ebp
    mov    ebp, esp
    push    ecx;压栈,为存对象首地址预留4byte空间
    mov    DWORD PTR _this$[ebp], ecx;ecx中保存临时对象首地址,放到刚才预留的空间
    mov    eax, DWORD PTR _this$[ebp];将临时对象首地址给ecx
    mov    DWORD PTR [eax], OFFSET ??_7X@@6B@;将类X的vtable首地址存到临时对象首地址所指向的内存 即初始化临时对象的vptr指针
    mov    ecx, DWORD PTR _this$[ebp];将临时对象的首地址给ecx
    mov    edx, DWORD PTR ___that$[ebp];将y的首地址给edx
    mov    eax, DWORD PTR [edx+4];将偏移y首地址4byte处内存内容给edx,即将y包含的父对象中的成员变量i的值给edx
    mov    DWORD PTR [ecx+4], eax;将eax的值给偏移临时对象首地址4byte处内存,即将eax的值给临时对象的成员变量i
    mov    eax, DWORD PTR _this$[ebp];将临时对象的首地址给eax,作为返回值。构造函数总是返回对象首地址
    mov    esp, ebp
    pop    ebp
    ret    4

  从拷贝函数可以看出,临时对象只拷贝了y的所包含的的父对象部分(y被切片了),并且临时对象的vptr指针也初始化为类X的vtable首地址。

  下面是函数f的汇编码:

; 24   : void f(X x) {

    push    ebp
    mov    ebp, esp

; 25   :     x.b();

    lea    ecx, DWORD PTR _x$[ebp];将参数x的首地址给ecx,作为隐含参数传递给成员函数b
    call    ?b@X@@UAEXXZ                ; 调用x中的成员函数b 这里是用对象直接调用,因此没有访问vtable

  这里调用的是类X里面的成员函数,并且没有访问虚表vtable

  下面是类X里面的虚成员函数b的汇编码:

?b@X@@UAEXXZ PROC                    ; X::b, COMDAT
; _this$ = ecx

; 8    :     virtual void b() {

    push    ebp
    mov    ebp, esp
    push    ecx;为保存对象首地址预留4byte空间
    mov    DWORD PTR _this$[ebp], ecx;ecx中保存有对象x的首地址,放到刚才预留的空间

; 9    :         i = 2;

    mov    eax, DWORD PTR _this$[ebp];将x首地址给eax
    mov    DWORD PTR [eax+4], 2;将2写给偏移x首地址4byte处,即将2赋给x的成员变量i

; 10   :     }

    mov    esp, ebp
    pop    ebp
    ret    0
?b@X@@UAEXXZ ENDP