• 深入C++虚函数表


    文章目录

    什么是多态?

    多态的分类:

     运行时的多态

     虚函数的定义:

    运行时的多态(晚绑定)

    虚函数注意:

    虚函数:

    前提知识:函数指针的判别

    重载、覆盖(重写)、隐藏(重定义)的对比 

     虚函数表

    单一继承,无虚函数覆盖: 

    单一继承,有覆盖:

    多继承下,无覆盖:

    多继承下,有覆盖:


    什么是多态

            多态性是面向程序设计的关键技术之一。若程序设计语言不支持多态性,不能称为面向对象的语言。

            多态性是考虑在不同层次的类中,以及在同一类中,同名的成员的关系问题。    

            说白了就是指在父类中定义的属性和方法被子类继承之后,可以具有不同的数据类型或表现出不同的行为,这使得同一个属性或方法在父类及其各个子类中具有不同的含义。

    多态的分类:

            多态又分为编译时的多态性(静态的多态性),函数重载,运算符的重载都属于编译时的多态

            以类的虚成员函数为基础的运行时的多态

     运行时的多态

     虚函数的定义:

            虚函数是一个类的成员函数,定义格式如下:

            virtual返回类型 函数名 (参数表)

            关键字virtual指明该成员函数为虚函数。virtual仅用于类定义中,如果虚函数在类外定义,则不可加virtual。当某一个类的一个类成员函数被定义为虚函数,则由该类派生出来的所有派生类中该函数始终保持虚函数的特征。

            注意:运行时的多态性:公有继承+基类和子类有的同名同参的虚函数+基类的指针或者引用指向基类对象或者派生类对象

    运行时的多态(晚绑定)

    定义父类A,子类B继承A,编写测试用例父类对象传入指针,引用

    1. class A
    2. {
    3. public:
    4. virtual void fn()
    5. {
    6. cout << "A::fn" << endl;
    7. }
    8. };
    9. class B :public A
    10. {
    11. public:
    12. virtual void fn()
    13. {
    14. cout << "B::fn" << endl;
    15. }
    16. };
    17. void test1(A a)
    18. {
    19. a.fn();
    20. }
    21. void test2(A& pa)
    22. {
    23. pa.fn();
    24. }
    25. void test(A* pa)
    26. {
    27. pa->fn();
    28. }
    29. void main()
    30. {
    31. A a;
    32. B b;
    33. a.fn();//a直接调用fn
    34. b.fn();//b直接调用fn
    35. test(&a);//传入a对象地址,找到A下虚表
    36. test(&b);//传入b对象地址,找到B下虚表
    37. test2(a);//同理,找到相应类下虚指针,指向所对应虚表,调用对应虚函数
    38. test2(b);
    39. }

    虚函数注意:

    虚函数的默认参数是静态绑定的,在重新定义虚函数时候,不重新定义继承而来的参数值
    除非在调用时候实际的传递想要的参数

    1. class Parent
    2. {
    3. public:
    4. virtual void fn(int a = 10)
    5. {
    6. cout << "parent fn a = " << a << endl;
    7. }
    8. };
    9. class Child :public Parent
    10. {
    11. public:
    12. virtual void fn(int b = 20)
    13. {
    14. cout << "child fn b = " << b << endl;
    15. }
    16. };
    17. void main()
    18. {
    19. Child cc;
    20. Parent* p = &cc;
    21. p->fn(); //child fn b=10 默认参数静态绑定
    22. p->fn(100); //child fn b=100
    23. }

    给一个例题看看输出结果: 

    1. class Parent
    2. {
    3. public:
    4. void print()
    5. {
    6. cout << "parent print" << endl;
    7. test();
    8. }
    9. virtual void test()
    10. {
    11. cout << "parent test" << endl;
    12. }
    13. };
    14. class Child :public Parent
    15. {
    16. public:
    17. void show()
    18. {
    19. cout << "Child show" << endl;
    20. print();
    21. }
    22. virtual void test()
    23. {
    24. cout << "child test" << endl;
    25. }
    26. };
    27. void main()
    28. {
    29. Child cc;
    30. cc.show();
    31. }
    1. 理解函数调用顺序
    2. 运行结果:
    3. Child show
    4. parent print
    5. child test

    虚函数:

    注意以下几点:

    1. 派生类中定义虚函数必须和基类中的虚函数同名外,还必须同参数表,同返回类型。否则被认为是重载,而不是虚函数。如基类中返回基类指针,派生类中返回派生类指针是允许的,这是一个例外
    2. 只有类的成员函数才能说明为虚函数,这是因为虚函数仅仅适用于有继承关系的类对象。友元函数和全局函数不能作为虚函数
    3. 静态成员函数,是所有同一类对象共有,不受限于某个对象,不能作为虚函数。
    4. 内联函数每个对象一个拷贝,无映射关系,不能作为虚函数。
    5. 构造函数和拷贝构造函数不能作为虚函数。构造函数和拷贝构造函数是设置虚表指针。
    6. 析构函数可以定义为虚函数,构造函数不能定义为虚函数,因为在调用函数构造时对象还没有完成实例化(虚表指针没有设置)。在基类中及其派生类中都有动态分配的内存空间时,必须把析构函数定义为虚函数,实现撤销对象时的多态性。
    7. 实现动态多态性时,必须使用基类类型的指针变量或引用,使该指针向基类的不同派生类的对象,并通过该指针指向虚函数,才能实现动态的多态性。
    8. 函数执行速度要稍微慢一些,为了实现多态性,每一个派生类中均要保存相应虚函数的入口地址表,函数的调用机制也是间接实现。所以多态性总要是付出一定代价,但通用性是一个更高的目标。
    9. 如果定义放在类外,virtual只能加在函数声明前面,不能加在函数定义前面,正确的定义必须不包括virtual。

    前提知识:函数指针的判别

    void ( *signal( int, void (*func) ( int, int )) ) ( int );
    //signal是个函数名,往左看,是个*,说明返回值是个指针,向右看,右边是个(int),说明这个指针指向一个函数,再往左看,左边是函数的返回值类型
    //函数指针函数

    1. void fn()
    2. {
    3. cout << "fn" << endl;
    4. }
    5. void main()
    6. {
    7. void (*p)(); //函数指针---右左法则
    8. void (*q[3])();//找到q,向右看是个数组,所以q是个数组名,往左看,左边是个*(说明是指针),也就是说数组q中有3个指针,再继续向右看,右边是个(),说明是函数
    9. //函数指针数组 q[0],q[1],q[2]
    10. fn(); //地址
    11. }
    1. int max(int a, int b)
    2. {
    3. return a > b ? a : b;
    4. }
    5. int min(int a, int b)
    6. {
    7. return a < b ? a : b;
    8. }
    9. int Add(int a, int b)
    10. {
    11. return a + b;
    12. }
    13. void main()
    14. {
    15. int (*pf[3])(int, int) = { max,min,Add };
    16. //pf[0]指向的是max,pf[1]指向的是min,pf[2]指向的是Add
    17. for (int i = 0; i < 3; i++)
    18. cout << pf[i](2, 4) << endl;;
    19. }

    重载、覆盖(重写)、隐藏(重定义)的对比 

     虚函数表

    当类中存在虚函数时,就会产生虚函数指针vfptr和虚函数表vbtable

    有以下概念:

    1. 一个类一张虚表,所有对象共享虚函数表vftable。
    2. 为了实现共享 :每一个对象中有一个指针,称为虚函数指针vfptr,指向虚函数表,这样就可以实现虚函数表的共享。系统会在每一个类中提供虚函数指针vfptr,我们看不见,所以一旦有virtual关键字,那么类的大小就会多加4个,多出来的就是虚函数指针的大小。
    3. 类中的布局:虚函数指针在前,其他成员变量在后,因为虚函数指针的布局优先级最高(在没有虚继承时)
    • 虚函数表结构,三部分:
    • RTTI:run-time type infornation运行时类型信息,在运行阶段提取出来的类型,可以使用typeid()函数获取。
    • 偏移:虚函数指针相对于整体作用域的偏移,用整体作用域-vfptr的位置得到。
    • 虚函数入口地址

    单一继承,无虚函数覆盖: 

    示例1:

    1. /*只要有虚函数,不管有多少个虚函数,都只多了4个字节,多了一个Vfptr
    2. vfptr虚指针指向了一个vftable,vftable中存储了当前类的虚函数的入口地址
    3. */
    4. #if 0
    5. class A
    6. {
    7. public:
    8. virtual void fa() { cout << "A::fa" << endl; }
    9. virtual void fb() { cout << "A::fb" << endl; }
    10. virtual void fc() { cout << "A::fc" << endl; }
    11. void fd() { cout << "A::fd" << endl; }
    12. private:
    13. int m_i;
    14. };
    15. void main()
    16. {
    17. cout<<sizeof(A)<
    18. A a;
    19. a.fa();
    20. a.fb();
    21. //A a;
    22. //typedef void (*Fun)();
    23. //Fun pf = NULL; //函数指针pf可以指向类A中的fa,fb,fc
    24. //pf = (Fun) * ((int*)(*(int*)(&a)));
    25. //pf(); //A::fa
    26. //pf = (Fun) * ((int*)*(int*)(&a) + 1);
    27. //pf(); //A::fb
    28. //pf = (Fun) * ((int*)*(int*)(&a) + 2);
    29. //pf(); //A::fc
    30. }

    示例2:

    1. class A
    2. {
    3. public:
    4. virtual void fa() { cout << "A::fa" << endl; }
    5. virtual void ga() { cout << "A::ga" << endl; }
    6. virtual void ha() { cout << "A::ha" << endl; }
    7. };
    8. class B :public A
    9. {
    10. public:
    11. virtual void fb() { cout << "B::fb" << endl; }
    12. virtual void gb() { cout << "B::gb" << endl; }
    13. };
    14. void main()
    15. {
    16. cout << sizeof(B) << endl;
    17. B b;
    18. typedef void (*Fun)();
    19. Fun pf = NULL; //函数指针pf可以指向类B中的fa,fb,fc,fb,gb
    20. pf = (Fun) * ((int*)(*(int*)(&b)));
    21. pf(); //A::fa
    22. pf = (Fun) * ((int*)*(int*)(&b) + 1);
    23. pf(); //A::ga
    24. pf = (Fun) * ((int*)*(int*)(&b) + 2);
    25. pf(); //A::ha
    26. pf = (Fun) * ((int*)*(int*)(&b) + 3);
    27. pf(); //B::fb
    28. pf = (Fun) * ((int*)*(int*)(&b) + 4);
    29. pf(); //B::gb
    30. }

    根据看监视:
    每个类里面只有一个虚指针,指向一个虚标
    发现B中的虚表中有5个虚函数,前三个是从A继承过来的虚函数的入口地址,后面两个是B类自己的虚函数 

    内存分布如下

    单一继承,有覆盖:

    1. class A
    2. {
    3. public:
    4.     virtual void fa() { cout << "A::fa" << endl; }
    5.     virtual void fb() { cout << "A::fb" << endl; }
    6.     virtual void fc() { cout << "A::fc" << endl; }
    7. };
    8. class B :public A
    9. {
    10. public:
    11.     /*
    12.     覆盖 子类重写了基类的同名同参的virtual函数,在B类的对象模型中,在继承下来虚表的同一处地址修改成B的重写的函数
    13.     */
    14.     virtual void fa() { cout << "B::fa" << endl; }
    15.     virtual void gb() { cout << "B::gb" << endl; }
    16. private:
    17.     int m_i;
    18.     int m_j;
    19. };
    20. void main()
    21. {
    22.     B b; 
    23. }

    多继承下,无覆盖:

    对于多继承,每个父类都有自己的虚表
    将最终子类的虚函数放在第一个父类的虚表中
    这样做解决了不同的父类类型的指针指向比较清晰

    1. class A
    2. {
    3. public:
    4. virtual void fa() { cout << "A::fa" << endl; }
    5. virtual void ga() { cout << "A::ga" << endl; }
    6. };
    7. class B
    8. {
    9. public:
    10. virtual void fb() { cout << "B::fb" << endl; }
    11. virtual void gb() { cout << "B::gb" << endl; }
    12. };
    13. class C
    14. {
    15. public:
    16. virtual void fc() { cout << "C::fc" << endl; }
    17. virtual void gc() { cout << "C::gc" << endl; }
    18. };
    19. class D :public A, public B, public C
    20. {
    21. public:
    22. virtual void fd() { cout << "D::fd" << endl; }
    23. virtual void gd() { cout << "D::gd" << endl; }
    24. };
    25. void main()
    26. {
    27. D d;
    28. cout << sizeof(D) << endl;
    29. }

    监视其内存:

    多继承下,有覆盖:

    不相同的子类虚函数会贴在第一个父类虚表后面的位置。

    相同的全部覆盖。

    1. class A
    2. {
    3. public:
    4. virtual void f() { cout << "A::f" << endl; }
    5. virtual void ga() { cout << "A::ga" << endl; }
    6. };
    7. class B
    8. {
    9. public:
    10. virtual void f() { cout << "B::f" << endl; }
    11. virtual void gb() { cout << "B::gb" << endl; }
    12. };
    13. class C
    14. {
    15. public:
    16. virtual void f() { cout << "C::f" << endl; }
    17. virtual void gc() { cout << "C::gc" << endl; }
    18. };
    19. class D :public A, public B, public C
    20. {
    21. public:
    22. virtual void f() { cout << "D::f" << endl; }
    23. virtual void gd() { cout << "D::gd" << endl; }
    24. };
    25. void main()
    26. {
    27. D d;
    28. cout << sizeof(D) << endl;
    29. //函数指针
    30. typedef void(*Fun)();
    31. //第一个虚表
    32. Fun pf = (Fun) * (((int*)*(int*)(&d)));
    33. pf();//D:f
    34. pf = (Fun) * (((int*)*(int*)(&d)) + 1);
    35. pf();//A:ga
    36. pf = (Fun) * (((int*)*(int*)(&d)) + 2);
    37. pf();//D:gd
    38. //第二个虚表
    39. pf = (Fun) * ((int*)*(int*)((int*)(&d) + 1));
    40. pf();//D:f
    41. pf = (Fun) * ((int*)*(int*)((int*)(&d) + 1) + 1);
    42. pf();//B:gb
    43. //第三个虚表
    44. pf = (Fun) * ((int*)*(int*)((int*)(&d) + 2));
    45. pf();//D:f
    46. pf = (Fun) * ((int*)*(int*)((int*)(&d) + 2) + 1);
    47. pf();//c:gc
    48. }

     内存分布:

    日常巩固复习

  • 相关阅读:
    打印机出现黄色感叹号!无法查看属性和设置,开机查看打印机,打印自动变灰色问题无法使用!
    Youtube下载神器YT
    30天精通Nodejs--第一天:入门指南
    C语言描述数据结构 —— 常见排序(1)直接插入排序、希尔排序、选择排序、堆排序
    IPD流程概要
    笔试强训第25天--编程题--(树根+星际密码)
    互联网Java工程师面试题·Spring篇·第七弹
    贝叶斯视角下的机器学习
    一键部署k8s集群
    WGAN(1)——为什么不能直接拟合原始分布Pr
  • 原文地址:https://blog.csdn.net/weixin_51609435/article/details/126060633