多态性概述

多态指的是同样的消息被不同类型的对象接收时导致的不同的行为。消息指的是对类的成员函数的调用,不同的行为指的的是不同的实现,就是调用了不同的函数

最简单的一个例子就是**“+”**,可以实现整数、浮点数和双精度浮点数之间的加法,包括混合类的加法。同样的一个“+”消息,被不同类型对象接受后,采用不同的方式进行加法运算。这就是多态性。

多态的类型

面向对象的多态性可以分为四类:

  • 重载多态
  • 强制多态
  • 包含多态
  • 参数多态

前两种称为专用多态,后两种称为通用多态

  1. 重载多态也就是之前学过的普通函数和类的成员函数的重载,当然也包括运算符的重载
  2. 强制多态,简单说举个例子就是**+**涉及混合类型的时候,会进行强制类型的转化,这是强制多态的实例。
  3. 包含多态主要指的是类族中定义于不同类的同名成员函数的多态行为,通过虚函数实现。
  4. 参数多态与类模版相关联,使用的时候必须赋予实际的类型才能实例化。

本篇所要介绍的正式运算符的重载和虚函数。

多态的实现

多态从实现的角度可以分为两类,编译时的多态和运行时的多态,前者顾名思义,在编译的过程中确定了同名操作的具体操作对象,这种操作的具体对象的过程就叫做绑定(binding)

绑定指的是计算机程序自身彼此关联的过程,把一个标识符和一个存储地址联系在一起的过程。用面向对象的术语说,把一条消息和一个对象的方法相结合的过程。

绑定也分为两种,静态绑定动态绑定

绑定在编译连接阶段完成的情况称之为静态绑定。也称之为早期绑定或者早绑定。

有了静态绑定,动态绑定顾名思义就是在程序运行的过程完成绑定。也称之为晚期绑定和后绑定。对应着运行时的多态。

运算符的重载

运算符重载是对已有的运算符赋予多重含义,是同一个运算符作用于不同类型的数据时导致不同的行为。

运算符重载的规则

  1. C++中的运算符除了少数几个之外,都能重载,而且只能重载已有的运算符
  2. 重载之后运算符的优先级和结合性都不会改变。
  3. 运算符是针对新类型数据的实际需要,对原有运算符进行适当的改造。重载的功能应该和原有功能类似,不能改变操作数的数目,至少一个是自定义的类型(否则也不叫重载)。

不能重载的几个操作符,类属关系运算符**"."**、成员函数指针运算符".*"、作用域分辨符"::“和三目运算符”?:"。


重载形式有两种,重载为类的非静态成员函数重载为非成员函数

重载为类的非静态成员函数和非成员函数的一般形式:

1
2
3
4
返回类型 operator 运算符(形参表)
{
函数体
}

当以非成员函数的形式重载,有时需要访问类中的成员,这时候可以声明为友元函数。

需要注意的是:

当运算符重载为类的成员函数时,函数的参数个数比原来的操作数个数要少一个(后置“++”“–”除外);当重载为非成员函数的时候,参数个数与原操作数的个数相等。原因是,重载为类的成员函数时,第一个操作数会被作为函数调用的目的对象,因此无需出现在参数表中,函数体中可以直接访问第一个操作数的成员,而重载为非成员函数时,运算符所有的操作数必须显式通过参数传递。

运算符重载为成员函数

主要是单目运算符和双目运算符的区别。

双目运算符,应该将前面的类的数据类型作为那个类的成员函数,后面的类的数据类型放在形参里。

单目运算符分为两类,前置单目运算符后置单目运算符。(++,–就是单目运算符,放的位置不一样叫法不一样)

  • 对于前置单目运算符,重载的成员函数没有形参;

  • 对于后置的单目运算符,函数要带有一个int型的形参,这里的int型参数在运算中不起任何作用,只是用于区别前置和后置。

运算符重载为非成员函数

对于双目运算符,前后两个数据,只需要一个是自定义数据类型就能够进行操作符的重载,且两个数据类型都需要作为函数的形参。

