当前位置: 首页 > news >正文

C++学习-入门到精通【10】面向对象编程:多态性

C++学习-入门到精通【10】面向对象编程:多态性


目录

  • C++学习-入门到精通【10】面向对象编程:多态性
    • 一、多态性介绍:多态电子游戏
    • 二、类继承层次中对象之间的关系
      • 1.从派生类对象调用基类函数
      • 2.将派生类指针指向基类对象
      • 3.通过基类指针调用派生类的成员函数
      • 4.virtual函数和virtual析构函数
        • 声明virtual函数
        • 调用虚函数
        • CommissionEmployee层次中的virtual函数
        • virtual析构函数
    • 三、类型域和switch语句
    • 四、抽象类和纯虚函数
      • 1.抽象类
      • 2.纯虚函数
    • 五、实例研究:应用多态性的工资发放系统
      • 1.创建抽象基类:Employee类
      • 2.创建具体的派生类:SalariedEmployee类
      • 3.创建具体的派生类:CommissionEmployee类
      • 4.创建间接派生的具体类:BasePlusCommissionEmployee类
      • 5.演示多态性的执行过程
    • 六、多态性、虚函数和动态绑定的底层实现机制
      • 实现多态的三级指针
    • 七、实例研究:应用向下强制类转换、dynamic_cast、typeid和type_info并使用多态性和运行时类型信息的工资发放系统
      • 使用dynamic_cast决定对象类型
      • 展示雇员类型


一、多态性介绍:多态电子游戏

下面我们通过一个例子来简单介绍一下什么是多态。

假设现在要设计一个电子游戏,它操作多种不同类型的对象,包括类Martian、Venutian、Plutonian、SpaceShip和laserBeam的对象。假定所有的这些类都是从一个通用基类SpaceObject类继承而来,该基类有一个成员函数draw。每个派生类都以适合自己的方式来实现draw函数。屏幕管理器程序维护一个容器(例如,一个vector对象),其中装有指向各种不同类对象的SpaceObject指针。为了刷新屏幕,屏幕管理器需定时向每个不同类型的对象发送同样的信息,也就是draw。每一种类型的对象都有自己与众不同的响应方式。例如,Martian对象可能把自己绘制成红色,并带有适当数量的天线。SpaceShip对象可能会把自己绘制成银色的飞碟,LaserBeam对象可能会将自己绘制成横贯屏幕的白色光柱。相同信息(本例中为draw)发送给不同类型的对象,产生了“不同形式”的结果——故而称之为多态性

多态性的屏蔽管理器非常便于向系统中添加新类,因为只需做最少量的代码改动即可。假设我们打算将Mercurian的对象添加到上述的游戏中。为此,必须构建一个从SpaceObject类继承而来的Mercurian类对象,但是它需要提供自己的成员函数draw的定义。然后,当指向Mercurian类对象的指针出现在容器中时,程序员并不需要修改屏幕管理器的代码。屏幕管理器对容器中每个对象调用成员函数draw,而不管对象的类型,所以这样新的Mercurian对象只需“直接插入”即可。因此只需构建和包含Mercurian类本身,而不需要改动系统,程序员就可以利用多态性容纳新加入的类,甚至包括那些在系统创建时没有预见到的类。

多态性的特点

  • 利用多态性,程序员可以处理普遍性问题并让执行时的环境自己关心特殊性。可以指挥各种对象执行与它们相符的行为,甚至不需要知道它们的类型(只要这些对象属于同一个继承层次,并且它们都是通过一个共同的基类指针或者一个共同的基类引用访问的)。
  • 多态性提高了软件的可扩展性,调用多态行为的软件可以用与接收消息的对象类型无关的方式编写。因此,不用修改基本系统,就可以把能够响应现有消息的新类型的对象添加到系统中。只有实例化新对象的客户代码必须修改,以适应新类型。

二、类继承层次中对象之间的关系

在上一个章节中我们简单的介绍了基类和派生类之间的关系,现在,我们将更详细地分析在继承层次结构中各个类之间的关系。下面几节提供了一系列实例,演示如何将基类指针和派生类指针指向基类对象和派生类对象,以及如何利用这些指针调用操作这些对象的成员函数。

  1. 将派生类对象的地址赋给了基类指针。然后,展示通过该基类指针调用函数,从而调用基类的功能,也就是句柄类型决定了哪个函数被调用;
  2. 将基类对象的地址赋给派生类指针,结果将导致编译错误;我们将讨论这一错误信息,并研究编译器不允许这样赋值的原因;
  3. 将派生类对象的地址赋给基类指针,然后分析为什么利用该基类指针只能调用基类的功能,而当通过该基类指针试图调用派生类的成员函数时,编译出现错误;
  4. 展示如何从指向派生类对象的基类指针获取多态行为。首先介绍virtual函数(虚函数)和多态性,这是通过将基类函数声明为virtual实现的。然后把派生类对象的地址赋给基类指针,并利用这一指针调用派生类的功能,这恰恰是我们需要达到多态性行为的功能。

演示这些例子就是想说明一个重要思想:public继承的派生类的对象可以当作它的基类对象进行处理。这导致了一些有趣的操作。例如,在程序中可以创建一个基类指针的数组,数组元素指向许多派生类的对象。尽管事实上派生类对象和基类对象是不同的类型,但是编译器允许这样的赋值,因为每个派生类对象都是一个基类对象。然而,反过来是不行的,不能将一个派生类对象当作一个基类对象来处理。因为派生类中可能还新增的数据成员,这是基类对象中没有的。

1.从派生类对象调用基类函数

下面例子中重用了CommissionEmployee和BasePlusCommissionEmployee的最终版本。演示了将基类指针和派生类指针指向基类对象和派生类对象的三种方式。前两种是简单直接的——将基类指针指向基类对象并调用基类的功能,以及将派生类指针指向派生类对象并调用派生类的功能。然后将把基类指针指向派生类对象,以及展示基类的功能的确在派生类对象中是可用的,来说明派生类和基类之间的关系(is-a的关系)。

类定义参考上一篇文章
test.cpp

