当前位置:   article > 正文

C++技能进阶指南——多态语法剖析

C++技能进阶指南——多态语法剖析

        前言:多态是面向对象的三大特性之一。顾名思义, 多态就是多种状态。 那么是什么的多种状态呢? 这里的可能有很多。比如我们去买火车票, 有普通票, 学生票; 又比如我们去旅游, 有儿童票, 有成人票等等。 这些都是多态的例子。 具体转化为我们的编程思想就是:让不同类型的对象去完成相同的事, 这就是多态

        本篇内容主要讲述多态, 多为语法方面的知识点。 适合已经学完继承的友友们观看。

        

目录

一、多态的相关概念

1.1虚函数

1.2虚函数的重写

1.3虚函数重写的两个例外

 1.4override 和 final 的使用

二、重载、重写、隐藏(重定义)的区别

三、如何构成多态

四、抽象类

五、普通继承和接口继承

六、静态绑定和动态绑定


具体什么是多态在前言中已经提到, 正文部分不做赘述。

一、多态的相关概念

1.1虚函数

        被virtual关键字修饰的成员函数叫做虚函数。 例如:

  1. //A位基类
  2. class A
  3. {
  4. public:
  5. virtual void func() //定义一个虚函数
  6. {
  7. cout << "Afunc()" << endl;
  8. }
  9. };

         需要注意的是, 对于构造函数和析构函数来说。 析构函数可以是虚函数, 但是构造函数不可以是虚函数。 

具体原因如下:(建议看完整篇文章和总结虚函数表机制——c++多态底层原理-CSDN博客​​​​​​ 之后再来看下面这段解释):

        首先:通过之前的学习, 我们知道了, 虚函数的地址是存在虚函数表里面的。 想要调用对应的虚函数, 我们需要先去虚函数表中寻找对应虚函数的地址。 但是虚函数表是在构造函数的初始化列表初始化的。如果构造函数是虚函数, 那么调用构造函数的时候就找不到。 所以构造函数没办法是虚函数。

1.2虚函数的重写

        虚函数的重写就是: 在派生类当中, 有一个和基类中某一个虚函数函数头的虚函数(函数头就是:函数的返回值, 函数名, 函数的参数列表)。 这个时候就会构成虚函数的重写, 即 子类重写了基类的虚函数

  1. //A位基类
  2. class A
  3. {
  4. public:
  5. virtual void func()
  6. {
  7. cout << "Afunc()" << endl;
  8. }
  9. };
  10. //B类继承A类
  11. class B : public A
  12. {
  13. public:
  14. //重写A类的func函数
  15. virtual void func() //注意, 这里的virtual可以不写, 因为编译器默认这里是加了virtual的
  16. {
  17. cout << "Bfunc()" << endl;
  18. }
  19. };

        需要注意的是, 上图中派生类的func可以不加virtual, 因为基类的func是虚函数, 编译器会默认派生类中和他函数头相同的函数也是虚函数。 

