• (入门自用)--C++--抽象类--多态原理--虚表--1020


    0. 补充几个知识点

    析构函数重写的意义

    1. class Person
    2. {
    3. public:
    4. virtual ~Person()
    5. {
    6. cout<<"~Person"<
    7. }
    8. };
    9. class Student :public Person
    10. {
    11. virtual ~Student()
    12. {
    13. cout<<"~Student"<
    14. }
    15. };
    16. int main()
    17. {
    18. Person*ptr1=new Person;
    19. Person*ptr2=new Student;
    20. delete ptr1;
    21. delete ptr2;
    22. return 0;
    23. }

     

     子类析构函数重写父类析构函数,这里才能正确调用。指向谁调用谁的析构函数。

    否则都是根据this指针去调用父类的析构函数而导致子类中的元素没有被析构。

    重载、重写、隐藏

    重载两个函数在同一作用域
    函数名相同、参数不同。(参数个数、参数类型、类型顺序)
    重写两个函数分别在基类和派生类的作用域
    函数名/参数/返回值必须相同(协变除外)
    两个函数必须是虚函数
    隐藏两个函数必须分别在基类和派生类的作用域
    函数名相同
    不构成重写才是隐藏

    1.抽象类

    纯虚函数:在虚函数的后面写上 =0 。

    1. class Car
    2. {
    3. public:
    4. virtual void Drive() = 0;
    5. };

    Car就是一个抽象类。

    包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象。

    1. class Benz :public Car
    2. {
    3. };

    子类继承后也不能实例化出对象。只有重写了纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。(接口是基类的,实现是派生类的)

    1. class Car
    2. {
    3. public:
    4. virtual void Drive() = 0;
    5. };
    6. class Benz :public Car
    7. {
    8. public:
    9. virtual void Drive()
    10. {
    11. cout << "Benz-舒适" << endl;
    12. }
    13. }
    14. void Test()
    15. {
    16. Car* pBenz = new Benz;
    17. pBenz->Drive();
    18. }

    1、假设A为抽象类,下列声明( D)是正确的。

    A、 int fun(A);        B、 A Obj;        
    C、 A fun(int);        D、 A *p; 

    抽象类不能初始化,不能当做返回值,不能当做参数,可以作为指针变量,因为此时还没有初始化。

    总结:

    1.抽象类只能用作其他类的基类, 不能定义抽象类的对象。

    2.抽象类 不能用于参数类型、函数返回值或显示转换的类型

    3.抽象类 可以定义抽象类的指针和引用,此指针可以指向它的派生类,进而实现多态性。

    1.1 接口继承和实现继承

    普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实 现。

    虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口,即函数的缺省参数还是按照基类的来。所以如果不实现多态,不要把函数定义成虚函数。

    2. 多态的原理

    2.1 虚表

    一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。

    虚函数表,本质是函数指针数组。在编译阶段生成。

    1. class Base
    2. {
    3. public:
    4. virtual void Func1()
    5. {
    6. cout << "Func1()" << endl;
    7. }
    8. private:
    9. int _b = 1;
    10. };
    11. int main()
    12. {
    13. Base b;
    14. int size=sizeof(b);
    15. return 0;
    16. }

     除了_b成员,还多一个__vfptr放在对象的前面。对象中的这个指针我们叫做虚函数表指针。

    2.2 父类和子类中的虚表

    1. #include
    2. using namespace std;
    3. class Person
    4. {
    5. public:
    6. virtual void Buyticket()
    7. {
    8. cout << "买票--全价" << endl;
    9. }
    10. virtual void Func1()
    11. {
    12. cout<<"Person::Func1()"<
    13. }
    14. private:
    15. int _age = 29;
    16. };
    17. class Student :public Person
    18. {
    19. public:
    20. virtual void Buyticket()
    21. {
    22. cout << "买票--半价" << endl;
    23. }
    24. virtual void Fun2()//新增虚函数 Func1并未重写
    25. {
    26. cout<<"Studen::Func2()"<
    27. }
    28. private:
    29. int _num = 2089060001;
    30. };
    31. void fun1(Person& p)
    32. {
    33. p.Buyticket();
    34. }
    35. void fun2(Person p)
    36. {
    37. p.Buyticket();
    38. }
    39. int main()
    40. {
    41. Person a;
    42. Student b;
    43. fun1(a);
    44. fun1(b);
    45. fun2(a);
    46. fun2(b);
    47. return 0;
    48. }

    父类中的虚表的元素指向的是 Person::Buyticket

    子类继承下来的虚表中元素指向的是Studen::Buyticket

    基类对象和派生类对象虚表是不一样的,这里我们发现Buyticket完成了重写,所以b的虚表中存的是重写的Studen::Buyticket,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。

    注意:

    只要是虚函数,就要放入虚表

    虚表存的是虚函数指针,不是虚函数。虚函数存放于公共代码段。

    2.2.1 如何查看派生类中新增的虚函数

    在上述代码中,Func1()首先是一个虚函数,但是子类没有对Fun1()进行重写,但子类中新增了一个虚函数Func2(),但打开监视窗口却在虚表中找不到Func1()这个虚函数对应的元素。

    1. typedef void(*VFPTR)();//将返回类型为void的 无参的 函数指针重命名为VFPTR
    2. void PrintVFTable(VFPTR table[])
    3. {
    4. for(size_t i=0;table[i]!=nullptr;i++)
    5. {
    6. printf("vft[%d]:%p",i,table[i]);
    7. VFPTR pf=table[i];
    8. pf();//使用函数指针 调用函数
    9. }
    10. }
    11. int main()
    12. {
    13. Person A;
    14. Student B;
    15. PrintVFTable((VFPTR*)*(int*)&A);
    16. PrintVFTable((VFPTR*)*(int*)&B);
    17. return 0;
    18. }

     

     发现在子类的虚表中存在这个虚函数。

    在VS2019版本中没有重写的虚函数,也可以通过监视窗口看到了

    2.2.2 单继承中的虚函数表

    B继承A类,B中新增虚函数,会放在B类的虚表里面。B中新增的非虚函数,不会放到虚表里面。

    派生类的虚表生成:         

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

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

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

    2.2.3 多继承中的虚函数表

    多继承的时候会有多张虚表。

    假设D类先继承B1,然后继承B2,B1和B2基类均包含虚函数,D类对B1和B2基类的虚函数重写了,并且D类增加了新的虚函数,则:D类对象有两个虚表,D类新增加的虚函数放在第一张虚表最后。

    多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表里。

    2.3动态绑定和静态绑定

    静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态。比如函数重载。

    动态绑定又称为后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。

    3. 多态函数调用的区别

    构成多态的调用,运行时到指向对象虚表中找调用虚函数地址,所以p指向谁就调用谁的虚函数。

    不构成多态的调用,则为普通调用,编译时确定调用函数地址。

    所以如果是普通对象调用虚函数和普通函数,是一样快的。如果是指针 对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函 数表中去查找。

  • 相关阅读:
    MySQL索引
    学生成绩管理系统(C语言有结构体实现)
    排查服务器异常流量保姆级教程
    java8(一)Stream API
    MQ - 25 基础功能:Topic、分区、消费分组的设计
    DockerFile解析
    IOday5
    2310C++子类已调用基类构造器
    部署Kubernetes(k8s)时,为什么要关闭swap、selinux、firewall 防火墙?
    D. AND, OR and square sum(二进制位+贪心)
  • 原文地址:https://blog.csdn.net/qq_68741368/article/details/127440098