• C++修炼之路之多态---多态的原理(虚函数表)


    目录

    一:多态的原理 

    1.虚函数表

     2.原理分析

    3.对于虚表存在哪里的探讨

    4.对于是不是所有的虚函数都要存进虚函数表的探讨

    二:多继承中的虚函数表

    三:常见的问答题 

    接下来的日子会顺顺利利,万事胜意,生活明朗-----------林辞忧 

    接上篇的多态的介绍后,接下来介绍多态的原理以及虚函数表的相关知识

    一:多态的原理 

    1.虚函数表

    这里从一道经典笔试题引入

    对于这道题我们可能想到的是计算类 大小的对齐规则,结果为4,但结果为8,这是因为有虚函数的类要多考虑一指针

    在32位系统下是8

    如果这里再添加几个虚函数呢?

     

    所以在这里不管类里面有多少个虚函数 ,只要是包含虚函数的类计算大小都要考虑添加一指针,再考虑对齐

    但这里的一指针是什么呢?

    但这里我们就看到在b1中除了_b还存有 一个_vfptr的指针在对象的前面,这个指针就叫做虚函数表指针,其中v代表virtual,f代表funcation

    每一个含有虚函数的类都至少有一个虚函数表指针,他的类型为函数指针数组,而虚函数的地址是存放在虚函数表中的,虚函数表也叫虚表

     2.原理分析

    1. class Base
    2. {
    3. public:
    4. virtual void Func1()
    5. {
    6. cout << "Func1()" << endl;
    7. }
    8. virtual void Func2()
    9. {
    10. cout << "Func2()" << endl;
    11. }
    12. private:
    13. int _b = 1;
    14. };
    15. class Derived : public Base
    16. {
    17. virtual void Func1()
    18. {
    19. cout << "Func()" << endl;
    20. }
    21. private:
    22. int _a = 0;
    23. };
    24. int main()
    25. {
    26. Base b1;
    27. Derived d1;
    28. return 0;
    29. }

     

    解释多态调用的两个条件

    对于条件一:必须是父类的指针或引用来调用函数

    1.父类的指针指向父类对象时,依据虚函数表指针(vfptr),在虚函数表中找到函数的地址,再call这个地址来执行接下来的操作

    2.父类的指针指向子类对象时,先完成切片,找到父类的那一部分,依据虚函数表指针(vfptr),在虚函数表中找到函数的地址,再call这个地址来执行接下来的操作

    3.由于经过虚函数的重写后,虚函数的地址是不相同的,所以结果是不相同的,这是就形成了多态

    对于编译器来说上面的两个调用是执行的同样的操作,都只是取对象的头四个字节,就是虚函数表指针,然后去虚表中找到对应调用函数的地址,然后执行接下来的操作

    4.如果是父类的对象调用函数的话这时就要分析可能会总成的结果

    这时尤其是这样的场景,Person* ptr=new Person,Student s;   *ptr=s ,这样如果支持能拷贝虚函数表指针的话,这时delete  ptr,就调用的是 Student类的析构函数,导致直接错误的

    5.对于多态调用是在运行时,去虚表里面找到函数指针,确定函数指针后,调用函数;

    对于普通调用是在编译链接时,确定函数地址

    6.派生类中只有一个虚表指针(菱形继承除外),同一个类的对象共用一张虚表

    7.虚函数也是也是和成员函数一样存在代码段的,不同的是虚函数会将自己的地址存在虚表中

    对于条件二:虚函数的重写

    从上面就可以看出虚函数的重写也叫覆盖,覆盖了原先虚函数的地址,重写是语法层的叫法,而覆盖是原理层的叫法

    三:派生类的虚表生成

    1.先将基类中的虚表内容拷贝一份到派生类的虚表中

    2.如果派生类重写了基类中的某个虚函数,用派生类自己的虚函数的地址来覆盖虚表中基类的虚函数地址

    3.派生类自己新增的虚函数按其在派生类中的声明顺序增加到派生类虚表的最后

    3.对于虚表存在哪里的探讨

    对于栈和堆是不可能的,只有代码段或者静态区,但我们可以自己验证是存在哪里的

    验证代码

    1. class Base {
    2. public:
    3. virtual void func1() { cout << "Base::func1" << endl; }
    4. virtual void func2() { cout << "Base::func2" << endl; }
    5. private:
    6. int a;
    7. };
    8. void func()
    9. {
    10. cout << "void func()" << endl;
    11. }
    12. int main()
    13. {
    14. Base b1;
    15. Base b2;
    16. static int a = 0;
    17. int b = 0;
    18. int* p1 = new int;
    19. const char* p2 = "hello world";
    20. printf("静态区:%p\n", &a);
    21. printf("栈:%p\n", &b);
    22. printf("堆:%p\n", p1);
    23. printf("代码段:%p\n", p2);
    24. printf("虚表:%p\n", *((int*)&b1));
    25. printf("虚函数地址:%p\n", &Base::func1);
    26. printf("普通函数地址:%p\n", func);
    27. return 0;
    28. }

    对于这里的取虚表地址

     

    可以这样来理解,&b1是整个类的地址,然后强转为(int*),再解引用取得就是头四个字节,即虚表地址 

     

    我们发现 和虚表地址最接近的为代码段的地址,所以可以确定虚表是存在代码段的

    4.对于是不是所有的虚函数都要存进虚函数表的探讨

    首先确定答案 一定都是存在虚函数表的

    接下来我们在vs上监视窗口来查看

    分析代码

    1. class Base {
    2. public:
    3. virtual void func1() { cout << "Base::func1" << endl; }
    4. virtual void func2() { cout << "Base::func2" << endl; }
    5. private:
    6. int a;
    7. };
    8. class Derive :public Base {
    9. public:
    10. virtual void func1() { cout << "Derive::func1" << endl; }
    11. virtual void func3() { cout << "Derive::func3" << endl; }
    12. virtual void func4() { cout << "Derive::func4" << endl; }
    13. void func5() { cout << "Derive::func5" << endl; }
    14. private:
    15. int b;
    16. };
    17. class X :public Derive {
    18. public:
    19. virtual void func3() { cout << "X::func3" << endl; }
    20. };
    21. int main()
    22. {
    23. Base b;
    24. Derive d;
    25. X x;
    26. Derive* p = &d;
    27. p->func3();
    28. p = &x;
    29. p->func3();
    30. return 0;
    31. }

      

    对于这里监视窗口的显示,在这里对于b是只有两个虚函数都存进了虚函数表中,但对于d和x都应该是四个虚函数存进虚函数表的,但在这里都只存了两个虚函数,但验证多态调用的话,结果为

    结果是多态调用, 这时我们就不得不质疑此时监视窗口 的结果了

    为了进一步的证明。我们可以调用内存窗口来查看

    在内存中我们就会发现后两个地址与前两个虚函数的地址很接近,所以我们暂时可以认为虚函数是都存在虚函数表中的,

    为了确定结果,我们可以使用打印虚表来验证猜想

    1. class Base {
    2. public:
    3. virtual void func1() { cout << "Base::func1" << endl; }
    4. virtual void func2() { cout << "Base::func2" << endl; }
    5. private:
    6. int a;
    7. };
    8. class Derive :public Base {
    9. public:
    10. virtual void func1() { cout << "Derive::func1" << endl; }
    11. virtual void func3() { cout << "Derive::func3" << endl; }
    12. virtual void func4() { cout << "Derive::func4" << endl; }
    13. void func5() { cout << "Derive::func5" << endl; }
    14. private:
    15. int b;
    16. };
    17. class X :public Derive {
    18. public:
    19. virtual void func3() { cout << "X::func3" << endl; }
    20. };
    21. typedef void (*VFUNC)();
    22. //void PrintVFT(VFUNC a[])
    23. void PrintVFT(VFUNC* a)
    24. {
    25. for (size_t i = 0; a[i] != 0; i++)
    26. {
    27. printf("[%d]:%p->", i, a[i]);
    28. VFUNC f = a[i];
    29. f();
    30. //(*f)();
    31. }
    32. printf("\n");
    33. }
    34. int main()
    35. {
    36. Base b;
    37. PrintVFT((VFUNC*)(*((long long*)&b)));//32位的话,可以采用int
    38. Derive d;
    39. X x;
    40. // PrintVFT((VFUNC*)&d);
    41. PrintVFT((VFUNC*)(*((long long*)&d)));
    42. PrintVFT((VFUNC*)(*((long long*)&x)));
    43. return 0;
    44. }

     

    这样看,只要是虚函数,都会将地址存到类的虚函数表里面的

     

    二:多继承中的虚函数表

    同样的我们可以采用例子来介绍

    1. class Base1 {
    2. public:
    3. virtual void func1() { cout << "Base1::func1" << endl; }
    4. virtual void func2() { cout << "Base1::func2" << endl; }
    5. private:
    6. int b1;
    7. };
    8. class Base2 {
    9. public:
    10. virtual void func1() { cout << "Base2::func1" << endl; }
    11. virtual void func2() { cout << "Base2::func2" << endl; }
    12. private:
    13. int b2;
    14. };
    15. class Derive : public Base1, public Base2 {
    16. public:
    17. virtual void func1()
    18. {
    19. cout << "Derive::func1" << endl;
    20. }
    21. virtual void func3() { cout << "Derive::func3" << endl; }
    22. private:
    23. int d1;
    24. };
    25. int main()
    26. {
    27. Derive d;
    28. Base1* p1 = &d;
    29. p1->func1();
    30. Base2* p2 = &d;
    31. p2->func1();
    32. return 0;
    33. }

    采用监视窗口的话 

    就会发现对于基类的两张虚表中都没有存derived类的fun3() ,但我们可以使用多态的调用来验证下

    所以的话,fun3是一定存在基类的两张 虚表中的其中一个里面,这样采用内存来看

    所以最好的方式,我们还是来打印两个基类的虚函数表的 

    1. typedef void (*VFUNC)();
    2. //void PrintVFT(VFUNC a[])
    3. void PrintVFT(VFUNC* a)
    4. {
    5. for (size_t i = 0; a[i] != 0; i++)
    6. {
    7. printf("[%d]:%p->", i, a[i]);
    8. VFUNC f = a[i];
    9. f();
    10. //(*f)();
    11. }
    12. printf("\n");
    13. }
    14. class Base1 {
    15. public:
    16. virtual void func1() { cout << "Base1::func1" << endl; }
    17. virtual void func2() { cout << "Base1::func2" << endl; }
    18. private:
    19. int b1;
    20. };
    21. class Base2 {
    22. public:
    23. virtual void func1() { cout << "Base2::func1" << endl; }
    24. virtual void func2() { cout << "Base2::func2" << endl; }
    25. private:
    26. int b2;
    27. };
    28. class Derive : public Base1, public Base2 {
    29. public:
    30. virtual void func1()
    31. {
    32. cout << "Derive::func1" << endl;
    33. }
    34. virtual void func3() { cout << "Derive::func3" << endl; }
    35. private:
    36. int d1;
    37. };
    38. int main()
    39. {
    40. Derive d;
    41. PrintVFT((VFUNC*)(*(int*)&d));
    42. //PrintVFT((VFUNC*)(*(int*)((char*)&d+sizeof(Base1))));
    43. Base2* ptr = &d;
    44. PrintVFT((VFUNC*)(*(int*)ptr));
    45. /*Base1* p1 = &d;
    46. p1->func1();
    47. Base2* p2 = &d;
    48. p2->func1();*/
    49. return 0;
    50. }

     

    所以此时我们就会知道,派生类的虚函数地址是存在第一个基类的虚函数表里面的 

    三:常见的问答题 

     

  • 相关阅读:
    语义分割 U-Net 应用入门
    mulesoft Module 1 quiz 解析
    Android中使用Java操作List集合的方法合集,包括判读是否有重复元素等
    Cannot use @TaskAction annotation on method TransformTask.transform()
    pdf增强插件 Enfocus PitStop Pro 2022 mac中文版功能介绍
    “批量剪辑,统一视频封面,让你的创作更高效!“
    企业管理中,商业智能BI主要做哪些事情?
    STM32基于HAL库的非DMA的轮询ADC单通道与多通道的采样
    Qt中的枚举变量,Q_ENUM,Q_FLAG以及Qt中自定义结构体、枚举型做信号参数传递
    Aeraki Mesh 正式成为CNCF沙箱项目,腾讯云携手合作伙伴加速服务网格成熟商用
  • 原文地址:https://blog.csdn.net/Miwll/article/details/138046337