IT评测·应用市场-qidao123.com技术社区

标题: Cpp多态机制的深入理解(20) [打印本页]

作者: 灌篮少年    时间: 2024-11-1 19:55
标题: Cpp多态机制的深入理解(20)

前言

  多态也是三大面向对象语言的特性之一,同时我也觉得他也蛮有意思的
  与封装“一个方法,多个接口”差别的是,多态可以实现 “一个接口,多种方法
  调用同名函数时,可以根据差别的对象(父类对象或子类对象)调用属于自己的函数,实现差别的方法,因此 多态 的实现依赖于 继承

一、多态的概念

  在使用多态的代码中,差别对象完成同一件事会产生差别的效果
  比如在购买高铁票时,普通人原价,门生半价,而军人可以优先购票,对于 购票 这一雷同的动作,需要 根据差别的对象提供差别的方法
二、多态的定义与实现

两个必要条件


虚函数

  被virtual修饰的类成员函数称为虚函数
   全局虚函数没有意义,由于虚函数是为多态而用的
  

虚函数的重写

  虚函数的重写(覆盖):派生类中有一个跟基类完全雷同的虚函数(即派生类虚函数与基类虚函数的返回值范例、函数名字、参数列表完全雷同(范例雷同即可)),称子类的虚函数重写了基类的虚函数
  1. // 基类
  2. class Person
  3. {
  4. public:
  5.     // 虚函数
  6.     virtual void BuyTicket() { cout << "买票-全价" << endl; }
  7. };
  8. // 派生类
  9. class Student : public Person
  10. {
  11. public:
  12.     // 虚函数重写
  13.     virtual void BuyTicket() { cout << "买票-半价" << endl; }
  14. };
  15. // 三种函数实现
  16. // 引用
  17. void Func(Person& p)
  18. {
  19.         p.BuyTicket();
  20. }
  21. // 指针
  22. //void Func(Person* p)
  23. //{
  24. //        p->BuyTicket();
  25. //}
  26. // 非引用指针,调用父类
  27. //void Func(Person p)
  28. //{
  29. //        p.BuyTicket();
  30. //}
复制代码
测试效果:

重写的三个例外

  派生类重写基类虚函数时,与基类虚函数返回值范例差别。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变
   这个相识一下就行,现实我感觉挺没啥用处的
假如你也有这种感觉,鼓励你致电老本,去好好批斗他!
  1. class A {};
  2. class B : public A {};
  3. class Person
  4. {
  5. public:
  6.         // 协变 返回值可以是父子类对象指针或引用
  7.         //virtual A* BuyTicket() // 返回值是父类指针
  8.         virtual Person* BuyTicket()
  9.         {
  10.                 cout << "Person-> 买票-全价" << endl;
  11.                 return nullptr;
  12.         }
  13. };
  14. class Student : public Person
  15. {
  16. public:
  17.         //virtual B* BuyTicket()// 返回值是子类指针
  18.         virtual Student* BuyTicket()
  19.         {
  20.                 cout << "Student-> 买票-半价" << endl;
  21.                 return nullptr;
  22.         }
  23. };
复制代码
  假如基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,固然基类与派生类析构函数名字差别。固然函数名不雷同,看起来违背了重写的规则,实在否则,这里可以理解为编译器对析构函数的名称做了特别处理惩罚,编译后析构函数的名称统一处理惩罚成destructor
  1. class Person
  2. {
  3. public:
  4.         // 析构函数名不同,构成重写,编译器将析构函数名字统一处理成destructor
  5.         virtual ~Person()
  6.         {
  7.                 cout << "~Person()" << endl;
  8.         }
  9. };
  10. class Student : public Person
  11. {
  12. public:
  13.     virtual ~Student()
  14.         {
  15.                 cout << "delete[]" << _ptr << endl;
  16.                
  17.                 delete[] _ptr;
  18.                 cout << "~Student()" << endl;
  19.         }
  20. private:
  21.         int* _ptr = new int[10];
  22. };
  23. void Func(Person& p)
  24. {
  25.         p.BuyTicket();
  26. }
  27. int main()
  28. {
  29.         // 正常情况调用析构没有问题
  30.         //Person p;
  31.         //Student s;
  32.         //Func(p);
  33.         //Func(s);
  34.         // 派生类有动态开辟的内存,需要调用多态
  35.         // 指向谁调用谁
  36.         Person* p1 = new Person;
  37.         Person* p2 = new Student;
  38.         delete p1;
  39.         delete p2;
  40.         return 0;
  41. }
