文章详情

  • 游戏榜单
  • 软件榜单
关闭导航
热搜榜
热门下载
热门标签
php爱好者> php文档>C++多态实现(转载)

C++多态实现(转载)

时间:2011-03-30  来源:长风破浪

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

      

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

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

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

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

 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将是什么结构呢?我们举个例子来说明:

 B():a(0){}
 virtual void func1(){}
 virtual void func2(){}
 virtual void func3(){}
};

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

  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在表中是第三个,所以地址是eax+8,然后进行调用。这样,编译器就实现了动态联编。

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

 

排行榜 更多 +
辰域智控app

辰域智控app

系统工具 下载
网医联盟app

网医联盟app

运动健身 下载
汇丰汇选App

汇丰汇选App

金融理财 下载