#include <iostream>
#include <stdexcept>
#include <iomanip>
#include "BasePlusCommissionEmployee.h"
using namespace std;int main()
{// 创建一个基类对象CommissionEmployee commissionEmployee("Peter", "Griffin", "222-22-2222", 10000, .06);// 创建一个基类指针CommissionEmployee* commissionEmployeePtr = nullptr;// 创建一个派生类对象BasePlusCommissionEmployee basePlusCommissionEmployee("Chris", "Griffin", "333-33-3333", 6000, .05, 300);// 创建一个派生类指针BasePlusCommissionEmployee* basePlusCommissionEmployeePtr = nullptr;// 设置输出的浮点数格式,固定输出小数点后两位cout << fixed << setprecision(2);// 输出基类对象和派生类对象cout << "Print base-class and derived-class objects:\n\n";commissionEmployee.print();cout << "\n\n";basePlusCommissionEmployee.print();// 使用基类指针指向基类对象,并使用指针输出该对象commissionEmployeePtr = &commissionEmployee;cout << "\n\nCalling print with base-class pointer to "<< "\nbase-class object invokes base-class print function:\n\n";commissionEmployeePtr->print();// 使用派生类指针指向派生类对象,并使用指针输出该对象basePlusCommissionEmployeePtr = &basePlusCommissionEmployee;cout << "\n\nCalling print with derived-class pointer to "<< "\nderived-class object invokes derived-class print function:\n\n";basePlusCommissionEmployeePtr->print();// 将基类指针指向一个派生类对象commissionEmployeePtr = &basePlusCommissionEmployee;cout << "\n\n\nCalling print with base-class pointer to "<< "\nderived-class object\n"<< "invokes base-class print function on that derived-class object:\n\n";commissionEmployeePtr->print();cout << endl;
}

运行结果:

在这里插入图片描述

如先前提到的,每个BasePlusCommissionEmployee类的对象都是一个CommissionEmployee类的对象,且多了一个数据成员。派生类的earnings成员函数和print成员函数重新定义了基类中的对应函数(基类的函数一样被继承到派生类,只不过被派生类中重新定义的函数给隐藏了),以实现对应功能。

在上述代码中我们先使基类指针和派生类指针分别指向基类对象和派生类对象,再通过指针来调用它们的成员函数。然后我们将一个派生类对象的地址赋给了基类指针,此时使用基类指针执行print函数,可以看到它执行的是基类版本的print函数。这表明被调用的功能取决于用来调用函数的句柄(如指针或引用)类型,而不是句柄所指向的对象类型。

调用的功能取决于句柄的类型,与句柄指向类型无关。

2.将派生类指针指向基类对象

在上一小节中,我们将一个基类指针指向了一个派生类对象,并解释了C++编译器允许这种赋值的原因:每个派生类对象都是一个基类对象。
在这一小节中,我们采用相反的做法,将派生类指针指向一个基类对象。使用的测试基类与派生类代码同样是CommissionEmployee和BasePlusCommissionEmployee类。

假设编译器允许这样的赋值,会产生什么后果呢?通过一个BasePlusCommissionEmployee类的指针,可以为它所指向的对象(即一个基类对象)调用派生类中的每个成员函数(调用函数的句柄的一个派生类指针,它将会调用派生类的成员函数)。当它调用成员函数setBaseSalary时,这就会导致一些问题,基类中是没有baseSalary这个数据成员的,因此该函数可能会覆盖内存中其他的重要数据(按照派生类对象中baseSalary相对于其他数据成员的位置进行覆盖),这很有可能是其他对象中的数据。

test.cpp

#include <iostream>
#include <iomanip>
#include "BasePlusCommissionEmployee.h"
using namespace std;int main()
{CommissionEmployee commissionEmployee("Peter", "Griffin", "222-22-2222", 10000, .06);BasePlusCommissionEmployee* basePlusCommissionEmployeePtr = nullptr;basePlusCommissionEmployeePtr = &commissionEmployee;
}

出现了编译错误:

在这里插入图片描述

3.通过基类指针调用派生类的成员函数

利用基类指针,编译器只允许调用基类的成员函数。因此如果基类指针指向了派生类对象,并且试图访问只在派生类中存在的成员函数就会产生编译错误。

虽然通过public继承的派生类的对象都是基类的对象,可以通过基类指针调用派生类的成员函数,但是这种行为也可能产生编译错误。当基类指针试图调用派生类中特有的成员函数时就会产生错误。使用基类指针作为句柄来调用成员函数时,只能调用与它关联的类类型的成员函数,也就是只能调用基类中存在的成员函数。

如果我们现在非要通过一个基类的指针来调用派生类中才存在的成员函数,这能做到吗?
可以,我们只要显式地把基类指针强转换为派生类指针,就可以通过该基类指针调用只有派生类拥有的成员函数,这就是向下强制类型转换技术。向下强制类型转换具有潜在危险,我们会在本文的后续章节中介绍如何安全地向下转换。

test.cpp,尝试使用一个指向派生类对象的基类指针调用只在派生类中存在的成员函数

#include <iostream>
#include <string>
#include "BasePlusCommissionEmployee.h"
using namespace std;int main()
{CommissionEmployee* commissionEmployeePtr = nullptr;BasePlusCommissionEmployee basePlusCommissionEmployee("Chris", "Griffin", "222-22-2222", 6000, .04, 500);// 将派生类对象的地址赋给基类指针commissionEmployeePtr = &basePlusCommissionEmployee;// 通过基类指针调用派生类对象的基类成员函数string firstName = commissionEmployeePtr->getFirstName();string lastName = commissionEmployeePtr->getLastName();string ssn = commissionEmployeePtr->getSocialSecurityNumber();double sales = commissionEmployeePtr->getGrossSales();double rate = commissionEmployeePtr->getCommissionRate();// 试图通过基类指针调用只存在于派生类中的成员函数double Salary = commissionEmployeePtr->getBaseSalary();commissionEmployeePtr->setBaseSalary(700);
}

出现编译错误:

在这里插入图片描述

如果一个派生类对象的地址已经赋给了一个它的直接或间接基类指针,把这个基类指针进行强制类型转换而转换为派生类指针是被允许的。事实上,为了发送那些在基类中不出现的派生类对象的信息,这种转换是必要的。

4.virtual函数和virtual析构函数

在第一个节中,我们使用一个基类的指针指向了派生类的对象,然后通过这个指针调用print成员函数。
前面我们说过,句柄类型决定了哪个类的函数被调用。在这种情形下,调用成员函数的句柄的一个基类指针,所以程序会调用基类中的成员函数,尽管该指针指向的对象是一个派生类对象,且该派生类中有自己定制的print函数。

