zoukankan      html  css  js  c++  java
  • C++多态实现(转载)

         主要是清华一本教程的抄录和转载

          

    这一部分,我们简要地介绍一下在C++中多态是怎样实现的。

    早期联编或静态联编,因为指针要调用那一个函数是在编译时就确定的。

    多态也称为动态联编或迟后联编,因为到底调用哪一个函数,在编译时不能确定,而要推迟到运行时确定。也就是说,要等到程序运行时,确定了指针所指向的对象的类型时,才能够确定
      多态的基本思想是:在编译时,C++编译器不知道调用哪一个函数,而要到运行时确定。这意味着应把函数的入口地址保存在某一个地方,以便于在调用前查询,而存储函数入口地址的地方也应能被相关的对象访问。例如,一个Vehicle * unicycle指针指向car对象,然后,unicycle->message()调用car的成员函数,这个函数的入口地址由unicycle指向的对象决定。
      在C++中,一般实现方法如下:包含虚函数的对象,增加了一个隐含的数据成员,且是它的第一个数据成员,该数据成员指向一个指针数组,而指针数组存储对象的虚函数地址,需要说明的是这个实现与具体的编译器有关。
      某一个类的虚函数地址表被该类的所有对象共享,甚至有可能两个类共享同一个虚函数地址表。内存开销包括:
      ◇ 每一个对象增加了一个额外的数据成员。
      ◇ 每一个类有一个指针表,用于存储该类各虚函数的地址。
      所以,unicycle->message()的调用过程是:首先检查unicycle指向的对象的隐含的数据成员,在我们前面所举的例子中,该数据成员指向的指针表只有一个元素,即message函数的入口地址,被调用的函数根据指针表确定。
      有虚函数的的对象的内部组织,我们可以用11-4的示意图来说明:
      正象我们在图11-4中看到的,有虚函数的所有对象均有一个隐含的指针数据成员,且指向存放虚函数入口地址的指针表。类Vehicle对象与truck对象共用一个表,而carboat有自己的message函数,所以,它们需要自己的虚函数指针表。

    virtual将一个成员函数说明为虚函数,对于编译器来讲,它的作用是告诉编译器,这个类含有虚函数,对于这个函数不使用静态联编,而是使用动态联编机制。编译器就会按照动态联编的方案进行一系列的工作。
      对于每个包含虚函数的类,编译器都为其创建一个表(称之为VTABLE表)。在VTABLE表中放置的是每个类自己的虚函数地址,在每个包含虚函数的类中放置了一个指针(VPTR),指向VTABLE表。通过基类指针调用虚函数时,编译器会在函数调用的地方插入一段特定的代码。这段代码的作用就是得到VPTR,找到VTABLE,并在VTABLE表中找到相应的虚函数地址,然后进行调用。

    #include "iostream.h"
    //
    没有虚函数的类
    class A
    {
    public:
     int a;
     A():a(0){}
    };
    //
    有一个虚函数的类
    class B
    {
    public:
     int a;
     B():a(0){}
     virtual void func(){}
    };
    //
    有两个虚函数的类
    class C
    {
    public:
     int a;
     C():a(0){}
     virtual void func(){}
     virtual void func2(){}
    };

    void main()
    {
     cout<<"没有虚函数的类大小是"<<sizeof(A)<<endl;
     cout<<"有一个虚函数的类大小是"<<sizeof(B)<<endl;
     cout<<"有两个虚函数的类大小是"<<sizeof(C)<<endl;
     A a;
     B b;
     C c;
     int* tmp;
     tmp=(int*)&b;
     //打印对象b中两个整型单元中的值
     cout<<(*tmp)<<endl;
     cout<<(*(tmp+1))<<endl;
     //改变b中成员a的值
     b.a=1;
     //再次打印对象b两个整型单元中的值
     cout<<(*tmp)<<endl;
     cout<<(*(tmp+1))<<endl;
    }

    程序运行结果为:
      没有虚函数的类大小是4
      有一个虚函数的类大小是8
      有两个虚函数的类大小是8
      4358196
      0
      4358196
      1

    main函数分为两部分,从第一部分可以很清楚的看到没有虚函数的类的长度是4,就是所有成员变量的长度,带一个或多个虚函数的类的长度是8,是所有成员变量加上一个VPTR的长度,因为这只是指向VTABLE表的指针,所以带几个虚函数对这个指针是不起作用的,它们只影响VTABLE表的长度。
      从第二部分可以看到,B类对象b的大小是8个字节,即两个整型的大小,从我们改变它的成员变量a的值前后两个整型单元的数值,我们可以看到,第一个整型单元的值没有改变,第二个整型单元的值从0变成1,这正是成员变量a的位置。第一个整型单元是什么呢?这就是VPTR所在的位置。第二部分告诉我们VPTR的位置是在这个类的开始,并且永远处在类的开始位置。将来定义了这个类的对象以后,该对象指针的位置就是这个VPTR的位置。
      这里可能有个疑问,如果一个类里面没有成员变量,那怎么办?它的对象的长度岂不是要变成0,一个0长度的数据的地址是什么?即使分配给它一个地址,当给下一个对象分配空间时,岂不是要与这个对象的地址相同。为了避免这个问题,编译器强制这个类的对象的长度非0,如果这个类没有任何成员变量,也没有虚函数,即理论上它的对象的长度是0,但是,编译器会向这个对象中插入一个""成员。当类中仅有虚函数时,该对象中仅有一个VPTR。如果将上面程序的所有成员变量全注释掉,再重新编译运行程序,我们就会看到这种情况。
      程序运行结果是:
      没有虚函数的类大小是1
      有一个虚函数的类大小是4
      有两个虚函数的类大小是4
      我们已经清楚VPTR,下面我们来讨论VTABLE的构造。
      每个包含虚函数的类都有自己的VTABLE表,VTABLE表中存放各自类的虚函数地址,对于没有重定义的虚函数使用基类函数的地址。不管怎样,在每个类中总要有全体虚函数的地址,并且虚函数地址在各自表中的顺序必须一致,因为在函数查找时,只是根据VPTR加上一个偏移量来确定某个虚函数,如果VTABLE表的虚函数不全,或者顺序不对,就不可能查到正确的函数地址。

    但是如果派生类中有自己的虚函数,那么VTABLE将是什么结构呢?我们举个例子来说明:

    class A
    {
    public:
     int a;
     A():a(0){}
     virtual void func1(){}
     virtual void func2(){}
    };

    class B public A
    {
    public:
     int a;
     B():a(0){}
     virtual void func1(){}
     virtual void func2(){}
     virtual void func3(){}
    };

    class C public B
    {
    public:
     int a;
     C():a(0){}
     virtual void func1(){}
     virtual void func3(){}
    };

    这三个类的VTABLE表是下面的情况:

    A

    B

    C

    A::func1

    B::func1

    C::func1

    A::func2

    B::func2

    B::func2

    .

    B::func3

    C::func3

    从这个表中可以看到,派生类中和基类都有的虚函数,在各自的VTABLE表中位置必须一样,如果派生类中没有重定义该虚函数,则它就使用基类的虚函数地址。派生类自己定义虚函数依次添加在VTABLE表中。
      当编译器对类的定义进行编译时,如果类中含有虚函数,它就采用动态联编机制,生成这样一个VTABLE表,记录这个类的虚函数的地址。编译器在类的构造函数开头部分秘密添加了一段代码(如果用户没有显式定义构造函数,编译器会生成缺省构造函数),这段代码对用户是不可见的,它的作用是:初始化VPTR,使它指向该类的VTABLE。这样,当生成该类对象时,所做的第一件事就是将VPTR赋值,使它指向对应的VTABLE表。
      现在有了VTALBE表,生成了该类对象,并已经设置了VPTR,使它指向VTABLE,那么当使用基类指针调用虚函数时,到底是怎么实现的呢?
      对于上面的基类B和派生类C,我们看下面的程序段:
      B* pb=new C();
      pb->func3();
      pbB型指针,但指向的是C类对象,并调用虚函数func3()。我们看一下这个函数调用在VC6.0编译器中编译出的代码(汇编语言的代码跟具体编译器有关,""后面部分是汇编语言的注释):
      mov edx, DWORD PTR _pb$[ebp]
      ;将this指针取出到edx
      mov eax, DWORD PTR [edx]
      ;将VPTR取出到eax
      mov esi, esp
      ;与本文无关,不用管它
      mov ecx, DWORD PTR _pb$[ebp]
      ;将this指针作为参数传给函数
      call DWORD PTR [eax+8]
      ;到VTABLE中查找,并进行调用
      在这段代码中,寄存器edx存放的是对象的首地址,它对应该对象的this指针,因为每个成员函数调用都有个隐含参数,就是该对象的this指针。第一句是将this指针取出。因为VPTR保存在对象的首部,正是this指针指向的地址,所以第二句是将指向寄存器edx指向的位置的值,即this指针指向的双字,即VPTR取出,存在寄存器eax中。第三句与我们要讨论的没有关系,它是将堆栈指针保存起来,等该函数调用完返回后,进行检查,这是VC编译器的一种保护措施,我们不用管它。第四句就是将this指针作为参数传给这个函数。在汇编语言中,给一个函数传递参数可以有两种方法:通过堆栈或寄存器,一般比较小的函数调用或者经过优化后的程序会使用寄存器传参。第五句是去VTABLE表中查找虚函数位置,因为一个指针是四个字节,func3在表中是第三个,所以地址是eax8,然后进行调用。这样,编译器就实现了动态联编。

    C++中,构造函数不能定义为虚函数,而析构函数可以定义为虚函数。
      派生类的析构函数能够自动调用基类的析构函数,但用抽象类指针处理对象时,析构函数如何调用会有问题。例如11-23,假定抽象类Shape表示所有能画出来的不同的对象,而PictureShape派生类,并有指向其它Shape派生类对象的指针作为它的数据成员:

  • 相关阅读:
    利用cookie改变背景色
    AsyncResult
    元组Tuple
    子查询和高效分页
    事务
    健康亮黄灯 疾病有信号
    每天学点舒压减压秘诀
    药房里买得到的传世名方:新版
    电子设备热循环和振动故障预防
    LED照明应用基础与实践
  • 原文地址:https://www.cnblogs.com/dragonsuc/p/2000208.html
Copyright © 2011-2022 走看看