深入理解C++多态及实现机制

1、多态

多态是面向对象程序的核心概念。C++的多态实现是基于继承和虚函数,这种实现称为动态多态性。多态性可以简单的概括为“1个接口,多种方法”,在程序运行的过程中才决定调用的机制,简单的说就是可以通过父类指针调用子类的函数,可以让父类指针有多种形态。


2、问题引入

先看下面几个问题:

//code sample1
class A{
    void func(){}
};

class B :public A{
    void func(){}
};

int main(void)
{
     cout<<sizeof(A)<<" "<<sizeof(B)<<endl;
     return 0;
}

输出的结果是:1 1


//code sample2
class A{
    virtual void funcA(){}
};
class B:public A{
    virtual void funcB(){}
};

int main(void)
{
      cout<<sizeof(A)<<" "<<sizeof(B)<<endl;
      return 0; 
}
输出结果是: 4 4


//code sample3
class A{
    virtual void funcA(){}
};
class B:virtual public A{
     virtual void funcB(){}
};

int main(void)
{
   cout<<sizeof(A)<<" "<<sizeof(B)<<endl;
   return 0;
}

输出结果是:4 12


分析结果

1、对于sample1情况,没有出现虚函数,且也没有任何成员变量,因此是一个空类。空类理论上可以实例化,每个实例在内存中都有独一无二的地址来表明,所以会占用1个字节的空间。

2、对于第二、三种情况,引入了虚函数,而且在第三种情况中,还引入了虚基类的概念。下面从内存分配角度来说明这个问题。

最简单的对象模型(如图1所示)


图1,简单对象模型

在内存中,对象a的静态/非静态成员函数和成员变量的地址都存储在一个表中,通过表内存储的地址指向相应的部分。

空间上:没必要为每一个实例存储静态成员变量和成员函数;

效率上:每次执行实例的一个成员函数都要遭表内进行搜索;

这是最初的对象模型,后来改良成表格驱动对象模型(如图2所示):



图2、 表格驱动对象模型
上述模型将变量和函数进行了分割存储。

最后为了支撑虚函数,引入了现在的C++对象模型(如图3所示):

图3、C++对象模型
在C++对象模型中,对象的非静态成员变量和指向虚函数表的指针,静态成员变量和函数,非静态成员函数分离存储。类的每一个实例都存有vptr和非静态成员函数,他们独立拥有这些数据,并不和其他的实例共享。
然后分析上面的第二种情况,class A和继承自A的class B都拥有虚函数,因此都会有一个vptr,因此sizeof运算得到的结果都为4.如果往里面添加一个非静态int型变量,那么相应可以得到8个字节的大小;但如果往里面添加静态int型变量,大小却没有改变。
第三种情况,

3、单一继承
class A
{
public:
    int a;
    void foo(){}
    virtual void funcA(){}
    virtual void func()
    {cout << "class A's func." << endl;}
};
 
classB : public A
{
public:
    int b;
    void foo(){}
    virtual void funcB(){}
    virtual void func()
    {cout << "class B's func." << endl;}
};
 
int main(void)
{
    A *pa = newB;
    pa->func();
}

输出的结果是:class B's func.

多态就是多种状态,一个事物可能有多种表现形式,譬如动物,有十二生肖甚至更多的表现形式。当基类里实现了某个虚函数,但派生类没有实现,那么类 B 的实例里的虚函数表中放置的就是 &A::func。此外,派生类也实现了虚函数,那么类 B 实例里的虚函数表中放置的就是 B::func。A *pa = new B; 因为 B 实现了 func,那么它被放入 A 实例的虚拟函数表中,从而代替 A 实例本身的虚拟函数。pa->func(); 调用的结果就不稀奇了,这是虚函数机制带来的。

class A 和 class B 的内存布局和 vptr 可能是下面的样子:

  1. ———-
  2. |   int a |
  3. ———-
  4. |    vptr | ——–>|      &A::funcA()
  5. ———-             ————————————————-
  6.                           |      &A::func()
  7.                          ————————————————-
  1. ———-
  2. |   int a |
  3. ———-
  4. |    vptr | ——–>|     &A::funcA() 依旧是 A 的虚函数
  5. ———-             ————————————————-
  6. |   int b |              |     &B::func() A::func()
  7. ———-             ————————————————-
  8.                           |     &B::funcB()
  9.                           ————————————————-