那么我们有没有办法让调用的函数由调用成员函数的对象的类型来决定呢?(上面例子中,实际上调用成员函数的对象仍是派生类对象,只不过它通过基类指针这个句柄来调用。)
使用virtual函数,调用成员函数的对象的类型来决定调用哪个版本的virtual函数,而不是由句柄类型来决定。

为什么virtual函数是有用的?
下面我们在通过一个例子来思考一下virtual函数为什么是有用的。假设一组形状如类Circle、Triangle、Rectangle和Square都是由基类Shape派生而来的,每个类都可以通过成员函数draw来绘制自己,但是不同形状的draw函数实现大相径庭。在要绘制一组形状的程序中,能够像处理基类Shape的对象一样统一调用处理所有的形状是非常关键的。我们使用基类指针来指向这些派生类的对象,但是直接使用这个指针来调用draw函数只能调用基类中的draw函数,如果使用向下转换的方法,那么每个派生类对应的代码都不相同,所以这就需要使用virtual函数,让程序根据任意时刻基类Shape指针所指向的对象的类型,动态地(即在执行时)决定应该调用哪个派生类的draw函数,这就是多态行为。

声明virtual函数

要允许上面所述的这种行为,必须在基类中把draw函数定义为virtual函数,在每个派生类中重写draw函数(与之前的重新定义作区分,重写函数是多态,且被重写的函数必须声明为virtual函数),使之能够绘制正确的形状。
从实现角度来看,重写一个函数与重新定义一个函数没什么不同。在派生类中重写的函数和它重写的基类函数具有相同的签名和返回值类型(即函数原型相同)。没有将基类函数声明为virtual,那么这就是重新定义。如果声明为virtual,那么这就是在重写该函数并导致多态性行为。
要声明一个virtual函数,必须在基类中该函数的原型前加上关键字virtual。例如
virtual void draw() const;
声明出现在基类Shape中。上述的函数原型声明draw函数是一个无参、无返回值的虚函数。这个函数声明为const,是因为draw函数一般不会改变调用它的Shape对象——虚函数不一定非要声明为const。

一旦一个函数声明为virtual,那么从整个继承层次结构的那一点起向下的所有类中,它将保持是virtual的,即使当派生类重写此函数时并没有显式地将它声明为virtual。
所以好的编程习惯是,即使某些函数因类层次结构中高层已经声明为virtual而成为隐含的virtual函数,但是为了程序更加清晰可读,最好类层次结构的每一级中都把它们显式地声明为virtual函数。

错误预防技巧:C++11中,在派生类的每一个覆盖函数上使用 override 关键字,会迫使编译器检查基类中是否有同名且同参数列表的成员函数(相同的签名)。如果没有,则编译器报错。

当派生类选择不重写从其基类继承而来的虚函数时,派生类就会简单地继承它的基类的虚函数的实现。

调用虚函数

通过基类指针或引用调用虚函数

如果程序通过指向派生类对象的基类指针(比如,shapePtr->draw())或者指向派生类对象的基类引用(比如,shapeRef.draw())调用虚函数,那么程序会根据所指对象的类型,动态地选择正确的派生类draw函数。在执行时选择合适的调用函数称为动态绑定迟绑定

通过对象名称调用虚函数

当虚函数通过按名引用特定对象和使用圆点成员选择运算符的方式(如,squareObject.draw())被调用时,调用哪个函数在编译时就已经决定了(称为静态绑定),所调用的虚函数正是为该特定对象所属的类定义的(或继承而来的)函数,这并不是多态性行为。因此,使用虚函数进行动态绑定只能通过指针引用句柄完成。

CommissionEmployee层次中的virtual函数

现在,让我们看看在雇员类层次结构中虚函数是怎样导致多态性行为发生的。

对之前的类CommissionEmployee和类BasePlusCommissionEmployee的头文件进入修改,声明每个类中的earnings和print函数是虚函数。因为在CommissionEmployee类中的earnings和print函数是虚函数,所以派生类的这两个函数进行了重写,此外,还将它们声明为override

注意,虚函数的关键字virtual只需要在函数原型前声明即可,函数定义前不需要。

将类层次结构中的earnings和print函数声明为虚函数只需要修改它们的头文件即可,函数定义不需要改变。

CommissionEmployee.h

#include <string>class CommissionEmployee
{
public:// 参数分别为firstName、lastName、socialSecurityNumber、grossSales和commnissionRateCommissionEmployee(const std::string&, const std::string&,const std::string&, double = 0.0, double = 0.0);void setFirstName(const std::string&);std::string getFirstName() const;void setLastName(const std::string&);std::string getLastName() const;void setSocialSecurityNumber(const std::string&);std::string getSocialSecurityNumber() const;void setGrossSales(double);double getGrossSales() const;void setCommissionRate(double);double getCommissionRate() const;// 为实现多态性行为,声明为虚函数virtual double earnings() const; // 计算工资virtual void print() const; // 输出CommissionEmployee的成员
private:std::string firstName;std::string lastName;std::string socialSecurityNumber; // 社保号码double grossSales; // 销售总额double commissionRate; // 提成比例
};

BasePlusCommissionEmployee.h

#include "CommissionEmployee.h"class BasePlusCommissionEmployee : public CommissionEmployee
{
public:BasePlusCommissionEmployee(const std::string&, const std::string&,const std::string&, double = 0.0, double = 0.0, double = 0.0);void setBaseSalary(double);double getBaseSalary() const;// 收入发生变化,所以earnings成员函数需要重写virtual double earnings() const override; // 显式声明为override,增强代码可读性// 同样的打印函数一样需要重写virtual void print() const override;
private:double baseSalary;
};

测试代码:
现在,如果将一个基类的指针指向一个派生类的对象,并且使用该指针调用earnings和print函数,那么这个派生类对象中相应的函数就会被调用。

test.cpp

