C++多态介绍

C++多态

简介

多态是C++面向对象的核心特征之一,核心思想是一个接口,多种实现,即同一操作作用于不同对象时,会产生不同的行为。多态性极大提升了代码的灵活性和可扩展性,时设计可维护系统的关键工具

向上转型

在继续了解多态之前我们先引入一个新概念转型,转型我们主要了解一下向上转型

复制代码
 #include <iostream>
 using namespace std;
 ​
 class A
 {
     public:
     A()
     {
         cout << "A" << endl;
     }
 ​
     void print()
     {
         cout << "print A" << endl;
     }
 };
 ​
 class B : public A
 {
     public:
     B()
     {
         cout << "B" << endl;
     }
 ​
     void print()
     {
         cout << "print B" << endl;
     }
 };
 ​
 int main()
 {
     A a;
     B b;
 ​
     //向上转型,将派生类赋值给基类
     a = b;
     //向下转型,引发报错
     //b = a;
 ​
     return 0;
 }

输出结果

至于为什么只能向上转型而不能向下转型,大致可以这样解释

如图所示,派生类中包含了基类,但是基类中并不一定包含派生类,所以可以直接使用派生类类元素为基类元素赋值,这里就引出了C++中的类型兼容规则

类型兼容规则

类型兼容规则是指在需要基类对象的任何地方,都可以使用共有派生类对象来代替。通过公有继承,派生类得到了基类中除构造函数外的所有成员。这样,公有派生类实际就具备了基类的所有功能,凡是基类能解决的问题,共有派生类都可以解决。

类型兼容规则中所指的替代包括一下情况:

  • 子类对象可以直接赋值给父类对象使用

  • 子类对象可以直接赋值给父类对象

  • 子类对象可以直接初始化父类对象

  • 父类指针可以直接指向子类对象

  • 父类引用可以直接引用子类对象

在替代之后,派生类对象就可以作为基类的对象使用,但是只能使用从基类继承的成员

类型兼容规则是多态性的重要基础之一

虚函数

复制代码
 #include <iostream>
 using namespace std;
 ​
 class A
 {
     public:
     A()
     {
         cout << "A" << endl;
     }
 ​
     void print()
     {
         cout << "print A" << endl;
     }
 };
 ​
 class B : public A
 {
     public:
     B()
     {
         cout << "B" << endl;
     }
 ​
     void print()
     {
         cout << "print B" << endl;
     }
 };
 ​
 int main()
 {
     A a;
     B b;
 ​
     A * pa = new A();
     B * pb = new B();
 ​
     a.print();
     //类型赋值之后调用的是基类的方法
     a = b;
     a.print();
     
     //指针赋值之后仍然调用的是基类的方法
     pa->print();
     pa = pb;
     pa->print();
 ​
     return 0;
 }

输出结果

上述代码中我们发现当使用派生类为基类赋值之后调用方法时编译器默认使用的还是基类的方法,此时当我们在A类中的print函数前加上virtual关键字之后再次运行程序,结果如下

此时我们发现当修改后使用指针赋值时编译器调用的就是派生类内的方法了,使用virtual关键字修饰的函数我们称之为虚函数,虚函数允许派生类重写基类函数,并在运行时根据对象实际类型调用对应版本,而非编译时的类型,因此虚函数可以通过基类的指针调用派生类的方法

函数遮蔽
复制代码
 #include <iostream>
 using namespace std;
 ​
 class A
 {
     public:
     A()
     {
         cout << "A" << endl;
     }
 ​
     virtual void print()
     {
         cout << "print A" << endl;
     }
 };
 ​
 class B : public A
 {
     public:
     B()
     {
         cout << "B" << endl;
     }
 ​
     //修改函数参数列表,遮蔽被破坏
     void print(int num)
     {
         cout << "print B" << endl;
     }
 };
 ​
 int main()
 {
     A a;
     B b;
 ​
     A * pa = new A();
     B * pb = new B();
     
     pa->print();
     pa = pb;
     pa->print();
 ​
     return 0;
 }

