C++多态详解

打印 上一主题 下一主题

主题 973|帖子 973|积分 2919

多态的使用

概念

   多态分为静态多态(编译时多态)和动态多态(运行时多态)。
静态多态是函数重载和函数模版,传差别类型的参数可以调差别的函数,通过参数的差别到达了多种形态。静态是因为实参传给形参的参数匹配是在编译时完成的
动态多态是传差别的对象完成差别的活动(函数),到达多种形态,比如买票这个活动,普通人买票时全价,学生买票是半价,武士买票是优先买票。

  多态的构成条件和使用



  • 多态的构成条件:
    多态是一个继续关系下的类对象,调用同一个函数,产生差别的活动
  • 虚函数
  1. class Person
  2. {
  3. public:
  4.         // 虚函数
  5.         virtual void Func()
  6.         {
  7.                 cout << "买票" << endl;
  8.         }
  9. };
复制代码


  • 虚函数的重写/覆盖
    重写/覆盖的条件:子类必须有一个和父类完全雷同的虚函数(返回值类型,函数名,参数列表的类型完全雷同
    重写是重写了实现
  • 实现多态的两个紧张条件:

  • 必须是父类的指针或引用调用虚函数
  • 被调用的函数必须是虚函数,且子类必须对父类虚函数完成重写/覆盖
满意多态指向谁调用谁,不满意多态看函数参数的类型是什么就调用什么
  1. class Person
  2. {
  3. public:
  4.         // 条件3
  5.         virtual void BuyTicket() { cout << "买票-全价" << endl; }
  6. };
  7. class Student : public Person
  8. {
  9. public:
  10.     // 条件3
  11.         virtual void BuyTicket() { cout << "买票-打折" << endl; }
  12. };
  13. // 不满足多态的情况下指向谁调用谁,ptr指向Person类都调用Person类
  14. // 满足多态就和指向对象的类型有关是哪个对象类型就调用哪个类
  15. void Func(Person* ptr)// 条件1
  16. {
  17.         // 这⾥可以看到虽然都是Person指针Ptr在调⽤BuyTicket
  18.         // 但是跟ptr没关系,而是由ptr指向的对象决定的。
  19.         ptr->BuyTicket();// 条件2
  20. }
  21. int main()
  22. {
  23.         Person ps;
  24.         Student st;
  25.         Func(&ps);
  26.         Func(&st);
  27.         return 0;
  28. }
复制代码



  • 注意:在重写基类虚函数时,派⽣类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继续后基类的虚函数被继续下来了在派生类仍旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用,许多题有这样的坑。
子类的子类也可以不加virtual,只要父类加了virtual,就能继续下来
一道选择题

B

重写的本质是重写虚函数的实现部分
绝不重新定义继续而来的缺省值,保留原来的缺省值

  1. class A
  2. {
  3. public:
  4.         virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }
  5.         virtual void test() { func(); }
  6. };
  7. class B : public A
  8. {
  9. public:
  10.         void func(int val = 0) { std::cout << "B->" << val << std::endl; }
  11. };
  12. int main(int argc, char* argv[])
  13. {
  14.         B* p = new B;
  15.         p->test();
  16.         // B->1
  17.         p->func();
  18.         // B->0
  19.         //  没有构成多态,是子类的指针
  20.         //  在自己的域里找,找不到到继承的域里找
  21.        
  22.         return 0;
  23. }
复制代码
协变(了解)



  • 子类重写父类虚函数时,与父类虚函数返回值类型差别。即父类虚函数返回父类对象的指针大概引
    子类虚函数返回子类对象的指针大概引用时,称为协变。
  1. class A {};
  2. class B : public A {};
  3. // 返回的类必须也必须是子类继承父类的关系
  4. // 可以是不同的类或不是自己的类
  5. // 可以是自己的类
  6. class Person
  7. {
  8. public:
  9.         /*virtual A* BuyTicket()
  10.         {
  11.                 cout << "买票-全价" << endl;
  12.                 return nullptr;
  13.         }*/
  14.         virtual Person* BuyTicket()
  15.         {
  16.                 cout << "买票-全价" << endl;
  17.                 return nullptr;
  18.         }
  19. };
  20. class Student : public Person
  21. {
  22. public:
  23.         /*virtual B* BuyTicket()
  24.         {
  25.                 cout << "买票-打折" << endl;
  26.                 return nullptr;
  27.         }*/
  28.         virtual Student* BuyTicket()
  29.         {
  30.                 cout << "买票-打折" << endl;
  31.                 return nullptr;
  32.         }
  33. };
  34. void Func(Person* ptr)
  35. {
  36.         ptr->BuyTicket();
  37. }
  38. int main()
  39. {
  40.         Person ps;
  41.         Student st;
  42.         Func(&ps);
  43.         Func(&st);
  44.         return 0;
  45. }