复制代码

  1. class Person
  2. {
  3. public:
  4.         virtual ~Person()
  5.         {
  6.                 cout << "~Person()" << endl;
  7.         }
  8. };
  9. class Student : public Person
  10. {
  11. public:
  12.     // 派生类virtual关键字省略
  13.         ~Student()
  14.         {
  15.                 cout << "~Student()" << endl;
  16.         }
  17. };
复制代码

override 和 final

  C++对函数重写的要求比较严酷,但是有些情况下由于疏忽,大概会导致函数名字母次序写反而无法构成重载
  1. // final 修饰虚函数,不能重写
  2. class Car
  3. {
  4. public:
  5.         // 加了final关键字,虚函数不能被重写
  6.         virtual void Drive() final {}
  7. };
  8. class Benz :public Car
  9. {
  10. public:
  11.         virtual void Drive() { cout << "Benz-舒适" << endl; }
  12. };
  13. int main()
  14. {
  15.         Benz b;
  16.         return 0;
  17. }
复制代码

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



三、抽象类

概念

  在虚函数的背面写上 = 0 ,则这个函数为纯虚函数。包罗纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才气实例化出对象。纯虚函数规范了派生类必须重写,别的纯虚函数更表现出了接口继承
  1. class Car
  2. {
  3. public:
  4.         // 纯虚函数 强制派生类重写虚函数
  5.         virtual void Drive() = 0;
  6. };
  7. int main()
  8. {
  9.         Car c;
  10.         return 0;
  11. }
复制代码

  1. class Car
  2. {
  3. public:
  4.         // 纯虚函数 强制派生类重写虚函数
  5.         virtual void Drive() = 0;
  6. };
  7. class Benz :public Car
  8. {
  9. public:
  10.         virtual void Drive()
  11.         {
  12.                 cout << "Benz-舒适" << endl;
  13.         }
  14. };
  15. class BMW :public Car
  16. {
  17. public:
  18.         virtual void Drive()
  19.         {
  20.                 cout << "BMW-操控" << endl;
  21.         }
  22. };
  23. int main()
  24. {
  25.         // Car c;
  26.         Benz b1;
  27.         BMW b2;
  28.         // 基类可以定义指针 指向谁调用谁
  29.         Car* ptr1 = &b1;
  30.         Car* ptr2 = &b2;
  31.         ptr1->Drive();
  32.         ptr2->Drive();
  33.        
  34.         return 0;
  35. }
复制代码

接口继承和实现继承

  普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目标是为了重写,达成多态,继承的是接口。以是假如不实现多态,不要把函数定义成虚函数
四、多态的原理

  在讲解原理之前,不如我们先来看这么一段神奇代码
  1. #include <iostream>
  2. using namespace std;
  3. class Test
  4. {
  5.         virtual void func() {};
  6. };
  7. int main()
  8. {
  9.         Test t;        //创建一个对象
  10.         cout << "Test sizeof(): " << sizeof(t) << endl;
  11.        
  12.         return 0;
  13. }
复制代码
  大概你会觉得没有对象,会觉得是0,但是你忽然想起了之前讲过的空类也占内存空间,你大概会想是不是1
  但是实在都错了,真相是4/8(取决于你的体系是32位照旧64位),大概我这么一说,你也猜到了实在有一个隐藏变量,且范例是指针范例
   实在,就是靠着这个虚表指针和虚表实现了多态
  虚表和虚表指针

  在 vs 的监视窗口中,可以看到涉及虚函数类的对象中都有属性 __vfptr(虚表指针),可以通过虚表指针所指向的地址,找到对应的虚表
  虚函数表中存储的是虚函数指针,可以在调用函数时根据差别的地址调用差别的方法
   大概有点混,有三个“虚”,大家别被整虚了!
虚表指针指向虚表,虚表里面存放着虚函数指针,以是虚表的本质实在是个函数指针数组
    接下来我会给出一段代码,在该代码中父类 Person 有两个虚函数(func3 不是虚函数),子类 Student 重写了 func1 这个虚函数,同时新增了一个 func4 虚函数
  1. #include <iostream>
  2. using namespace std;
  3. class Person
  4. {
  5. public:
  6.         virtual void func1() { cout << "Person::fun1()" << endl; };
  7.         virtual void func2() { cout << "Person::fun2()" << endl; };
  8.         void func3() { cout << "Person::fun3()" << endl; };        //fun3 不是虚函数
  9. };
  10. class Student : public Person
  11. {
  12. public:
  13.         virtual void func1() { cout << "Student::fun1()" << endl; };
  14.         virtual void func4() { cout << "Student::fun4()" << endl; };
  15. };
  16. int main()
  17. {
  18.         Person p;
  19.         Student s;
  20.        
  21.     return 0;
  22. }