总结一下:

  • 当引入虚函数的时候,会添加 vptr 和 其指向的一个虚拟函数表从而增加额外的空间,这些信息在编译期间就已经确定,而且在执行期不会插足修改任何内容。
  • 在类的构造和析构函数当中添加对应的代码,从而能够为 vptr 设定初值或者调整 vptr,这些动作由编译器完成,class 会产生膨胀。
  • 当出现继承关系时,虚拟函数表可能需要改写,即当用基类的指针指向一个派生类的实体地址,然后通过这个指针来调用虚函数。这里要分两种情况,当派生类已经改写同名虚函数时,那么此时调用的结果是派生类的实现;而如果派生类没有实现,那么调用依然是基类的虚函数实现,而且仅仅在多态仅仅在虚函数上表现。
  • 多态仅仅在虚函数上表现,意即倘若同样用基类的指针指向一个派生类的实体地址,那么这个指针将不能访问和调用派生类的成员变量和成员函数。
  • 所谓执行期确定的东西,就是基类指针所指向的实体地址是什么类型了,这是唯一执行期确定的。以上是单一继承的情况,在多重继承的情况会更为复杂。
4、多重继承

class A
{
public:
    virtual ~A(){cout << "A destruction" << endl;}
    int a;
    void fooA(){}
    virtual void func(){cout << "A func." << endl;};
    virtual void funcA(){cout << "funcA." << endl;}
};
 
class B
{
public:
    virtual ~B(){cout << "B destruction" << endl;}
    int b;
    void fooB(){}
    virtual void func(){cout << "B func." << endl;};
    virtual void funcB(){cout << "funcB." << endl;}
};
 
class C : public A,public B
{
public:
    virtual ~C(){cout << "C destruction" << endl;}
    int c;
    void fooC(){}
    virtual void func(){cout << "C func." << endl;};
    virtual void funcC(){cout << "funcC." << endl;}
};
 
int main(void) 
{  
    return 0;
}

当用基类的指针指向一个派生类的实体地址,基类有两种情况,一种是 class A 和 class B,如果是 A,问题容易解决,几乎和上面单一继承情况类似;但倘若是 B,要做地址上的转换,情况会比前者复杂。先展现class A,B,C 的内存布局和 vptr:

  1. ———-
  2. |   int a |
  3. ———-
  4. |    vptr | ——–>|      &A::~A()
  5. ———-             ————————————————-
  6.                             |      &A::func()
  7.                             ————————————————-
  8.                             |      &A::funcA()
  9.                             ————————————————-
  1. ———-
  2. |   int b |
  3. ———-
  4. |    vptr | ——–>|     &B::~B()
  5. ———-             ————————————————-
  6.                             |     &B::func()
  7.                             ————————————————-
  8.                             |     &B::funcB()
  9.                             ————————————————–

 

  1.                             |      &C::~C() &A::~A()
  2. ———-             ————————————————-
  3. |   int a |               |      &C::func() &A::func()
  4. ———-             ————————————————-
  5. ———-             |      &C::funcC()
  6. |    vptr | ——–>————————————————-
  7. ———-             |      &A::funcA()
  8. ———-             ————————————————-
  9. |   int b |               |      &B::funcB() 跳
  10. ———-             ————————————————-
  11. ———-
  12. |    vptr | ——–>|     &C::~C() &B::~B() 跳
  13. ———-             ————————————————-
  14. |   int c |               |     &C::func() &B::func() 跳
  15. ———-             ————————————————-
  16.                            |     &B::funcB()
  17.                       

多重继承中,会有保留两个虚拟函数表,一个是与 A 共享的,一个是与 B 相关的,他们都在原有的基础上进行了修改:

对于 A 的虚拟函数表:

  • 覆盖派生类实现的同名虚函数,并用派生类实现的析构函数覆盖原有虚函数
  • 添加了派生类独有的虚函数
  • 添加了右端父类即 B 的独有虚函数,需跳转

对于 B 的虚拟函数表:

  • 覆盖派生类实现的同名虚函数,并用派生类实现的析构函数覆盖原有虚函数,但需跳转
int main(void)
{
     A *pa = new C;
     B *pb = new C;
     C *pc = new C;
     pa->func();
     pb->func();
     pc->funcC();
     delete pb;
     delete pa;
     delete pc;
}

输出结果是:
C func.
C func.
funcC.
C destruction
B destruction
A destruction
C destruction
B destruction
A destruction
C destruction
B destruction
A destruction

7 行和 8 行的行为有很大的区别,7 行的调用和上面的单一继承的情况类似,不赘述。8 行的 pb->func(); 中,pb 所指向的是上图第 9 行的位置,编译器已在内部做了转换,也就是 pa 和 pb 所指的位置不一样,pa 指向的是上图第 3 行的位置。接着需要注意的是,pb->func(); 调用时,在虚拟函数表中找到的地址需要再进行一次跳转,目标是 A 的虚拟函数表中的 &C::func(),然后才真正执行此函数。所以,上面的情况作了指针的调整。

那什么时候会出现跳,常见的有两种情况:

  1. 右端基类,对应上面的具体是 B,调用派生类虚拟函数,比如 pb->~C() 和 pb->func()
  2. 派生类调用右端基类的虚拟函数,比如 pc->funcB()

所以 delete pa; 和 delete pa; 的操作是不一样的,pb->funcB(); 和 pc->funcB(); 也不一样。

C++ 为实现多态引入虚函数机制,带来了空间和执行上的折损。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值