#include <iostream>
#include <iomanip>
#include "BasePlusCommissionEmployee.h"
using namespace std;int main()
{CommissionEmployee commissionEmployee("Peter", "Griffin", "222-22-2222", 10000, .06);CommissionEmployee* commissionEmployeePtr = nullptr;BasePlusCommissionEmployee basePlusCommissionEmployee("Chris", "Griffin", "333-33-3333", 5000, .04, 300);BasePlusCommissionEmployee* basePlusCommissionEmployeePtr = nullptr;cout << fixed << setprecision(2);// 调用基类对象和派生类对象静态绑定的print函数cout << "Invoking print function on base-class and derived-class "<< "\nobjects with static binding\n\n";commissionEmployee.print();cout << "\n\n";basePlusCommissionEmployee.print();// 调用动态绑定的print函数cout << "\n\n\nInvoking print function on base-class and derived-class "<< "\nobjects with dynamic binding";commissionEmployeePtr = &commissionEmployee;cout << "\n\nCalling virtual function print with base-class pointer"<< "\nto base-class object invokes base-class "<< "print function:\n\n";commissionEmployeePtr->print();basePlusCommissionEmployeePtr = &basePlusCommissionEmployee;cout << "\n\nCalling virtual function print with derived-class pointer"<< "\nto derived-class object invokes derived-class "<< "print function:\n\n";basePlusCommissionEmployeePtr->print();// 将基类指针指向一个派生类对象commissionEmployeePtr = &basePlusCommissionEmployee;cout << "\n\nCalling virtual function print with base-class pointer"<< "\nto derived-class object invokes derived-class "<< "print function:\n\n";commissionEmployeePtr->print();cout << endl;
}

运行结果:

在这里插入图片描述

可以看到当将print函数声明为虚函数之后,我们虽然使用的是基类的指针来调用print函数,但是它却是根据指向对象的类型来调用函数。

virtual析构函数

现在我们来考虑一下,当我们要使用多态性来处理类层次中动态分配的对象时,就会产生一个问题。到目前为止,我们看到的析构函数都是非虚析构函数,即没有用virtual关键字声明的析构函数。如果要删除一个具有非虚析构函数的派生类对象,却显式地通过指向该对象的一个基类指针对它应用delete运算符,要删除一个类的对象会调用析构函数,此时调用的函数并不是一个虚函数,且调用的句柄类型是基类类型,那么就会调用基类的析构函数来释放派生类对象的空间,这在C++标准中是未定义的。

这种问题同样也可以通过在基类中创建virtual析构函数来解决。如果基类析构函数声明为虚函数,那么任何派生类的析构函数也都是虚函数,并且对基类的析构函数进行了重写。
例如,在类CommissionEmployee中定义,virtual ~CommissionEmployee() {},现在,如果对一个基类指针用delete运算符显式地删除它所指的类层次中某个对象,那么系统会根据该指针所指对象的调用相应类的析构函数。切记,派生类对象被销毁时,派生类对象中属于基类的部分也会被销毁,因为执行派生类和基类的析构函数是很重要的。基类的析构函数在派生类的析构函数执行之后自动执行。

从此以后,我们将在所有包括虚函数的类中包括virtual析构函数。

错误预防:如果一个类中含有virtual函数,该类就要提供一个virtual析构函数,即使该析构函数并不一定是该需要的。这可以保证当一个派生类的对象通过基类指针删除时,这个自定义的派生类析构函数会被调用。

常见错误:构造函数不能是virtual函数,声明一个构造函数为virtual是一个编译错误。
因为继承并不继承构造函数,每一个类都应该有它自己的构造函数。

C++11:Final 成员函数和类
C++11之前,派生类可以覆盖基类的任何虚函数。
在C++11中,基类的virtual函数在原型中声明为final,如:virtual someFunction(parameters) final;。那么该函数在任何派生类中都不能被覆盖。这保证了基类final成员函数定义被所有基类对象和所有基类的直接、非直接派生类的对象使用。

同样的,在C++11之前,任何现有类在层次上均可以作为基类使用,而在C++11中,可以将类声明为final以防被当作基类使用。如:

class MyClass final
{// class body
}

试图重写一个final函数或者继承一个final基类会导致编译错误。

三、类型域和switch语句

我们要实现对不同的派生类对象执行不同的操作,除了上面使用的多态性之外,还可以使用最基础的switch语句。

不过要使用switch语句的话,需要判断对象的类型,将不同类型的对象分开,为特定的对象调用合适的操作。例如,在上面的电子游戏的例子中,屏幕处理器可以使用switch语句,区分不同类的对象,然后对每种类的对象执行相应的操作。

然而,使用switch逻辑容易导致程序产生各种潜在的问题。例如,程序员可能忘记必要的类型检查,或忘记在switch语句中检查所有可能的情况。当对一个基于switch语句的系统通过添加新类型的方式进行修改时,程序员可能会忘记在所有相关的switch语句中插入这些新的类型。而且每增加或删除一个类都需要修改系统中的每条switch语句,但跟踪这些语句需要花费大量的时间,并且很容易出错。

所以多态性程序设计可以消除不必要的switch逻辑。通过通过多态性机制可以完成同样的逻辑,从而使程序员避免与switch逻辑相关的各种典型的错误。

同时,使用多态性设计的程序相较于使用switch语句实现的程序,看上去更简单,因为它们包含更少的分支逻辑和更简单有序的代码。

四、抽象类和纯虚函数

1.抽象类

当把一个类作为类型时,都假设程序将创建这种类型的对象。然而,在有些情况下,定义程序员永远不打算实例化任何对象的类是有用的。这样的类称为抽象类。因为通过抽象类在类的继承结构中作为基类,所以我们称它们为抽象基类。这些类不能用来实例化对象,因为抽象类是不完整的——其派生类必须在这些类的对象实例化前定义那些“缺少的部分”。

构造抽象类的目的是为其他类提供适合的基类。可以用来实例化对象的类称为具体类。这些类定义或继承实现它们声明的每一个成员函数。举个例子,我们可以定义一个抽象基类TwoDimensionalShape(二维形状),然后由它派生出具体类,如Square、Circle和Triangle。还可以定义一个抽象基类ThreeDimensionalShape(三维形状),并由它派生出具体类,如Cube、Sphere和Cylinder。

抽象基类太宽泛以至于无法定义真实的对象,在实例化对象之前,需要更加具体的内容。例如,有人要你绘制一个二维形状,到底应该绘制什么形状呢?具体类提供了详细说明,使得类实例化对象是合理的。

继承层次不需要包含任何抽象类,但是我们将看到,很多优秀的面向对象系统都有以抽象基类打头的类继承层次。在有些情况下,抽象类构成了层次结构的上面几层。之前举的形状继承层次就是一个很好的例子。它开始于抽象基类Shape,其接下来的一层有两个更抽象的基类,即TwoDimensionalShapeThreeDimensionalShape。再下一层是定义了二维形状的具体类和三维形状的具体类。