1.3虚函数重写的两个例外

        协变:派生类在重写基类的虚函数的时候, 与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用, 派生类虚函数返回派生类对象的指针或者引用的时候, 成为协变。

  1. //A位基类
  2. class A
  3. {
  4. public:
  5. virtual A* func()
  6. {
  7. cout << "Afunc()" << endl;
  8. }
  9. };
  10. //B类继承A类
  11. class B : public A
  12. {
  13. public:
  14. //重写A类的func函数
  15. virtual B* func()
  16. {
  17. cout << "Bfunc()" << endl;
  18. }
  19. };
  20. //C类继承A类
  21. class C : public A
  22. {
  23. virtual C* func()
  24. {
  25. cout << "Cfunc" << endl;
  26. }
  27. };

    

        析构函数的重写: 基类析构函数如果加了virtual, 那么说明基类的析构函数为虚函数。 这个时候如果派生类的析构函数也就变成了虚函数。 那么成不成为虚函数对于析构函数来说有什么不同呢?

        首先我们需要知道的是, 在一个普通的类之中, 编译器其实将析构函数统一处理成为了destructor。

        然后, 对于一个派生类来说, 如果它的析构函数不是虚函数。 当我们使用父类的指针构成多态时, 只会析构派生类的一部分:

  1. //A位基类
  2. class A
  3. {
  4. public:
  5. virtual A* func()
  6. {
  7. cout << "Afunc()" << endl;
  8. }
  9. //其他动态内存分配的空间
  10. //int* ...
  11. //double* ...
  12. };
  13. //B类继承A类
  14. class B : public A
  15. {
  16. public:
  17. //重写A类的func函数
  18. virtual B* func()
  19. {
  20. cout << "Bfunc()" << endl;
  21. }
  22. //其他动态内存分配的空间
  23. //int* ...
  24. //double* ...
  25. };
  26. void test_func(A* p)
  27. {
  28. p->func();
  29. }
  30. int main()
  31. {
  32. C c;
  33. A* p = nullptr;
  34. p = &c;
  35. delete p;
  36. return 0;
  37. }

         如上图, 假如delete p, 那么就只能释放属于C类自己的那一部分。那么属于A类的那一部分将得不到释放。 

        但是, 如果我们对A类的析构函数使用虚函数。 那么派生类的析构函数也变成了虚函数, 这个时候如果再形成多态。delete p就能将A类和C类都释放掉。

  1. //A位基类
  2. class A
  3. {
  4. public:
  5. virtual A* func()
  6. {
  7. cout << "Afunc()" << endl;
  8. }
  9. virtual ~A()
  10. {}
  11. };
  12. //B类继承A类
  13. class B : public A
  14. {
  15. public:
  16. //重写A类的func函数
  17. virtual B* func()
  18. {
  19. cout << "Bfunc()" << endl;
  20. }
  21. virtual ~B()
  22. {}
  23. };

 1.4override 和 final 的使用

先谈override, override是用来检验某个虚函数是否构成了重写。如果没有构成重写, 那么编译器就会报错。

        如下为构成重写:

  1. //A位基类
  2. class A
  3. {
  4. public:
  5. virtual void func()
  6. {
  7. cout << "Afunc()" << endl;
  8. }
  9. };
  10. //B类继承A类
  11. class B : public A
  12. {
  13. public:
  14. //重写A类的func函数
  15. virtual void func() override
  16. {
  17. cout << "Bfunc()" << endl;
  18. }
  19. };

如下为没有构成重写:

  1. //A位基类
  2. class A
  3. {
  4. public:
  5. void func()
  6. {
  7. cout << "Afunc()" << endl;
  8. }
  9. };
  10. //B类继承A类
  11. class B : public A
  12. {
  13. public:
  14. //重写A类的func函数
  15. virtual void func() override
  16. {
  17. cout << "Bfunc()" << endl;
  18. }
  19. };

二、重载、重写、隐藏(重定义)的区别

  • 重载: 函数处于相同作用域内, 并且函数的函数名相同, 参数不同。
  • 重写: 函数分别处于基类和派生类中,并且都是虚函数, 并且有相同的函数头
  • 隐藏: 继承体系中函数分别处在基类和派生类的作用与之中, 不是虚函数,并且都具有相同的函数头

三、如何构成多态

        要形成多态有两个条件:

  • 一、虚函数的重写。
  • 二、父类的指针指向子类,或者父类的引用引用子类对象。

        如下为一个多态的实例:

  1. //A位基类
  2. class A
  3. {
  4. public:
  5. virtual void func()
  6. {
  7. cout << "Afunc()" << endl;
  8. }
  9. };
  10. //B类继承A类
  11. class B : public A
  12. {
  13. public:
  14. virtual void func()
  15. {
  16. cout << "Bfunc()" << endl;
  17. }
  18. };
  19. //C类继承A类
  20. class C : public A
  21. {
  22. virtual void func()
  23. {
  24. cout << "Cfunc" << endl;
  25. }
  26. };
  27. int main()
  28. {
  29. C c;
  30. B b;
  31. A* p = nullptr;
  32. p = &c;
  33. p->func();
  34. p = &b;
  35. p->func();
  36. return 0;
  37. }

         在这串代码中, B类和C类都是A类的派生类。 他们都有对A类中的虚函数func进行重写, 满足条件一。 

        然后基类的指针p先是指向了C类的对象。 又指向了B类的对象。 构成了父类的指针指向子类, 满足条件二。

        所以, 这就是一个多态。

