美文网首页从汇编到C++
Cpp7 C++的多态实现 -- 虚表

Cpp7 C++的多态实现 -- 虚表

作者: Asura_Luo | 来源:发表于2018-05-04 02:51 被阅读0次

Cpp7 C++的多态实现 -- 虚表

多态的实现原理


#include "stdafx.h"

#include <stdio.h>
#include <windows.h>


class A
{
public:
    int x;
    virtual void Test()
    {
        printf("A \n");
    }
protected:
private:
};

class B:public A
{
public:
    int x;
    void Test()
    {
        printf("B \n");
    }
protected:
private:
};

void Fun(A* p)
{
    p->Test();
}

int main(int argc, char* argv[])
{
    A a;
    B b;

    Fun(&b);
    return 0;
}

//我们发现在这里 调用的test函数 是b的 因为fun方法传入的对象是b b继承自a 这里体现了多态

//反编译

31:   void Fun(A* p)
32:   {
00401050   push        ebp
00401051   mov         ebp,esp
00401053   sub         esp,40h
00401056   push        ebx
00401057   push        esi
00401058   push        edi
00401059   lea         edi,[ebp-40h]
0040105C   mov         ecx,10h
00401061   mov         eax,0CCCCCCCCh
00401066   rep stos    dword ptr [edi]
33:       p->Test();
00401068   mov         eax,dword ptr [ebp+8]
0040106B   mov         edx,dword ptr [eax]
0040106D   mov         esi,esp
0040106F   mov         ecx,dword ptr [ebp+8]
00401072   call        dword ptr [edx]    //间接调用 + 虚表
00401074   cmp         esi,esp
00401076   call        __chkesp (00401240)
34:   }


@ILT+0(?Fun@@YAXPAVA@@@Z):
00401005   jmp         Fun (00401050)
@ILT+5(??0B@@QAE@XZ):
0040100A   jmp         B::B (00401190)
@ILT+10(??0A@@QAE@XZ):
0040100F   jmp         A::A (00401100)
@ILT+15(?Test@B@@UAEXXZ):
00401014   jmp         B::Test (004011f0)
@ILT+20(?Test@A@@UAEXXZ):
00401019   jmp         A::Test (00401140)
@ILT+25(_main):
0040101E   jmp         main (004010a0)

总结:
1. 当我们在类中定义虚函数时,就会产生虚表
2. 多态的实现 间接调用+虚表

虚表

观察带有虚函数的对象大小

#include "stdafx.h"

#include <stdio.h>
#include <windows.h>


class A
{
public:
    int x;
    void Test()
    {
        printf("A \n");
    }
protected:
private:
};

int main(int argc, char* argv[])
{
    A a;
    printf("%d \n",sizeof(a));
    return 0;
}

//结果是4
#include "stdafx.h"
#include <stdio.h>
#include <windows.h>

class A
{
public:
    int x;
    virtual void Test()
    {
        printf("A \n");
    }
protected:
private:
};

int main(int argc, char* argv[])
{
    A a;
    printf("%d \n",sizeof(a));
    return 0;
}

//结果是 8
#include "stdafx.h"

#include <stdio.h>
#include <windows.h>


class A
{
public:
    int x;
    virtual void Test()
    {
        printf("A \n");
    }
    virtual void Test1()
    {
        printf("A \n");
    }
protected:
private:
};

int main(int argc, char* argv[])
{
    A a;
    printf("%d \n",sizeof(a));
    return 0;
}

//结果还是8

发现:定义了虚函数 对象大小会多出4个字节,多个虚函数也只有多一个4字节

虚表的位置

#include "stdafx.h"

#include <stdio.h>
#include <windows.h>


class A
{
public:
    int x;
    virtual void Test()
    {
        printf("A \n");
    }
};

class B:public A
{
public:
    virtual void Test()
    {
        printf("B \n");
    }
};

void Fun(A* a)
{
    a->Test();
}

int main(int argc, char* argv[])
{
    A a;
    B b;
    Fun(&a);
    Fun(&b);
    return 0;
}

通过vc6的监视器发现a对象的具体结构

image

发现加了虚函数之后对象前面对了一个值 指向 0x00422fac

那么这里的值指向的就是虚表的位置

继续追踪
调出内存窗口查找此内存

image

里面的值是 00401028 (小端存储)

vc6中 ctrl +g 跳转到对应反汇编位置

image

这里指向的正好是A的test 方法
(此时多了个TEST1 是因为上次编译后的结果没有清理缓存 )

26:   void Fun(A* a)
27:   {
00401050   push        ebp
00401051   mov         ebp,esp
00401053   sub         esp,40h
00401056   push        ebx
00401057   push        esi
00401058   push        edi
00401059   lea         edi,[ebp-40h]
0040105C   mov         ecx,10h
00401061   mov         eax,0CCCCCCCCh
00401066   rep stos    dword ptr [edi]
28:       a->Test();
//取参数也就是a对象的指针 到eax
00401068   mov         eax,dword ptr [ebp+8] 
//读取eax也就是a对象的首地址 也就是虚表的位置 
0040106B   mov         edx,dword ptr [eax]  
0040106D   mov         esi,esp
//传递this指针,到ecx
0040106F   mov         ecx,dword ptr [ebp+8]
//调用虚表中记录的函数位置 这里是第一个就直接是edx
00401072   call        dword ptr [edx]
00401074   cmp         esi,esp
00401076   call        __chkesp (00401240)
29:   }