2.纯虚函数

通过声明类的一个或多个虚函数为纯虚函数(pure virtual function),可以使一个类成为抽象类。一个纯虚函数是在声明时“初始化值为0”的函数,如下所示:
virtual void draw() const = 0; // 纯虚函数
=0称为纯指示符。纯虚函数不提供函数的具体实现,每个派生的具体类必须重写所有基类的纯虚函数的定义,提供这些函数的具体实现。

虚函数和纯虚函数的区别是:
虚函数有函数的实现,并且提供派生类是否重写的选择权。
纯虚函数并不提供函数的实现,需要派生类重写这些函数以使派生类成为具体类,否则派生类仍然是抽象类。

当基类实现一个函数是没有意义的,并且程序员希望在所有具体的派生类中实现这个函数时,就会用到纯虚函数。

回到前面提到的电子游戏的例子,基类SpaceObject提供draw函数的实现是没有意义的(因为在没有更多关于太空对象类型信息的情况下,没办法绘制出一个泛指的太空对象)。可被定义成员虚函数(并不是纯虚函数)的一个情况是返回对象名称的函数。我们可以给泛指的SpaceObject对象起名字(例如,“space object”),可以为这个函数提供一个默认的函数实现,并且它没有必要是纯虚函数。
但是该函数依然声明为纯虚函数,这是因为它期望派生类重写这一函数的实现,从而为派生类对象提供更具体的名字。

抽象类为类层次结构中的各种类定义公共的通用接口。抽象类包含一个或多个纯虚函数,这些函数必须在具体的派生类中重写

提示
未能在派生类中重写纯虚函数会使得派生类也变成抽象的。试图实例化抽象类的对象将导致编译错误。

抽象类至少包含一个纯虚函数。抽象类也可以有数据成员和具体的函数(包括构造函数和析构函数),它们被派生类继承时也符合继承的一般规则。

虽然我们不能实例化抽象基类的对象,但是可以使用抽象基类来声明指向抽象类派生出的具体类对象的指针和引用。程序使用这些指针和引用来多态地实现派生类对象。

五、实例研究:应用多态性的工资发放系统

现在要解决下面的问题:
在这里插入图片描述
我们使用抽象类Employee来表示通常概念的雇员。直接从Employee类派生出类SalariedEmployee和类CommissionEmployee,再从类CommissionEmployee中派生出类BasePlusCommissionEmployee。下面的UML图显示了多态的雇员工资应用程序中类的继承层次结构。
在这里插入图片描述

抽象基类Employee声明了类层次结构的“接口”,即程序可以对所有的Employee类对象调用的一组成员函数集合。每个雇员,不论他的工资计算方式如何,都有名、姓和社会保险号码,因此在抽象基类Employee中含有private数据成员firstName、LastName和socialSecurityNumber。

派生类可以从基类继承接口并/或实现。为“实现继承”而设计的类层次结构往往将功能设置在较高层,即每个新派生类继承定义在基类中的一个或多个成员函数,并且派生类使用这些基类定义;为“接口继承”设计的类层次结构则趋于将功能设置在较低层,即基类指定一个或多个应为类继承层次中的每个类定义的函数(基类和派生类中的函数有相同原型),但是各个派生类提供自己对于这些函数的实现

1.创建抽象基类:Employee类

Employee类中除了包含操作Employee类的数据成员的各种get和set函数之外,还提供成员函数earningsprint。earnings函数应用于所有的雇员,但是每项收入的计算取决于雇员的类型。所以在基类Employee中把earnings函数声明为纯虚函数,因为这个函数默认的实现是没有任何意义的,没有足够的信息决定应该返回的收入是多少。各个派生类都用合适的实现来重写earnins函数。
要计算一个雇员的收入,程序把一个雇员对象的地址赋给一个基类Employee的指针,然后通过指针来调用该对象的earnings函数。我们维护元素为一个Employee指针的vector对象,每个指针都指向一个Employee对象(当然,不可能存在Employee对象,因为它是一个抽象类;不过因为继承的关系,Employee类的每个派生类的对象都可以认为是一个Employee对象)。程序迭代访问此vector对象并调用每个Employee的earnings函数。C++多态地执行这些函数调用。因为Employee类中的earnings函数是纯虚函数,所以所有的希望成为具体类的那些继承了Employee类的派生类都需要重写earnings函数。

Employee类中的print函数显示雇员的名、姓和社会保险号码。每个该类的派生类都重写了print函数,输出雇员的类型(例如,“salaried employee”)之后,紧接着还输出了雇员的其他信息。函数print可以调用earnings,即使earnings是类Employee的一个纯虚函数。(注意因为抽象类不可实例化任何对象,所以静态调用纯虚函数是非法的,只可以通过基类指针指向一个派生类对象来调用)。

下表中给出了earnings和print两个函数的期望实现结果。

earningsprint
Employee=0firstName lastName
socialSecurityNumber: SSN
SalariedEmployeeweeklySalarysalaried employee: firstName lastName
socialSecurityNumber: SSN
weekly salary: weeklySalary
CommissionEmployeecommissionRate * grossSalescommission employee: firstName lastName
socialSecurityNumber: SSN
gross sales: grossSales
commission rate: commissionRate
BasePlusCommissionEmployee(commissionRate * grossSales) + baseSalarybase-salaried commission employee: firstName lastName
socialSecurityNumber: SSN
gross sales: grossSales
commission rate: commissionRate
base salary: baseSalary

其中Employee类中的earnings函数被指定为=0表示它遇到一个上纯虚函数,因而没有实现。每个派生类都重写earnings函数,提供合适的实现。基类中的其他函数都被派生类直接继承。

Employee.h

#pragma once
#include <string>class Employee
{
public:// 构造函数初始化firstName、lastName和socialSecurityNumberEmployee(const std::string&, const std::string&, const std::string&);// 将析构函数定义成虚函数,以便可以实现多态行为virtual ~Employee() // 在类中定义析构函数,编译器自行决定是否内联{std::cout << "Excuting destructor: Employee object.\n";}void setFirstName(const std::string&);std::string getFirstName() const;void setLastName(const std::string&);std::string getLastName() const;void setSocialSecurityNumber(const std::string&);std::string getSocialSecurityNumber() const;// 定义纯虚函数,使Employee成为一个抽象类virtual double earnings() const = 0; // 因为如果想要得到一个雇员的工资情况,需要得知雇员的具体类型,在基类中实现它是没有意义的// 所以将它声明成一个纯虚函数virtual void print() const;
private:std::string firstName;std::string lastName;std::string socialSecurityNumber;
};