其实, 多态的应用场景多为这样:

  1. //A位基类
  2. class A
  3. {
  4. public:
  5. virtual void func()
  6. {
  7. cout << "Afunc()" << endl;
  8. }
  9. };
  10. //B类继承A类
  11. class B : public A
  12. {
  13. public:
  14. virtual void func()
  15. {
  16. cout << "Bfunc()" << endl;
  17. }
  18. };
  19. //C类继承A类
  20. class C : public A
  21. {
  22. virtual void func()
  23. {
  24. cout << "Cfunc" << endl;
  25. }
  26. };
  27. void test_func(A* p)
  28. {
  29. p->func();
  30. }
  31. int main()
  32. {
  33. C c;
  34. B b;
  35. test_func(&b);
  36. test_func(&c);
  37. return 0;
  38. }

        这样, 通过传送不同类型的对象给test_func函数, 就能构成多态。

四、抽象类

        如果一个虚函数后面加上 =0, 那么这个虚函数就是纯虚函数, 并且包含这个纯虚函数的类叫做抽象类。

        抽象类不能实例化对象。

  1. //A位基类
  2. class A
  3. {
  4. public:
  5. virtual void func() = 0;
  6. };
  7. int main()
  8. {
  9. A a;
  10. return 0;
  11. }

但是A的派生类如果重写了纯虚函数, 那么就可以这个派生类就可以实例化处对象。

但是如果A的派生类没有重写纯虚函数, 那么这个派生类同样不能实例化处对象。

  1. //A位基类
  2. class A
  3. {
  4. public:
  5. virtual void func() = 0;
  6. };
  7. //B类继承A类
  8. class B : public A
  9. {
  10. public:
  11. //重写A类的func函数
  12. };
  13. int main()
  14. {
  15. B b;
  16. return 0;
  17. }

五、普通继承和接口继承

        普通继承:在继承体系中, 派生类继承了基类的函数, 能够直接使用的是普通继承, 这类继承继承的是基类函数的实现。 

        接口继承:如果继承了基类的虚函数, 并且重写实现了多态。 那么就是一种接口继承, 多态的体系是一种接口的继承, 具体的函数实现是由派生类自己实现的。

六、静态绑定和动态绑定

        静态绑定: 静态绑定又被成为前期绑定,  当程序在编译的时候确定的要调用的函数, 确定了程序要执行的行为, 这个过程成为静态多态。 比如我们使用的函数重载就是静态的多态。

        动态绑定: 动态绑定又被成为后期绑定, 当程序在编译之后也就是运行期间根据不同的对象调用不同的函数。 这个过程叫做动态多态, 也就是多态。

------------------------------------------------------

ps: 本篇内容没有讲解多态的原理, 因为多态的原理其实就是虚函数表。 而虚函数表的详细讲解博主之前已经写过一篇: 总结虚函数表机制——c++多态底层原理-CSDN博客 。

        在这篇文章中, 博主用自己的理解讲解的虚函数表的机制与实现。 写的不甚严谨, 但是里面的结论却是博主通过调试一步一步验证的来的。感兴趣的友友们可以看一下。

后续补带有虚函数的类的内存大小的计算(暂时有点模糊, 先不写, 而且最近考试比较多。可能要等暑假才能补上这一板块)。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/知新_RL/article/detail/993274
推荐阅读
相关标签
  

闽ICP备14008679号