复制代码
析构函数的重写



  • 父类的析构函数为虚函数此时子类析构函数只要定义,无论是否加virtual关键字,都与父类的析构函数构成重写,虽然父类与子类析构函数名字差别看起来不符合重写的规则,现实上编译器对析构函数的名称做了特殊处置惩罚,编译后析构函数的名称同一处置惩罚成destructor,所以父类的析构函数加了vialtual修饰,子类的析构函数就构成重写。
  1. // 析构函数的名字会被特殊处理为destructor
  2. // 变相的符合了3重
  3. class A
  4. {
  5. public:
  6.         virtual ~A()
  7.         {
  8.                 cout << "~A()" << endl;
  9.         }
  10. };
  11. class B : public A
  12. {
  13. public:
  14.         ~B()
  15.         {
  16.                 cout << "~B()->delete:" << _p << endl;
  17.                 delete _p;
  18.         }
  19. protected:
  20.         int* _p = new int[10];
  21. };
  22. // 只有派生类Student的析构函数重写了Person的析构函数,下⾯的delete对象调⽤析构函数,才能
  23. // 构成多态,才能保证p1和p2指向的对象正确的调用析构函数。
  24. int main()
  25. {
  26.         // 父类的指针指向子类,根据指向的对象调用析构函数
  27.         // 不构成多态,会根据父类的类型调用两次A类
  28.         // 不构成多态存在内存泄漏的问题
  29.         A* p1 = new A;
  30.         A* p2 = new B;
  31.         // p1->destructor() + operator delete(free)
  32.         delete p1;
  33.         delete p2;
  34.         return 0;
  35. }
复制代码
为什么会调用两次A类的析构?
因为B类继续了A类,B类的析构竣事会自动调用A类的析构

所以继续那边构成~A构成函数隐藏,为什么构成函数隐藏,因为处置惩罚后函数名雷同(destructor),为什么函数名雷同,因为多态这里为了防止内存走漏,为了构成虚函数,指向的对象差别调用A和B类的析构
  1. class A
  2. {
  3. public:
  4.         ~A()
  5.         {
  6.                 cout << "~A()" << endl;
  7.         }
  8. };
  9. class B : public A
  10. {
  11. public:
  12.         ~B()
  13.         {
  14.                 cout << "~B()->delete:" << _p << endl;
  15.                 delete _p;
  16.         }
  17. protected:
  18.         int* _p = new int[10];
  19. };
复制代码

override和final关键字



  • C++11新增的关键字,override用来检查是否构成重写,因为编译器在编译阶段是检查不出来的,在运行时才能检查出来,加override是为了在编译时就检查出来
  1. class Car
  2. {
  3. public:
  4.         virtual void Dirve()
  5.         {}
  6. };
  7. class Benz :public Car
  8. {
  9. public:
  10.         virtual void Drive() override { cout << "Benz-舒适" << endl; }
  11. };
复制代码


  • 假如我们不想在子类重写这个虚函数,用final修饰,不能被重写
  1. class Car
  2. {
  3. public:
  4. virtual void Drive() final {}
  5. };
  6. class Benz :public Car
  7. {
  8. public:
  9. virtual void Drive() { cout << "Benz-舒适" << endl; }
  10. };
复制代码


  • 继续那边的final,作用是修饰为最终类,不能被继续
  1. class Car final
  2. {
  3. public:
  4. virtual void Drive() final {}
  5. };
复制代码
重载/重写/隐藏的对比


纯虚函数和抽象类



  • 在虚函数后面写上=0就是纯虚函数,纯虚函数不需要定义实现(现实上没有什么意义因为要被子类重写,重写之后父类的实现就没用了,但是语法上支持定义实现),只要声明即可包罗纯虚函数的类叫做抽象类抽象类不能被实例化出对象假如子类继续后不重写纯虚函数,那么子类也是抽象类。纯虚函数在某种程度上欺压了子类重写虚函数,因为不重写实例化不出对象。
  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. class BMW :public Car
  15. {
  16. public:
  17.         virtual void Drive()
  18.         {
  19.                 cout << "BMW-操控" << endl;
  20.         }
  21. };
  22. int main()
  23. {
  24.         // 父类无法实例化出对象
  25.         // 但是可以用指针和引用来实现多态
  26.         // Car c;
  27.         Car* pBenz = new Benz;
  28.         pBenz->Drive();
  29.         Car* pBMW = new BMW;
  30.         pBMW->Drive();
  31.         return 0;
  32. }
复制代码
多态是一种相对的概念,B既可以是父类也可以是子类

多态的原理

虚函数表指针

有几个虚函数就有几个虚函数表指针
虚函数表指针指向一个函数指针数组