Employee.cpp

#include <iostream>
#include <string>
#include "Employee.h"
using namespace std;Employee::Employee(const string& first, const string& last, const string& ssn): firstName(first),lastName(last),socialSecurityNumber(ssn)
{// 因为三个数据成员都是string类对象,存在拷贝构造函数,所以可以在成员初始化器中进行初始化cout << "Excuting constructor: Employee object.\n";
}// 数据成员firstName的set和get成员函数
void Employee::setFirstName(const string& first)
{ firstName = first;
}string Employee::getFirstName() const
{return firstName;
}// 数据成员lastName的set和get成员函数
void Employee::setLastName(const string& last)
{lastName = last;
}string Employee::getLastName() const
{return lastName;
}// 数据成员socialSecurityNumber的set和get成员函数
void Employee::setSocialSecurityNumber(const string& ssn)
{socialSecurityNumber = ssn;
}string Employee::getSocialSecurityNumber() const
{return socialSecurityNumber;
}// 虚函数print的定义
void Employee::print() const
{cout << getFirstName() << ' ' << getLastName()<< "\nsocial security number: " << getSocialSecurityNumber();
}

注意,虚函数print提供的实现会在每个派生类中被重写,但是这些print函数都会使用这个抽象类中的print函数的版本来输出Employee类层次结构中所有类共有的信息。

2.创建具体的派生类:SalariedEmployee类

SalariedEmployee类从Employee类中派生而来,它的public成员除了共有的成员之外还包括一个double类型的成员,用来表示周薪。

SalariedEmployee.h

#pragma once
#include "Employee.h"class SalariedEmployee : public Employee
{
public:SalariedEmployee(const std::string&, const std::string&, const std::string&, double = .0);virtual ~SalariedEmployee(){std::cout << "Excuting destructor: SalariedEmployee object.\n";}void setWeeklySalary(double);double getWeeklySalary() const;// 显式的声明virtual,提高可读性// 使用关键字override表示重写的虚函数virtual double earnings() const override;virtual void print() const override;
private:double weeklySalary;
};

SalariedEmployee.cpp

#include <stdexcept>
#include "SalariedEmployee.h"
using namespace std;SalariedEmployee::SalariedEmployee(const string& first, const string& last, const string& ssn, double salary): Employee(first, last, ssn) // 必须显式调用Employee的构造函数来初始化其中包含的数据成员
{setWeeklySalary(salary);cout << "Excuting constructor: SalariedEmployee object.\n";
}void SalariedEmployee::setWeeklySalary(double salary)
{if (salary > .0){weeklySalary = salary;}else{throw invalid_argument("salary must be greater than 0.0.");}
}double SalariedEmployee::getWeeklySalary() const
{return weeklySalary;
}// 重写纯虚函数earnings
double SalariedEmployee::earnings() const
{return getWeeklySalary();
}// 重写虚函数print
void SalariedEmployee::print() const
{cout << "salaried employee: ";Employee::print(); // 一定要使用二元作用域分辨运算符来指定调用类Employee版本的print函数,// 否则会陷入函数调用死递归中cout << "\nweekly salary: " << getWeeklySalary();
}

注意如果SalariedEmployee类中没有重写earnings函数的实现,那么该类会继承一个纯虚函数,此时该类变成一个抽象类。
如果没有重写print函数,那么派生类会继承基类中的实现,仅仅输出姓名和社保号码。这些信息是不足以充分描述一个SalariedEmployee类型的雇员的。

3.创建具体的派生类:CommissionEmployee类

该类中较抽象基类增加了两个数据成员grossSalescommissionRate

CommissionEmployee.h

#pragma once
#include "Employee.h"class CommissionEmployee : public Employee
{
public:CommissionEmployee(const std::string&, const std::string&, const std::string&,double = .0, double = .0);virtual ~CommissionEmployee(){std::cout << "Excuting destructor: CommissionEmployee object.\n";}void setGrossSales(double);double getGrossSales() const;void setCommissionRate(double);double getCommissionRate() const;virtual double earnings() const override;virtual void print() const override;
private:double grossSales;double commissionRate;
};

CommissionEmployee.cpp

#include <stdexcept>
#include "CommissionEmployee.h"
using namespace std;CommissionEmployee::CommissionEmployee(const string& first, const string& last,const string& ssn, double sales, double rate): Employee(first, last, ssn)
{setGrossSales(sales);setCommissionRate(rate);cout << "Excuting constructor: CommissionEmployee object.\n";
}// 数据成员grossSales的set和get成员函数
void CommissionEmployee::setGrossSales(double sales)
{if (sales > .0){grossSales = sales;}else{throw invalid_argument("gross sales must be greater than 0.0.");}
}double CommissionEmployee::getGrossSales() const
{return grossSales;
}// 数据成员commissionRate的set和get成员函数
void CommissionEmployee::setCommissionRate(double rate)
{if (rate > .0 && rate < 1.0){commissionRate = rate;}else{throw invalid_argument("gross sales must be greater than 0.0 and less than 1.0.");}
}double CommissionEmployee::getCommissionRate() const
{return commissionRate;
}// 纯虚函数的重写
double CommissionEmployee::earnings() const
{return getGrossSales() * getCommissionRate();
}// 虚函数的重写
void CommissionEmployee::print() const
{cout << "commission employee: ";Employee::print();cout << "\ngross sales: " << getGrossSales()<< "\ncommission rate: " << getCommissionRate();
}

4.创建间接派生的具体类:BasePlusCommissionEmployee类

BasePlusCommissionEmployee类是CommissionEmployee类的直接派生类,因此是Employee类的间接派生类。

BasePlusCommissionEmployee.h

#pragma once
#include "CommissionEmployee.h"class BasePlusCommissionEmployee : public CommissionEmployee
{
public:BasePlusCommissionEmployee(const std::string&, const std::string&,const std::string&, double = .0, double = .0, double = .0);~BasePlusCommissionEmployee(){std::cout << "Excuting destructor: BasePlusCommissionEmployee object.\n";}void setBaseSalary(double);double getBaseSalary() const;// 收入发生变化,所以earnings成员函数需要重写virtual double earnings() const override; // 显式声明为override,增强代码可读性// 同样的打印函数一样需要重写virtual void print() const override;
private:double baseSalary;
};

