• 【C++ 学习 ⑲】- 多态(下)


    目录

    一、虚函数表和多态的原理

    1.1 - 虚函数表

    1.2 - 多态的原理

    二、单继承和多继承关系中的虚函数表

    2.1 - 单继承关系中的虚函数表

    2.2 - 多继承关系中的虚函数表

    三、纯虚函数和抽象类



    一、虚函数表和多态的原理

    1.1 - 虚函数表

    1. 问:sizeof(b) 是多少?

      1. #include
      2. using namespace std;
      3. class Base
      4. {
      5. public:
      6. virtual void func1() { cout << "Base::func1()" << endl; }
      7. virtual void func2() { cout << "Base::func2()" << endl; }
      8. void func3() { cout << "Base::func3()" << endl; }
      9. protected:
      10. int _i = 1;
      11. };
      12. int main()
      13. {
      14. Base b;
      15. cout << sizeof(b) << endl;
      16. return 0;
      17. }

      通过调试可以发现,在 b 对象内存模型中,除了 _i 成员,还有一个名为 _vfptr 的成员,它是虚函数表指针,所以 sizeof(b) 是 8 或 16 字节

      一个含有虚函数的类对象至少有一个指向虚函数表的指针,虚函数表本质上是一个存放虚函数地址的函数指针数组,一般情况下在这个数组的最后面还放了一个 nullptr

      虚函数表可以简称为虚表。因为 func3 不是虚函数,所以没有放进虚表中。

    2. 提一个很容易混淆的问题:虚函数存在哪里?虚表又存在哪里?虚函数和普通函数一样,都是存在代码段的;而虚表存在哪里可以通过以下代码得知

      1. #include
      2. using namespace std;
      3. class Base
      4. {
      5. public:
      6. virtual void func1() { cout << "Base::func1()" << endl; }
      7. virtual void func2() { cout << "Base::func2()" << endl; }
      8. void func3() { cout << "Base::func3()" << endl; }
      9. protected:
      10. int _i = 1;
      11. };
      12. typedef void(*VFPTR)();
      13. int main()
      14. {
      15. int m = 0;
      16. printf("栈:%p\n", &m);
      17. int* p1 = new int;
      18. printf("堆:%p\n", p1);
      19. static int n = 0;
      20. printf("静态区:%p\n", &n);
      21. const char* str = "abcdef";
      22. printf("常量区:%p\n", str);
      23. Base b;
      24.    // 通过对象的地址获取虚函数表的地址
      25. VFPTR* p2 = (VFPTR*)*(int*)&b;  
      26. printf("虚表:%p\n", p2);
      27. return 0;
      28. }

      根据输出结果,我们有理由相信虚表也是存在代码段的

    3. 让派生类 Derive 继承自 Base,然后在派生类中重写基类虚函数 func1:

      1. #include
      2. using namespace std;
      3. class Base
      4. {
      5. public:
      6. virtual void func1() { cout << "Base::func1()" << endl; }
      7. virtual void func2() { cout << "Base::func2()" << endl; }
      8. void func3() { cout << "Base::func3()" << endl; }
      9. protected:
      10. int _i = 1;
      11. };
      12. class Derive : public Base
      13. {
      14. public:
      15. virtual void func1() { cout << "Derived::func1()" << endl; }
      16. protected:
      17. int _j = 2;
      18. };
      19. int main()
      20. {
      21. Base b;
      22. Derive d;
      23. return 0;
      24. }

      因为在派生类中重写了基类的虚函数 func1,所以基类对象 b 和派生类对象 d 的虚表是不一样的

      d 的虚表中存的是重写的 Derive::func1,所以虚函数的重写也叫作覆盖,覆盖就是虚表中虚函数的覆盖。重写是语法上的叫法,覆盖是原理层的叫法

    1.2 - 多态的原理

    1. #include
    2. using namespace std;
    3. class Base
    4. {
    5. public:
    6. virtual void func1() { cout << "Base::func1()" << endl; }
    7. virtual void func2() { cout << "Base::func2()" << endl; }
    8. void func3() { cout << "Base::func3()" << endl; }
    9. protected:
    10. int _i = 1;
    11. };
    12. class Derive : public Base
    13. {
    14. public:
    15. virtual void func1() { cout << "Derive::func1()" << endl; }
    16. protected:
    17. int _j = 2;
    18. };
    19. int main()
    20. {
    21. Base b;
    22. Base* pb = &b;
    23. pb->func1();  // Base::func1()
    24. Derive d;
    25. pb = &d;
    26. pb->func1();  // Derive::func1()
    27. return 0;
    28. }

    当基类指针 pb 指向基类对象 b 时,pb->func1(); 就是在 b 的虚表中找到虚函数 Base::func1

    当基类指针 pb 指向派生类对象 d 时,pb->func1(); 就是在 d 的虚表中找到虚函数 Derive::func1

    这样就让基类指针表现出了多种形态

    注意:不满足多态的函数调用是编译时确定好的,满足多态的函数调用是运行时去对象中找的

    1. Base b;
    2. b.func1();
    3. // 00195182 lea ecx,[b]
    4. // 00195185 call Person::func1 (01914F6h)
    5. // 汇编代码分析:
    6. // 虽然 func1 是虚函数,但是 b 是对象,不满足多态的条件,所以这里是普通函数的调用,
    7. // 编译时就确定好了函数的地址,直接 call。
    8. Base* pb = &b;
    9. pb->func1();
    10. // 注意:不相关的汇编代码被省去了
    11. // 001940DE mov eax,dword ptr [pb]
    12. // 001940E1 mov edx,dword ptr [eax]
    13. // 00B823EE mov eax,dword ptr [edx]
    14. // 001940EA call eax
    15. // 汇编代码分析:
    16. // 1、pb 中存的是 b 对象的地址,将 pb 移动到 eax 中
    17. // 2、[eax] 就是取 eax 值指向的内容,相当于把 b 对象中的虚表指针移动到 edx
    18. // 3、[edx] 就是取 edx 值指向的内容,相当于把虚表中第一个虚函数的地址移动到 eax
    19. // 4、call eax 中存的虚函数地址
    20. // 由此可以看出满足多态的函数调用,不是在编译时确定的,而是运行起来后去对象中找的。

     


    二、单继承和多继承关系中的虚函数表

    2.1 - 单继承关系中的虚函数表

    1. #include
    2. using namespace std;
    3. class Base
    4. {
    5. public:
    6. virtual void func1() { cout << "Base::func1()" << endl; }
    7. virtual void func2() { cout << "Base::func2()" << endl; }
    8. protected:
    9. int _i = 1;
    10. };
    11. class Derive : public Base
    12. {
    13. public:
    14. virtual void func1() { cout << "Derive::func1()" << endl; }
    15. virtual void func3() { cout << "Derive::func3()" << endl; }
    16. virtual void func4() { cout << "Derive::func4()" << endl; }
    17. protected:
    18. int _j = 2;
    19. };
    20. int main()
    21. {
    22. Base b;
    23. Derive d;
    24. return 0;
    25. }

    在 d 的虚表中,我们看不到虚函数 func3 和 func4,这可能是监视窗口故意隐藏了这两个函数,也可能是一个小 bug,我们可以通过以下代码进行验证

    1. typedef void(*VFPTR)();
    2. void PrintVftable(VFPTR vftable[])
    3. {
    4. for (size_t i = 0; vftable[i] != nullptr; ++i)
    5. {
    6. printf("第 %d 个虚函数地址:0X%p, -->", i, vftable[i]);
    7. vftable[i]();
    8. }
    9. cout << endl;
    10. }
    11. void test()
    12. {
    13.    Base b;
    14. VFPTR* p1 = (VFPTR*)*(int*)&b;
    15. PrintVftable(p1);
    16. Derive d;
    17. VFPTR* p2 = (VFPTR*)*(int*)&d;
    18. PrintVftable(p2);
    19. }

     

    2.2 - 多继承关系中的虚函数表

    1. #include
    2. using namespace std;
    3. class Base1
    4. {
    5. public:
    6. virtual void func1() { cout << "Base1::func1()" << endl; }
    7. virtual void func2() { cout << "Base1::func2()" << endl; }
    8. protected:
    9. int _i1;
    10. };
    11. class Base2
    12. {
    13. public:
    14. virtual void func1() { cout << "Base2::func1()" << endl; }
    15. virtual void func2() { cout << "Base2::func2()" << endl; }
    16. protected:
    17. int _i2;
    18. };
    19. class Derive : public Base1, public Base2
    20. {
    21. public:
    22. virtual void func1() { cout << "Derive::func1()" << endl; }
    23. virtual void func3() { cout << "Derive::func3()" << endl; }
    24. protected:
    25. int _j;
    26. };
    27. typedef void(*VFPTR)();
    28. void PrintVftable(VFPTR vftable[])
    29. {
    30. for (size_t i = 0; vftable[i] != nullptr; ++i)
    31. {
    32. printf("第 %d 个虚函数地址:0X%p, -->", i, vftable[i]);
    33. vftable[i]();
    34. }
    35. cout << endl;
    36. }
    37. int main()
    38. {
    39. Derive d;
    40. VFPTR* p1 = (VFPTR*)*(int*)&d;
    41. PrintVftable(p1);
    42. // VFPTR* p2 = (VFPTR*)*(int*)((char*)&d + sizeof(Base1));
    43. // PrintVftable(p2)
    44. // 或者:
    45. Base2* p2 = &d;
    46. PrintVftable((VFPTR*)*(int*)p2);
    47. return 0;
    48. }

    1. 派生类中的虚函数 func3 放在第一个继承自基类部分的虚函数表中

    2. 假设有以下场景

      1. Derive d;
      2. Base1* p1 = &d;
      3. p1->func1();
      4. Base2* p2 = &d;
      5. p2->func1();

      首先要确定的是

      所以在语句 p2->func1(); 中,需要修正 this 指针

      这也是为什么在 d 的两个虚表中,重写的虚函数 func1 的地址不一样


    三、纯虚函数和抽象类

    纯虚函数是一种特殊的虚函数,在某些情况下,在基类中不能对虚函数给出有意义的实现,就可以把它声明为纯虚函数。纯虚函数只有函数名、参数和返回值类型,没有函数体,具体实现留给派生类去做。具体语法:virtual 返回值类型 函数名(参数列表) = 0;

    含有纯虚函数的类被称为抽象类(或接口类),不能实例化对象,但可以创建指针和引用

    派生类必须重写抽象类中的纯虚函数,否则也属于抽象类

    1. #include
    2. using namespace std;
    3. class Car
    4. {
    5. public:
    6. virtual void Drive() = 0;
    7. };
    8. class AITO : public Car
    9. {
    10. public:
    11. virtual void Drive() { cout << "Intelligent" << endl; }
    12. };
    13. class AVATR : public Car
    14. {
    15. public:
    16. virtual void Drive() { cout << "Comfortable" << endl; }
    17. };
    18. void func1(Car* p) { p->Drive(); }
    19. void func2(Car& c) { c.Drive(); }
    20. int main()
    21. {
    22. AITO aito;
    23. AVATR avatr;
    24. func1(&aito);  // Intelligent
    25. func1(&avatr);  // Comfortable
    26. func2(aito);  // Intelligent
    27. func2(avatr);  // Comfortable
    28. return 0;
    29. }
  • 相关阅读:
    ②【Hash】Redis常用数据类型:Hash [使用手册]
    Zookeeper 和 Kafka 面试题
    信息安全实验——口令破解技术
    【RocketMQ系列一】初识RocketMQ
    Ubuntu20.04沉浸式装机
    Maven安装与配置,Idea配置Maven
    Android SeekBar使用避坑指南
    基于多尺度集成极限学习机回归(Matlab代码实现)
    undefined reference to symbol ‘g_signal_connect_data‘
    39. UE5 RPG角色释放技能时转向目标方向
  • 原文地址:https://blog.csdn.net/melonyzzZ/article/details/132672025