//虚表


00401023   jmp         A::A (004010d0)
00401028   jmp         A::Test (00401090)
0040102D   jmp         A::Test1 (00401110)
00401032   jmp         B::B (00401170)
00401037   jmp         Fun (00401050)
0040103C   jmp         B::Test (004011c0)
00401041   jmp         A::A (00401200)

虚表的结构

据观察,虚表中存储的都是函数地址,每个地址占用4个字节,有几个虚函数,则就有几个地址

虚表的内容

子类没有重写时的值

#include "stdafx.h"
#include <stdio.h>
#include <windows.h>

class A
{
public:
    int x;
    virtual void Test()
    {
        printf("A \n");
    }
};

class B:public A
{
public:
};

void Fun(A* a)
{
    a->Test();
}

int main(int argc, char* argv[])
{
    B b;
    Fun(&b);
    return 0;
}

//虚表
00401014   jmp         A::Test (00401140)

子类重写时的值

#include "stdafx.h"
#include <stdio.h>
#include <windows.h>

class A
{
public:
    int x;
    virtual void Test()
    {
        printf("A \n");
    }
};

class B:public A
{
public:
    virtual void Test()
    {
        printf("B \n");
    }
};

void Fun(A* a)
{
    a->Test();
}

int main(int argc, char* argv[])
{
    B b;
    Fun(&b);
    return 0;
}

//虚表

@ILT+15(?Test@B@@UAEXXZ):
00401014   jmp         B::Test (00401150)
@ILT+20(?Test@A@@UAEXXZ):
00401019   jmp         A::Test (004011e0)

析构函数问题

class A
{
private:
    int* a;
public:
    A()
    {
        a = new int[10];
    }
    ~A()
    {
        delete a;
    }
    int* get_a()
    {
        return a;
    }
}

class B:public A
{
private:
    int* b;
public:
    B()
    {
        b = new int[5];
    }
    ~B()
    {
        delete b;
    }
    int* get_arr(int flag)
    {
        if(flag == 1 )
        {
            return b;
        }
        else
        {
            return get_a();
        }
    }
}


int main()
{
    A* p = new B;
    delete p;
    return 0;
}

上述代码执行时,如果直接调用 指针类型是父类,那么只会执行父类的析构函数释放掉 int* a
而b类中的int* b却不会被释放掉

理论上最好的方式是 逐步往上调用所有的析构函数,这样才可以释放所有使用的内存

// _20180212.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"

#include <stdio.h>
#include <windows.h>

class A
{
private:
    int* a;
public:
    A()
    {
        a = new int[10];
    }
    virtual ~A()
    {
        delete a;
        printf("析构 A \n");
    }
    int* get_a()
    {
        return a;
    }
};

class B:public A
{
private:
    int* b;
public:
    B()
    {
        b = new int[5];
    }
    ~B()
    {
        delete b;
        printf("析构 B \n");
    }
    int* get_arr(int flag)
    {
        if(flag == 1 )
        {
            return b;
        }
        else
        {
            return get_a();
        }
    }
};


int main(int argc,char* argv[])
{
    A* p = new B;
    
    delete p;
    return 0;
}

这里 将父类的析构函数定义为虚函数,那么delete的时候 就会调用子类重写父类虚析构函数的析构函数(虽然名字不相同,但是会自动重写_编译器约定)

并且此时析构函数现实从下往上逐步执行

image

相关文章

  • Cpp7 C++的多态实现 -- 虚表

    Cpp7 C++的多态实现 -- 虚表 多态的实现原理 总结:1. 当我们在类中定义虚函数时,就会产生虚表2. 多...

  • Swift5.1学习随笔之多态

    多态的实现原理: OC:Runtime C++:虚表(虚函数表) Swift:纯Swift没有Runtime,更加...

  • pwnable.kr之uaf && c++虚函数

    c++的逆向还是要熟悉下。 一、关于c++虚函数 虚函数使得c++能够实现多态,每个类有一个虚表,每个对象在实现的...

  • 理解 C++ 虚函数表

    引言 虚表是 C++ 中一个十分重要的概念,面向对象编程的多态性在 C++ 中的实现全靠虚表来实现。在聊虚表之前我...

  • Swift 多态实现探究

    多态 父类指针指向子类对象 Swift 中多态的实现类似于 c++ 中的虚表 OC 多态实现利用的是 Runtim...

  • 深刻剖析之c++博客文章

    三大特性 封装、继承、多态 多态 C++ 虚函数表解析C++多态的实现原理 介绍了类的多态(虚函数和动态/迟绑定)...

  • GeekBand-C++面向对象高级编程(下)-Week2

    对象模型:虚函数表(vtbl)与虚表指针(vptr) 我们知道,C++中,可以通过虚函数来实现多态性,而虚函数是通...

  • C++ 虚函数表剖析

    一、概述 为了实现C++的多态,C++使用了一种动态绑定的技术。这个技术的核心是虚函数表(下文简称虚表)。本文介绍...

  • C++ 虚函数表剖析

    一、概述 为了实现C++的多态,C++使用了一种动态绑定的技术。这个技术的核心是虚函数表(下文简称虚表)。本文介绍...

  • 语法

    virtual 1.virtual声明的函数实现多态就是通用的多态实现 2.纯虚函数C++的纯虚函数用于表示一个类...

网友评论

    本文标题:Cpp7 C++的多态实现 -- 虚表

    本文链接:https://www.haomeiwen.com/subject/qzyrrftx.html