BasePlusCommissionEmployee.cpp

#include <stdexcept>
#include "BasePlusCommissionEmployee.h"
using namespace std;BasePlusCommissionEmployee::BasePlusCommissionEmployee(const string& first, const string& last, const string& ssn,double sales, double rate, double salary): CommissionEmployee(first, last, ssn, sales, rate)
{setBaseSalary(salary);cout << "Excuting constructor: BasePlusCommissionEmployee object.\n";
}void BasePlusCommissionEmployee::setBaseSalary(double salary)
{if (salary >= 0.0){baseSalary = salary;}else{throw invalid_argument("Base salary must be greater than 0.0.");}
}double BasePlusCommissionEmployee::getBaseSalary() const
{return baseSalary;
}double BasePlusCommissionEmployee::earnings() const
{// 使用作用域分辨运算符指定基类的作用域来调用在派生类中被隐藏的基类的earnings函数return getBaseSalary() + CommissionEmployee::earnings();
}void BasePlusCommissionEmployee::print() const
{cout << "base-salaried ";CommissionEmployee::print();cout << "\nbase salary: " << getBaseSalary();
}

5.演示多态性的执行过程

为了测试Employee类的层次结构,使用的测试程序为3个具体类SalariedEmployee、CommissionEmployee和BasePlusCommissionEmployee都创建一个对象。程序首先使用静态绑定方式对这些对象进行了操作,然后使用Employee指针的vector多态地对这些对象进行操作。

test.cpp

#include <vector>
#include <iomanip>
#include <stdexcept>
#include "SalariedEmployee.h"
#include "BasePlusCommissionEmployee.h"
using namespace std;// 通过函数实现多态行为
void virtualViaPointer(const Employee* const); 
void virtualViaReference(const Employee&);int main()
{cout << fixed << setprecision(2);SalariedEmployee salariedEmployee("Peter", "Griffin", "111-11-1111", 800);CommissionEmployee commissionEmployee("Chris", "Griffin", "222-22-2222", 10000, .06);BasePlusCommissionEmployee basePlusCommissionEmployee("Meg", "Griffin", "333-33-3333", 5000, .03, 300);cout << "\nEmployees processed individually using static binding:\n\n";salariedEmployee.print();cout << "\nearned $" << salariedEmployee.earnings() << "\n\n";commissionEmployee.print();cout << "\nearned $" << commissionEmployee.earnings() << "\n\n";basePlusCommissionEmployee.print();cout << "\nearned $" << basePlusCommissionEmployee.earnings() << "\n\n";// 创建一个元素为Employee指针的vector对象vector <Employee*> employees(3);employees[0] = &salariedEmployee;employees[1] = &commissionEmployee;employees[2] = &basePlusCommissionEmployee;cout << "\nEmployees processed ploymorphically via dynamic binding:\n\n";cout << "Virtual function calls made off base-class pointers:\n\n";for (const Employee* employeePtr : employees){virtualViaPointer(employeePtr);}cout << "Virtual function calls made off base-class references:\n\n";for (const Employee* employeeRef : employees){virtualViaReference(*employeeRef);}
}void virtualViaPointer(const Employee* const baseClassPtr)
{baseClassPtr->print();cout << "\nearned $" << baseClassPtr->earnings() << "\n\n";
}void virtualViaReference(const Employee& baseClassRef)
{baseClassRef.print();cout << "\nearned $" << baseClassRef.earnings() << "\n\n";
}

运行结果:

在这里插入图片描述

从上面结果中我们可以得出基类的指针和引用调用虚函数都会导致多态行为。

六、多态性、虚函数和动态绑定的底层实现机制

本节将会揭示C++实现多态性、虚函数和动态绑定的内部机制,让大家更透彻地理解这些功能的工作原理。

C++编译器为了支持运行时的多态性,而在编译时创建了一个数据结构。多态性是通过三级指针实现的。正在运行的程序使用这些数据结构执行虚函数,实现与多态性相关联的动态绑定。

当C++编译含有一个或多个虚函数的类时,它为这个类创建一个虚函数表(简称为 vtable)。vtable中包括指向类虚函数的指针(函数指针包含执行该函数任务的代码的内存空间的首地址)。每次调用该类的虚函数时,运行程序都会利用vtable选择正确的函数实现。

在Employee实例研究中,每个具体类都提供自己对虚函数earnings和print的实现。我们已经知道从抽象基类派生的类要成为一个具体类,就必须自己实现earnings函数,因为它是一个纯虚函数。而并不一定要自己实现print函数,因为它并不是一个纯虚函数,它可以继承抽象基类中的该函数的实现。此外,如果类CommissionEmployee是一个具体类,那么从该类派生出的类BasePlusCommissionEmployee不一定要自己实现earnings和print函数,它可以从CommissionEmployee类中继承。如果在类层次中的类以这种方式继承函数实现,那么在虚函数表中这些函数的指针将指向继承的函数实现。例如,如果BasePlusCommissionEmployee类中没有重写earnings函数,那么该类的虚函数表中的earnings函数指针和CommissionEmployee类中的虚函数表中的earnings函数指针都指向同一个earnings函数。

实现多态的三级指针

多态性是通过包含了三级指针的数据结构实现的。

第一级指针就是虚函数表中的指向应该被调用的虚函数的函数指针;

关于第二级指针,无论何时,只要我们实例化一个具有一个或多个虚函数的类的对象时,编译器会在这个对象的前面附上一个指针,指向对象所属类的虚函数表。即一个有虚函数的类的对象,即包含一个指向该类vtable的指针,也包含该类的数据成员。

第三级指针仅仅包含接收虚函数调用的对象句柄(使用这个句柄来调用虚函数)。这个句柄只能是指针或引用。

我们以之前的Employee类层次结构为例子,现在使用一个Employee类的指针来指向一个CommissionEmployee类对象,使用该指针句柄来调用print函数。

它会进行以下操作:

  1. 判断调用的函数的类别(是虚函数还是非虚函数)。是虚函数则进行下一步。如果是非虚函数则直接在编译阶段就进行了确定(编译器根据指针的静态属性,它是一个基类指针,所以与基类中的函数进行绑定);
  2. 根据基类指针找到派生类对象中指向虚函数表的指针,根据函数索引得到它在虚函数表中的偏移值,找到对应的函数指针;
  3. 根据找到的函数指针调用函数;