复制代码

  1. //打印虚表
  2. typedef void(*VF_T)();
  3. void PrintVFTable(VF_T table[])        //也可以将参数类型设为 VF_T*
  4. {
  5.         //vs中在虚表的结尾处添加了 nullptr
  6.         //如果运行失败,可以尝试清理解决方案重新编译
  7.         int i = 0;
  8.         while (table[i])
  9.         {
  10.                 printf("[%d]:%p->", i, table[i]);
  11.                 VF_T f = table[i];
  12.                 f();        //调用函数,相当于 func()
  13.                 i++;
  14.         }
  15.         cout << endl;
  16. }
  17. int main()
  18. {
  19.         //提取出虚表指针,传递给打印函数
  20.         Person p;
  21.         Student s;
  22.         //第一种方式:强转为虚函数地址(4字节)
  23.         PrintVFTable((VF_T*)(*(int*)&p));
  24.         PrintVFTable((VF_T*)(*(int*)&s));
  25.         return 0;
  26. }
复制代码
子类重写后的虚函数地址与父类差别

   由于平台差别指针巨细差别,因此上述传递参数的方式(VF_T * )( * (int * )&p 具有肯定的范围性
假设在 64 位平台下,需要更改为 (VF_T * )( * (long long * )&p
  1. //64 位平台下指针大小为 8字节
  2. PrintVFTable((VF_T*)(*(long long*)&p));
  3. PrintVFTable((VF_T*)(*(long long*)&s));
复制代码
  除此之外还可以间接将虚表指针转为 VF_T* 范例进行参数传递
  1. //同时适用于 32位 和 64位 平台
  2. PrintVFTable(*(VF_T**)&p);
  3. PrintVFTable(*(VF_T**)&s);
复制代码
传递参数时的范例转换路径

  不能直接写成 PrintVFTable((VF_T*)&p);,由于此时取的是整个虚表区域的首地址地址,无法定位我们所需要虚表的首地址,打印时会堕落
  综上所述,虚表是真实存在的,只要当前类中涉及了虚函数,那么编译器就会为其构建相应的虚表体系
   虚表是在 编译 阶段生成的
虚表指针是在构造函数的 初始化列表 中初始化的
虚表一样平常存储在 常量区(代码段),有的平台中大概存储在 静态区(数据段)
  1. int main()
  2. {
  3.         //验证虚表的存储位置
  4.         Person p;
  5.         Student s;
  6.         int a = 10;        //栈
  7.         int* b = new int;        //堆
  8.         static int c = 0;        //静态区(数据段)
  9.         const char* d = "xxx";        //常量区(代码段)
  10.         printf("a-栈地址:%p\n", &a);
  11.         printf("b-堆地址:%p\n", b);
  12.         printf("c-静态区地址:%p\n", &c);
  13.         printf("d-常量区地址:%p\n", d);
  14.         printf("p 对象虚表地址:%p\n", *(VF_T**)&p);
  15.         printf("s 对象虚表地址:%p\n", *(VF_T**)&s);
  16.         return 0;
  17. }
复制代码

   显然,虚表地址与常量区的地址非常接近,因此可以推测 虚表 位于常量区中,由于它需要被同一类中的差别对象共享,同时不能被修改(如同代码一样)
  虚函数调用过程

综上,我们可以大概想象出多态的原理了:
   也就是说,父类和子类的虚表实在是不一样的,在构成重写的条件下!
这就是多态!
  1. int main()
  2. {
  3.         Person* p1 = new Person();
  4.         Person* p2 = new Student();
  5.         p1->func1();
  6.         p2->func1();
  7.         delete p1;
  8.         delete p2;
  9.         return 0;
  10. }
复制代码
通过汇编代码来看的话:


动态绑定与静态绑定

  实在我们想一想,函数重载某种程度上也是一种多态,也是一个函数面临差别对象的时候有差别的效果,但是差别的是,重载在编译的时候就确定了待调用函数的地址,而动态绑定的代码,待调用地址存放在 eax 中,不确定

五、那…那单继承甚至多继承呢?

  坦率说,这很麻烦,我也不敢说我很懂,于是我在这里贴两篇文章,大家自行参阅吧!
《C++虚函数表解析》
《C++对象的内存布局》

总结

  我们终于学完三大面向对象特性了,坦率说,多态照旧蛮困难的,但是,我们难度的最高峰再过几篇就要来了,怕不怕!

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。




欢迎光临 IT评测·应用市场-qidao123.com技术社区 (https://dis.qidao123.com/) Powered by Discuz! X3.4