C++中与多态有关的几个概念

  多态(Polymorphism)面向对象程序设计(OOD)的一个重要特征之一(其它还包括继承和封装)。

  多态字面意思就是“多种形态”。多态性是指允许将父对象设置成为它的一个或更多的子对象相等技术。简单地说,就是允许把子类类型的指针赋值给父类类型的指针,赋值之后,父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作(调用对应子类型的成员函数)。也就是说,父亲的行为像儿子,而不是儿子的行为像父亲。把不同的子类对象都当作父类来看,可以屏蔽不同子类对象之间的差异,写出通用的代码,做出通用的编程,以适应需求的不断变化。

  虚函数是在类中被声明为virtual的成员函数,当编译器看到通过指针或引用调用此类函数时,对其执行晚绑定。它是C++中用于实现多态(polymorphism)的机制。核心理念就是通过基类访问派生类定义的函数。一个类函数的调用并不是在编译时刻被确定的,而是在运行时刻被确定的。由于编写代码的时候并不能确定被调用的是基类的函数还是哪个派生类的函数,所以被称为“虚”函数。虚函数只能借助于指针或者引用来达到多态的效果。

  编译时绑定(compile-time/early binding):当调用函数时,在编译阶段编译器就能够决定应该执行哪段代码。

  运行时绑定(run-time/late binding):当调用虚函数时,编译器不能确定应该执行的代码,在运行阶段,才将函数的调用与对应的函数体进行连接。

  虚函数实现的机制

  编译器对每个包含虚函数的类创建一个表(称为VTABLE)。在VTABLE中,编译器放置特定类的虚函数地址。在每个带有虚函数的类中,编译器秘密地设置一个指向函数地址数组的指针,称为vpointer(缩写为VPTR),指向这个对象的VTABLE。通过基类指针做虚函数调用时(也就是做多态调用时),编译器静态地插入取得这个VPTR,并在VTABLE表中查找函数地址的代码,这样就能调用正确的函数使晚捆绑发生。为每个类设置VTABLE、初始化VPTR、为虚函数调用插入代码,所有这些都是自动发生的。利用虚函数,这个对象的合适的函数就能被调用,哪怕在编译器还不知道这个对象的特定类型的情况下。

  因为在调用此类的构造函数时,在类的构造函数中,编译器会隐含执行vptr与vtable的关联代码,将vptr指向对应的vtable。这就将类与此类的vtable联系了起来。在调用类的构造函数时,指向基础类的指针此时已经变成指向具体的类的this指针,这样依靠此this指针即可得到正确的vtable,从而实现了多态性。在此时才能真正与函数体进行连接。

  虚拟函数表VTABLE包含此类及其父类的所有虚拟函数的地址。如果它没有重载父类的虚拟函数,vtable中对应表项指向其父类的此函数。反之,指向重载后的此函数。虚拟函数被继承后仍旧是虚拟函数,虚拟函数非常严格地按出现的顺序在vtable中排序,所以确定的虚拟函数对应vtable中一个固定的位置n,n是一个在编译时就确定的常量。所以,使用vptr加上对应的n,就可得到对应函数的入口地址。

  在C++的标准规格说明书中说到,编译器必需要保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证正确取到虚函数的偏移量)。VPTR总指向VTABLE的开始地址,所有基类和它的子类的虚函数地址(子类自己定义的虚函数除外)在VTABLE中存储的位置总是相同的。

  实例

 1 class Base{
2
3 public:
4
5 void bfun(){}
6
7 virtual void vfun1(){ cout << "Base::vfun1()" << endl; }
8
9 virtual void vfun2(){cout << "Base::vfun2()" << endl; }
10
11 private:
12
13 int a;
14
15 };
16
17 class Derived : public Base{
18
19 public:
20
21 void dfun(){}
22
23 //在派生类(子孙都包括)中也是虚函数,即使不再使用virtual关键字:
24
25 void vfun1(){ cout << "Derived::vfun1()" << endl; }
26
27 // virtual void vfun2(){}
28
29 virtual void vfun3(){}
30
31 private:
32
33 int b;
34
35 };
36
37 int main(int argc, char* argv[]){
38
39 Base b;
40
41 Derived td;
42
43 Base *pb = &td;
44
45 pb->vfun2();
46
47 return 0;
48
49 }


  两个类VPTR指向的虚函数表(VTABLE)分别如下

  base类:

base-vtable
vptr-> &base::vfun1
&base::vfun2

  derived类:

derived-vtable
vptr-> &derived::vfun1
&base::vfun2
&derived::vfun3

  分析:derived::vfun1的地址覆盖了base::vfun1的地址,发生了函数覆盖的情况,这也就是为什么override翻译成覆盖的原因。在VTABLE中,编译器放置了在这个类中或在它的基类中所有已声明为virtual的函数的地址。如果在这个派生类中没有对在基类中声明为virtual的函数进行重新定义,编译器就使用基类的这个虚函数地址(vfun2的入口就是这种情况),然后编译器在这个类中放置 VPTR。当使用简单继承时,对于每个对象只有一个VPTR(如果是多继承则一个类会有多个VTABLE)。VPTR被初始化为指向相应的VTABLE是在构造函数中发生的。

  虚拟函数与构造、析构函数

  1、构造函数本身不能是虚拟函数

  子类的构造函数的调用顺序是:父类——>子类

  想一想,在基类构造函数中使用虚机制,则可能会调用到子类,此时子类尚未生成,有何后果!?。

  2、析构函数本身常常是虚拟函数

  子类的构造函数的调用顺序是:子类——>父类

 1 class A{
2
3 public:
4
5 A() { foo();}
6
7 ~A() { foo();}
8
9 };
10
11 class B: public A {
12
13 public:
14
15 void foo(){}
16
17 };
18
19 void main(){
20
21 A * a = new B;
22
23 delete a;
24
25 }

  但是若类中使用了虚拟函数,析构函数一定要是虚拟函数,比如使用虚拟机制调用delete,没有虚拟的析构函数,子类析构时可能会发生内存泄漏。像上面这个例子,new操作会导致构造函数A()和B()都被调用,但进行delete操作时,尽管a是实际指向一个B对象,但是最终只有~A()被调用,这是因为虚构函数不是虚函数,所以执行的是静态绑定。编译器根据ptr的类型A*来决定调用哪一个析构函数。

  虚函数最主要的缺点是执行效率较低,看一看虚拟函数引发的多态性的实现过程,就体会到其中的原因。

 

 

原文链接: https://www.cnblogs.com/easonpan/archive/2012/03/24/2415636.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍

    C++中与多态有关的几个概念

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/45054

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年2月8日 下午9:39
下一篇 2023年2月8日 下午9:41

相关推荐