• C++——多态底层原理


    虚函数表

    先来看这个问题:

    1. class Base
    2. {
    3. public:
    4. virtual void Func1()
    5. {
    6. cout << "Func1()" << endl;
    7. }
    8. private:
    9. int _b = 1;
    10. };

    sizeof(Base)是多少? 

    答案是:8

    因为Base中除了成员变量_b,还有一个虚函数表_vfptr(当类中有虚函数就会生成),虚函数表的本质是函数指针数组,用来存储虚函数的指针


    1. class Base
    2. {
    3. public:
    4. virtual void Func1()
    5. {
    6. cout << "Base::Func1()" << endl;
    7. }
    8. virtual void Func2()
    9. {
    10. cout << "Base::Func2()" << endl;
    11. }
    12. void Func3()
    13. {
    14. cout << "Base::Func3()" << endl;
    15. }
    16. private:
    17. int _a = 1;
    18. };
    19. class Derive :public Base
    20. {
    21. public:
    22. void Func1()
    23. {
    24. cout << "Derive::Func1()" << endl;
    25. }
    26. private:
    27. int _b = 2;
    28. };
    29. void test1()
    30. {
    31. Base bs;
    32. Derive de;
    33. }

     

    基类虚函数表

     

    派生类基函数表

    派生类虚函数表是这样生成的:先将基类的虚函数表内容拷贝过来,在把派生类中重写的虚函数的地址覆盖到被重写的虚函数地址上,最后将派生类自己增加的虚函数依次增加到虚表的末尾。

    上面这个例子中,派生类和基类的虚函数表都存储有两个函数地址,即Func1和Func2,但是派生类只对Func1重写,没有对Func2重写,编译器会将派生类中的Func1的地址进行覆盖,所以Func1的地址不同,Func2的地址相同(0x009f1370)

    注意:虚函数存在哪,虚函数表存在哪?

    虚函数和普通的成员函数一样都存在公共代码段,虚函数表只是存了虚函数的指针;虚函数表存在哪,这个要看编译器,感兴趣可以自己验证。

    我的编译器是将虚函数表存储在类对象的开始位置,使用32位系统虚表大小4byte

    1. void test()
    2. {
    3. int i = 0;
    4. printf("栈区:%p\n", &i);
    5. char* ch = new char;
    6. printf("堆区:%p\n", ch);
    7. static int ci = 1;
    8. printf("数据段:%p\n", &ci);
    9. const char* s = "hello";
    10. printf("代码段:%p\n", s);
    11. Derive de;
    12. printf("虚表:%p\n", *((int*)&de));
    13. }

    虚表地址和代码段地址相差48byte,基本可以认为虚表存在代码段。大家可以参考这个方法,根据自己的编译器和环境来测试


    多态原理

    说了这么多,虚函数表在多态中有什么用?

    当我们有基类的指针或引用调用派生类的虚函数时,第一步是将派生类赋值给基类,这时派生类只会将属于基类的那部分交给基类的指针或引用,而属于基类的那部分中包含了派生类的虚函数表。这样造成的结果就是,调用虚函数时调用了派生类中重写过的虚函数,实现了多态。

    为什么直接将派生类赋值给基类就不能实现多态呢?因为这种写法会在编译器编译时,直接从符号表确定函数地址,程序运行时直接调用。而多态调用在编译期间不会确定,只在程序运行时到对象中寻找函数。其实编译器并不知道这个对象是基类还是派生类,只是无脑从虚函数表中找。


    下面这个测试程序,通过观察汇编代码,就可以直观看出普通调用和多态调用的区别:

    1. void test()
    2. {
    3. Base bs;
    4. Derive de;
    5. bs = de;
    6. Base& b = de;
    7. bs.Func1();
    8. b.Func1();
    9. }

    多继承的虚函数表

    前面都是以单继承为例讲虚函数表和多态原理,下面我们看多继承的虚函数表

    1. class Base1
    2. {
    3. public:
    4. virtual void Func1()
    5. {
    6. cout << "Base1::Func1()" << endl;
    7. }
    8. virtual void Func2()
    9. {
    10. cout << "Base1::Func2()" << endl;
    11. }
    12. private:
    13. int _a = 1;
    14. };
    15. class Base2
    16. {
    17. public:
    18. virtual void Func3()
    19. {
    20. cout << "Base2::Func3()" << endl;
    21. }
    22. virtual void Func4()
    23. {
    24. cout << "Base2::Func4()" << endl;
    25. }
    26. };
    27. class Derive :public Base1 ,public Base2
    28. {
    29. public:
    30. void Func1()
    31. {
    32. cout << "Derive::Func1()" << endl;
    33. }
    34. void Func3()
    35. {
    36. cout << "Derive::Func3()" << endl;
    37. }
    38. virtual void Func5()
    39. {
    40. cout << "Derive::Func5()" << endl;
    41. }
    42. private:
    43. int _b = 2;
    44. };
    45. void test()
    46. {
    47. Base1 b1;
    48. Base2 b2;
    49. Derive d;
    50. }

    调试: 

     

     

    多继承后的派生类有两个虚函数表,Func2和Func4没有重写,与基类函数地址相同。Func1和Func3重写后进行覆盖。派生类新增加的虚函数地址存到了最先继承的基类Base1的虚表中(第一张图的监视窗口未显示,有bug,在内存窗口可以看到) 

    重写是对虚函数函数体部分的重写

    看下面程序的运行结果是什么:

    1.  class A
    2.  {
    3.  public:
    4.    virtual void func(int val = 1){ std::cout<<"A->"<< val <
    5.    virtual void test(){ func();}
    6.  };
    7.  class B : public A
    8.  {
    9.  public:
    10.    void func(int val=0){ std::cout<<"B->"<< val <
    11.  };
    12.  int main(int argc ,char* argv[])
    13.  {
    14.    B*p = new B;
    15.    p->test();
    16.    return 0;
    17.  }

    test函数没有重写,直接调用A::test();func函数被重写,虚表中是B中func的函数地址,又因为重写只是对函数体的重写,val缺省值是0,结果是B->0.

  • 相关阅读:
    5-1.(OOP)初步分析MCV架构模式
    KnowStreaming贡献流程
    视频学习|Springboot在线学习系统
    centos如何配置永久ip
    Vue3 toRaw 和 markRaw
    MySQL——表的插入
    node.js+校内废品回收管理 毕业设计-附源码140933
    什么是视图
    Android Gradle 学习笔记(一)概述
    空气温湿度、光照度、二氧化碳传感器
  • 原文地址:https://blog.csdn.net/weixin_74269833/article/details/133671062