指向谁就调用谁,指向哪个对象就到那个对象的虚函数表中找到对应的函数,进行调用
  1. class Person
  2. {
  3. public:
  4.         virtual void BuyTicket()
  5.         {
  6.                 cout << "买票-全价" << endl;
  7.         }
  8. private:
  9.         string _name;
  10. };
  11. class Student : public Person
  12. {
  13. public:
  14.         virtual void BuyTicket()
  15.         {
  16.                 cout << "买票-打折" << endl;
  17.         }
  18. private:
  19.         string _id;
  20. };
  21. class Soldier : public Person
  22. {
  23. public:
  24.         virtual void BuyTicket()
  25.         {
  26.                 cout << "买票-优先" << endl;
  27.         }
  28. private:
  29.         string _codename;
  30. };
  31. void Func(Person* ptr)
  32. {
  33.         // 这⾥可以看到虽然都是Person指针Ptr在调⽤BuyTicket
  34.         // 但是跟ptr没关系,⽽是由ptr指向的对象决定的。
  35.         ptr->BuyTicket();
  36. }
  37. int main()
  38. {
  39.         // 其次多态不仅仅发生在子类对象之间,多个子类继承父类,重写虚函数后
  40.         // 多态也会发生在多个子类之间。
  41.         Person ps;
  42.         Student st;
  43.         Soldier sr;
  44.         Func(&ps);
  45.         Func(&st);
  46.         Func(&sr);
  47.         return 0;
  48. }
复制代码

动态绑定和静态绑定


  • 动态绑定是根据指向的对象决定(满意多态)
    满意多态条件的函数调⽤是在运行时绑定,也就是在运行时到指向对象的虚函数表中找到调用函数
    的地址,也就做动态绑定。运行时去找
  • 静态绑定是根据类型决定(不满意多态)
    对不满意多态条件(指针大概引用+调用虚函数)的函数调用是在编译时绑定,也就是编译时确定调用函数的地址,叫做静态绑定。静态绑定的服从更高一点

虚函数表


  • 有几个虚函数就放几个虚函数的地址到虚函数表中
  • 同类型的对象虚函数表是一样的,差别类型对象虚表各自独立

  • 子类由两部分构成,继续下来的父类和本身的成员,⼀般情况下,继续下来的父类中有虚函数表
    指针,本身就不会再天生虚函数表指针
    。但是要注意的这⾥继续下来的父类部分虚函数表指针和父
    类对象的虚函数表指针不是同⼀个,就像父类对象的成员和子类对象中的父类对象成员也独立的。
  • 子类中重写的父类的虚函数,原来是把父类的虚函数的地址拷贝了下来,但是重写了,就覆盖了父类的虚函数的地址
  • 子类的虚函数表中包罗三个部分:
    (1)父类的虚函数地址
    (2)子类重写的虚函数地址完成覆盖
    (3)子类本身的虚函数地址

6. 虚函数表本质是⼀个存虚函数指针的指针数组(虚函数指针数组),⼀般情况这个数组最后面放了⼀个0x00000000标记。vs有规定,gcc没有规定
7. 虚函数存在哪的?虚函数和普通函数⼀样的,编译好后是⼀段指令,都是存在代码段的,只是虚函数的地址(指针)又存到了虚表中。
8. 虚函数表存在哪的?这个问题严格说并没有标准答案C++标准并没有规定,我们写下面的代码可以对比验证⼀下。vs下是存在代码段(常量区)
  1. class Base
  2. {
  3. public:
  4.         virtual void func1() { cout << "Base::func1" << endl; }
  5.         virtual void func2() { cout << "Base::func2" << endl; }
  6.         void func5() { cout << "Base::func5" << endl; }
  7. protected:
  8.         int a = 1;
  9. };
  10. class Derive : public Base
  11. {
  12. public:
  13.         // 重写基类的func1
  14.         virtual void func1() { cout << "Derive::func1" << endl; }
  15.         virtual void func3() { cout << "Derive::func1" << endl; }
  16.         void func4() { cout << "Derive::func4" << endl; }
  17. protected:
  18.         int b = 2;
  19. };
  20. //int main()
  21. //{
  22. //        Base b;
  23. //        Derive d;
  24. //
  25. //        return 0;
  26. //}
  27. int main()
  28. {
  29.         int i = 0;
  30.         static int j = 1;
  31.         int* p1 = new int;
  32.         const char* p2 = "xxxxxxxx";
  33.         printf("栈:%p\n", &i);
  34.         printf("静态区:%p\n", &j);
  35.         printf("堆:%p\n", p1);
  36.         printf("常量区:%p\n", p2);
  37.         Base b;
  38.         Derive d;
  39.         Base* p3 = &b;
  40.         Derive* p4 = &d;
  41.         // %p对象就是地址
  42.         printf("Person虚表地址:%p\n", *(int*)p3);
  43.         printf("Student虚表地址:%p\n", *(int*)p4);
  44.         printf("虚函数地址:%p\n", &Base::func1);
  45.         printf("普通函数地址:%p\n", &Base::func5);
  46.         return 0;
  47. }
复制代码


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

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

前进之路

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表