输出结果

此时我们观察程序运行结果发现当函数遮蔽改变之后虚函数就无法生效

函数重载

多态的意义为使用同一接口,传递不同实例,执行不同操作,这种特性运用在函数中也能发挥重要作用,此时程序中含有多个类,恰巧每个类中都有一个重名函数,每个重名函数的功能又各不相同,因此我们的目标就是编写一个函数,希望能够通过这一个函数来调用不同类内的重名函数,以简化操作

复制代码
 #include <iostream>
 using namespace std;
 ​
 class A
 {
     public:
     virtual void print()
     {
         cout << "print A" << endl;
     }
 };
 ​
 class B : public A
 {
     public:
     void print()
     {
         cout << "print B" << endl;
     }
 };
 ​
 void test(A a)
 {
     a.print();
 }
 ​
 int main()
 {
     A a;
     B b;
 ​
     test(a);
     test(b);
 ​
     return 0;
 }

运行结果:

在上述代码中我们尝试通过test函数来实现向函数内传递不同参数时调用不同类内的函数的目标,但是根据输出结果我们发现结果并不理想,在此基础上对函数进行改进之后程序如下

复制代码
 #include <iostream>
 using namespace std;
 ​
 class A
 {
     public:
     virtual void print()
     {
         cout << "print A" << endl;
     }
 };
 ​
 class B : public A
 {
     public:
     void print()
     {
         cout << "print B" << endl;
     }
 };
 ​
 void test(A *a)
 {
     a->print();
 }
 ​
 int main()
 {
     A a;
     B b;
 ​
     test(&a);
     test(&b);
 ​
     return 0;
 }

输出结果

此时程序输出了我们想要的结果,对函数的改进为从原来的传值变成了传地址。由此可见当程序内想要调用某个类内的函数时只需要在main函数中实例化一个该类的对象,然后再将该对象取值后传入目标函数即可,这样就实现了一个函数解决多个问题

上述操作完成后我们发现用一个基类对象接受子类无法发生多态,只有基类指针接受子类对象的地址或指针,才会发生多态

总结

综合上述一系列操作我们不难推理出实现多态的前提

  1. 父类中有虚函数

  2. 子类覆写父类中的虚函数

  3. 通过已被子类对象赋值的父类指针或引用,调用共用接口

多态的定义:多态是指由继承而产生的相关的不同的类,其对象对同一消息会做出不同的相应

多态的作用:能增加程序的灵活性,可以减轻系统升级,维护,调试的工作量和时间复杂度,提高代码扩展性

  • 注意事项

    1. 基类中用virtual声明成员函数为虚函数。类外实现虚函数时,不必再加virtual

    2. 再派生类中重新定义此函数成为覆写,要求函数名,返回值类型,函数参数个数以及类型全部匹配

    3. 为了避免再派生类中写错虚函数,可在派生类的虚函数中添加override修饰,确保该函数并复写来自基类的虚函数

    对于override关键字

    复制代码
     #include <iostream>
     using namespace std;
     ​
     class A
     {
         public:
         virtual void print()  
         {
             cout << "print A" << endl;
         }
     };
     ​
     class B : public A
     {
         public:
         //此处使用override关键字之后,编译会引发报错
         void print(int num) override
         {
             cout << "print B" << endl;
         }
     };
     ​
     void test(A *a)
     {
         a->print();
     }
     ​
     int main()
     {
         A a;
         B b;
     ​
         test(&a);
         test(&b);
     ​
         return 0;
     }

    override关键字通过编译时检查和显示意图声明,有效减少继承体系中的隐藏错误,同时提高代码的可读性与维护性,建议在所有的虚函数重写场景中强制使用override

  • 限制:

    1. 只有类的成员函数才能声明为虚函数

    2. 静态成员函数不能是虚函数

    3. 内联函数不能是虚函数

    4. 构造函数不能是虚函数

    5. 析构函数可以是虚函数且通常声明为虚函数