zoukankan      html  css  js  c++  java
  • 多态的内幕(C++, C)语言两个版本

        本文通过分析C++编译器生成的汇编代码,分析多态的机制。并实现了一个C语言版本。

        在编译性语言里面,多态真的是一个伟大的发明。它可以现在写好代码,编译好,并且可以调用未来的代码。这多少有了点动态的感觉。

    很多人,也在脚本语言里面抱怨,为什么不提供多态的功能啊。脚本语言里面,一个函数参数,可以传递任何类型,甚至可以通过函数名的字符串调用函数,

    这样多态的作用就小了很多。对于面向对象来说,最重要的两个概念莫过于 继承 和 多态。继承可以减少代码重复,多态可以减少大量的条件判断,if else switch

    如果在代码中太多,你的程序应该不怎么面向对象。

        废话不说了,先给一个用于分析的程序:

    代码
    #include <iostream>
    using namespace std;

    class Base
    {
    public:
        
    virtual void vfun1() {cout << " Base::vfun1()" <<endl;}
        
    virtual void vfun2() {cout << " Base::vfun2()" <<endl;}
        
    virtual void vfun3() {cout << " Base::vfun3()" <<endl;}
    };

    class Concrete:public Base
    {
    public:
        
    void vfun1() {cout << " Concrete::vfun1()" <<endl;}
        
    void vfun2() {cout << " Concrete::vfun2()" <<endl;}
    };

    void override_demo2(Base &obj)
    {
        obj.vfun1();
        obj.vfun2();
        obj.vfun3();
    }

    typedef 
    long point_t;  //32 位系统 和 64 位系统上 都表示标准指针的长度,但是可能不兼容16位系统,在编译的时候修改一下
    typedef void (*func)();

    inline 
    void *  getvfptr(void *p, int offset)
    {
        point_t 
    *= (point_t *)*(point_t *)p;
        
    // cout << q[0] << endl
        
    //      << q[1] << endl
        
    //      << q[2] << endl;
        return (void *)(q[offset]);
    }

    void override_demo(Base &obj)
    {
        func f;
        f 
    = (func)getvfptr(&obj, 0);
        f();
        f 
    = (func)getvfptr(&obj, 1);
        f();
        f 
    = (func)getvfptr(&obj, 2);
        f();
    }

    int main()
    {
        Base base_obj;
        Concrete concrete_obj;
        cout 
    << "override_demo:" << endl;
        override_demo(base_obj);
        override_demo(concrete_obj);
        cout 
    << "override_demo2:" << endl;
        override_demo2(base_obj);
        override_demo2(concrete_obj);
    }

    这基本上是一个最简单的多态的演示了。我们先来看看 override_demo 这个函数。这个函数没有使用系统使用的多态功能,但是也实现了多态。

    通过仔细分析可以发现,这个代码的原理是取出 Base 类的地址,如果,Base 定义了 虚函数,那么会在Base的头部自动插入一个指针,指向虚表数组。

    函数调用是通过函数的地址,编译器会在Base类里面插入这个虚表,里面填上按照顺序填上虚函数的地址,在子类中,会复制一份Base的虚表数组,

    如果函数被重新定义,那么替换这个虚表中的函数地址,否则就用Base 类里面的地址,在调用虚函数的地方,把obj.vfunc1() 改成 调用虚表中的第一个函数。

    这样,即时子类指针转换成了父类指针,但是子类地址指针指向的虚表还是子类的,所以,会调用子类虚表中的第一个函数。

    上面的解释太抽象,可以看看汇编的代码:

    这是 override_demo2 的汇编代码,很能说明问题:

    void override_demo2(Base &obj)
    {
    00411950  push        ebp 
    00411951  mov         ebp,esp
    00411953  sub         esp,0C0h
    00411959  push        ebx 
    0041195A  push        esi 
    0041195B  push        edi 
    0041195C  lea         edi,[ebp-0C0h]
    00411962  mov         ecx,30h
    00411967  mov         eax,0CCCCCCCCh
    0041196C  rep stos    dword ptr es:[edi]
        obj.fun1();
    0041196E  mov         eax,dword ptr [obj] //取出obj的地址,就是getvfptr 中p的值
    00411971  mov         edx,dword ptr [eax] //取出obj第一个元素的值,也就是 getvfptr 中的 *(ponit_t *)p , 取出指针所指向的地址
    00411973  mov         esi,esp
    00411975  mov         ecx,dword ptr [obj]
    00411978  mov         eax,dword ptr [edx] //取出obj第一个元素的指针,指向的第一个元素,也就是 getvfptr 中的 q[0]
    0041197A  call        eax   //调用函数
    0041197C  cmp         esi,esp
    0041197E  call        @ILT+470(__RTC_CheckEsp) (4111DBh)
        obj.fun2();
    00411983  mov         eax,dword ptr [obj]
    00411986  mov         edx,dword ptr [eax]
    00411988  mov         esi,esp
    0041198A  mov         ecx,dword ptr [obj]
    0041198D  mov         eax,dword ptr [edx+4] //第二个函数
    00411990  call        eax 
    00411992  cmp         esi,esp
    00411994  call        @ILT+470(__RTC_CheckEsp) (4111DBh)
        obj.fun3();
    00411999  mov         eax,dword ptr [obj]
    0041199C  mov         edx,dword ptr [eax]
    0041199E  mov         esi,esp
    004119A0  mov         ecx,dword ptr [obj]
    004119A3  mov         eax,dword ptr [edx+8] //第三个函数
    004119A6  call        eax 
    004119A8  cmp         esi,esp
    004119AA  call        @ILT+470(__RTC_CheckEsp) (4111DBh)
    }

    这样看来,调用虚函数的代码并不是很高,但是可以发现,虚函数是不可能内联的,因为,调用它必须通过地址。而且,在之类中必须声明为 virtual

    否则,这个函数不会放入虚表中,也就不能产生多态了。

    依照这个思路,你可以改造成一个C语言的多态的方法。比如你定义一个基结构,它是一个函数指针列表,然后,定义几个子结构,子结构是和基结构一样排序

    的函数指针列表。下面是一个例子:

    代码
    #include <stdio.h>
    #include 
    <stdlib.h>

    typedef 
    void func();
    struct Base
    {
        func 
    *vfun1;
        func 
    *vfun2;
        func 
    *vfun3;
    };

    struct Child
    {
        func 
    *vfun1;
        func 
    *vfun2;
        func 
    *vfun3;
        
    char *hello;
    };


    void base_vfunc1()
    {
        printf(
    " base_vfunc1\n");
    }

    void base_vfunc2()
    {
        printf(
    " base_vfunc2\n");
    }

    void base_vfunc3()
    {
        printf(
    " base_vfunc3\n");
    }

    struct Base* init_base()
    {
        
    static struct Base base_vtable;
        base_vtable.vfun1 
    = base_vfunc1;
        base_vtable.vfun2 
    = base_vfunc2;
        base_vtable.vfun3 
    = base_vfunc3;
        
    return &base_vtable;
    }

    void child_vfunc3()
    {
        printf(
    " child_vfunc3\n");
    }
    struct Child * init_child()
    {
        
    struct Child *child;
        
    struct Base  *base_vtable;
        child 
    = malloc(sizeof(struct Child));
        base_vtable 
    = init_base();
        child
    ->vfun1 = base_vtable->vfun1;
        child
    ->vfun2 = base_vtable->vfun2;
        child
    ->vfun3 = child_vfunc3;
        child
    ->hello = "hello world";
        
    return child;
    }

    free_child(
    struct Child *ch)
    {
        
    if (ch) free(ch);
        ch 
    = NULL;
    }

    void override_demo(void *p)
    {
        
    struct Base *base;
        
    base = (struct Base *)p;
        
    base->vfun1();
        
    base->vfun2();
        
    base->vfun3();
    }

    int main()
    {
        
    struct Child *ch  = init_child();
        
    struct Base *base = init_base();
        printf(
    "base\n");
        override_demo(
    base);
        printf(
    "child\n");
        override_demo(ch);
        printf(ch
    ->hello);
        free_child(ch);
    }

    这样,你写一个函数,可以调用不同的代码了。

    当然,可能没有面向对象这样直观了。

  • 相关阅读:
    每天学一点MATLAB函数——文件编程函数
    每天学一点MATLAB函数——软件操作函数(1)
    C# 杂记
    ActiveX控件注册与反注册
    First Java Graphic Program
    判断式
    两个仿函数示例
    STL文件的读取与显示
    SQLite数据库(一)
    机器学习--如何理解Accuracy, Precision, Recall, F1 score
  • 原文地址:https://www.cnblogs.com/niniwzw/p/1940159.html
Copyright © 2011-2022 走看看