对于前置单目运算符,形参是操作的数据类型。

对于后置单目运算符,形参有两个,一个需要操作的数据类型,另一个是int型数据。

不难发现,成员函数和非成员函数的主要区别的就是,成员函数把前一个操作数据作为函数调用的对象,隐含进去了,非成员函数则不会。


本节用一个Complex(复数类)去说明

点击查看题目图片(地大实验题目)

源代码见github

虚函数

虚函数是动态绑定的基础。虚函数必须是非静态的成员函数。虚函数经过派生之后,在类族中就可以实现运行过程中的多态。

一般虚函数成员

声明语法

1
virtual 函数类型 函数名(形参表);

虚函数声明只能出现在类定义中的函数原型声明中,而不能在成员函数中实现。

运行过程中的多态需要满足的条件:

1. 赋值兼容规则
1. 声明虚函数
1. 由成员函数来调用或者是通过指针、引用来访问虚函数

如果派生类中并没有显式给出虚函数的声明,这时系统就会遵循以下规则来判断派生类的一个函数成员是不是虚函数:

  1. 该函数是否与基类的虚函数有相同的名称。
  2. 该函数是否与基类的虚函数有相同的参数个数和对应的参数类型。
  3. 该函数是否与基类的虚函数有相同的返回值类型或者满足赋值兼容规则的指针,引用类型的返回值。

如果满足了上述条件,派生类中的虚函数就会覆盖掉基类中的一切同名函数,也就是作用域屏蔽。当然也可以通过作用域分辨符"::"区分。

强调一下,只用通过基类的指针或引用调用虚函数时,才会发生动态绑定。

其实在不在派生类的虚函数前加上virtual关键字都无所谓,但是建议还是要加上,能够更加清晰得看出是虚函数。

对象切片指的是用派生类的实例去初始化基类的对象,调用的是基类的复制构造函数,也就是说,派生类的数据成员只有与基类相同的部分会进行复制,其余的不管。此时这个基类的对象就和派生类的对象毫无关系了,也非常符合类型兼容规则。

虚析构函数

不能声明虚构造函数但是可以声明虚析构函数。语法形式在正常的析构函数前加一个virtual关键字。

那么什么情况下需要使用到虚析构函数呢?

当基类的指针指向一个派生类的对象的时候,若此时进行delete(基类的指针)就会调用基类的析构函数而非派生类的析构函数,造成内存泄漏。(我其实觉得这种情况几乎不会碰到了解就好)跟普通成员函数的多态区别其实也不大。

纯虚函数与抽象类

纯虚函数

纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加“=0”,例如:

1
virtual void func() = 0

我的一大疑惑是为什么要引入纯虚函数?

  1. 为了方便使用多态特性,我们常常需要在基类中定义虚拟函数。
  2. 在很多情况下,基类本身生成对象是不合情理的。

例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。

为了解决上述问题,引入了纯虚函数的概念,将函数定义为纯虚函数,则编译器要求在派生类中必须予以重写以实现多态性。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。这样就很好地解决了上述两个问题。声明了纯虚函数的类是一个抽象类。所以,用户不能创建抽象类的实例,只能创建它的派生类(实现了基类中的纯虚函数的定义)的实例。纯虚函数最显著的特征是:它们必须在继承类中重新声明函数(不要后面的=0,否则该派生类也不能实例化),而且它们在抽象类中往往没有定义。定义纯虚函数的目的在于,使派生类仅仅只是继承函数的接口。纯虚函数的意义,让所有的类对象(主要是派生类对象)都可以执行纯虚函数的动作,但类无法为纯虚函数提供一个合理的缺省实现。所以类纯虚函数的声明就是在告诉子类的设计者,“你必须提供一个纯虚函数的实现,但我不知道你会怎样实现它”。

抽象类

很简单,有纯虚函数的类就是抽象类,不能实例化

如果你想了解更多,参见博客

程序实例——变步长梯形积分算法求解函数的定积分

参见github

综合实例——对个人银行账户管理程序的改进

参见github