每次调用虚函数时发生的指针间接引用操作和内存访问,都需要增加程序执行时间。而虚函数表和加入对象的vtable指针也要占用额外的内存。所以程序员需要根据系统的具体需求选择是否使用多态。

下图中展示了上面的类层次结构中类的vtable。并给出了一个虚函数调用的过程示例。

在这里插入图片描述

七、实例研究:应用向下强制类转换、dynamic_cast、typeid和type_info并使用多态性和运行时类型信息的工资发放系统

之前的程序中,多态地处理Employee对象时,我们并不需要知道它到底哪个类具体的对象。而汉我们是为basePlusCommissionEmployee对象增加10%的基本工资时,就不得不在运行时判定每个Employee对象的具体类型。本节演示了运行时类型信息(RTTI)动态强制类型转换的强大功能,它使程序在运行时能够判定对象的类型,从而对对象进行操作。

test.cpp

#include <iomanip>
#include <vector>
#include <typeinfo>
#include "SalariedEmployee.h"
#include "BasePlusCommissionEmployee.h"
using namespace std;int main()
{cout << fixed << setprecision(2);vector <Employee *> employees(3);// 初始化vector对象employees[0] = new SalariedEmployee("Peter", "Griffin", "111-11-1111", 800);employees[1] = new CommissionEmployee("Chris", "Griffin", "222-22-2222", 10000, .06);employees[2] = new BasePlusCommissionEmployee("Meg", "Griffin", "333-33-3333", 5000, .04, 300);cout << "\n";for (Employee* employeePtr : employees){employeePtr->print();cout << endl;// 将基类指针类型向下转换成派生类指针类型BasePlusCommissionEmployee* derivedPtr = dynamic_cast <BasePlusCommissionEmployee*>(employeePtr);// if (derivedPtr != nullptr){double oldBaseSalary = derivedPtr->getBaseSalary();cout << "old base salary: $" << oldBaseSalary << endl;derivedPtr->setBaseSalary(1.10 * oldBaseSalary);cout << "new base salary with 10% increace is: $" << derivedPtr->getBaseSalary() << endl;}cout << "earned $" << employeePtr->earnings() << "\n\n";}for (const Employee* employeePtr : employees){cout << "\ndeleting object of "<< typeid(*employeePtr).name() << endl;delete employeePtr;}
}

运行结果:

在这里插入图片描述

使用dynamic_cast决定对象类型

本例中,当遇到BasePlusCommissionEmployee类的对象时,我们希望将他们的基本工资提高10%。由于是以多态的方式处理每个雇员对象,因此不能(利用目前学习过的方法)在任意给定的时间确定正在被处理的雇员的类型。这就产生了一个问题,因为我们现在只对BasePlusCommissionEmployee这个类型的对象增加基本工资,所以我们需要在程序执行过程中,识别出该类的对象,这样才能只为该类对象增加工资。

要达到上述目的,必须使用运算符dynamic_cast决定每个对象所属的类型是不是BasePlusCommissionEmployee。这就是之前提到过的向下强制类型转换运算BasePlusCommissionEmployee* derivedPtr = dynamic_cast <BasePlusCommissionEmployee*>(employeePtr);这条语句动态地把employeePtr从类型Employee*向下强制转换为类型BasePlusCommissionEmployee*。如果该employeePtr所指向的对象是一个BasePlusCommissionEmployee对象,那么这个对象的地址就赋给派生类指针DerivedPtr;否则 ,DerivedPtr赋值为nullptr

注意,dynamic_cast除了进行类型转换之处,还要在类型转换之前进行类型检查。而static_cast仅仅进行类型转换。例如,static_cast<BasePlusCommissionEmployee*>(employeePtr)仅仅将类型Employee*转换成类型BasePlusCommissionEmployee*。如果使用这条语句进行转换的话,程序将试图为每个Employee对象增加基本工资,这对非BasePlusCommissionEmployee对象是未定义行为。

注意
如果本例中没有使用向下强制类型转换运算符将一个Employee类型的指针转换成BasePlusCommissionEmployee类型的指针,而直接进行这样的赋值会出现编译错误。因为派生类对象是一个基类对象,但是反过来并不成立。且如果没有进行这样的转换,也是无法使用一个基类指针来调用getBaseSalary这样的只在派生类中定义的函数的。

展示雇员类型

在程序的最后,使用for循环显示每个雇员对象的类型,并使用delete运算符释放每个vector元素所指向的动态分配的内存。

运算符typeid返回一个type_info类对象的引用,包含了包括操作数类型名称在内的关于该运算符操作数类型的信息。调用时,type_info类的成员函数name返回一个基于指针的字符串,它包含传递给typeid实参的类型名称。要使用运算符typeid必须包含头文件<typeinfo>
在这里插入图片描述

相关文章:

  • 论坛系统(4)
  • C++核心编程_赋值运算符重载
  • 多线程(3)
  • 带sdf 的post sim 小结
  • azure web app创建分步指南系列之一
  • CMP401GSZ-REEL混合电压接口中的23ns延迟与±6V输入范围设计实现
  • const ‘不可变’到底是值不变还是地址不变
  • 痉挛性斜颈相关内容说明
  • 无人机桥梁3D建模、巡检、检测的航线规划
  • Spine工具入门教程2之导入
  • Linux安装及管理程序
  • 简易WLAN上传下载查看器by批处理
  • 95套HTML高端大数据可视化大屏源码分享
  • AI与软件工程结合的未来三年发展路径分析
  • AI对软件工程的影响及未来发展路径分析报告
  • ARXML解析与可视化工具
  • ToolsSet之:渐变色生成工具
  • 复刻真实世界的虚拟系统Goal
  • 漏洞Reconfigure the affected application to avoid use of weak cipher suites. 修复方案
  • Ts中的 可选链操作符
  • 深圳公司网站建设设计/怎么免费推广自己网站
  • 网站制作的合同/成都网站seo技术
  • 专业代做网站制作/网站如何添加友情链接
  • 跨境独立站怎么运营/百度爱采购平台官网
  • 怎么给人介绍自己做的网站/百度提问首页
  • 网站建设的难处/网络技术培训