【C++】C++学习总结(五):多态性

一、多态的两种类型

多态:一种行为对应着多种不同的实现。根据C++实现多态的不同阶段,多态的实现分为静态联编和动态联编。

静态联编:在程序编译阶段就能实现的多态性,这种多态性成为静态多态性(编译时的多态性),可通过函数重载和运算符重载实现(运算符重载是一种特殊的函数重载);

动态联编:在程序执行阶段实现的多态性,这种多态性称为动态多态性(运行时的多态性),可通过继承、虚函数、基类的指针或引用等技术来实现。

(1)静态多态性(函数重载)

在同一类中的同名成员函数(不限于同类成员函数,可以是任意重载函数),定义时在形式参数的个数、顺序、类型方面有所不同,在程序编译时就能根据实际参数与形式参数的匹配情况,确定该类对象究竟调用了哪一个成员函数;

(2)动态多态性

在基类与派生类中存在的同名函数,要求该同名函数的原型在基类和派生类中完全一致,而且是虚函数。在编译时无法确定究竟调用的是哪一个同名函数,只有在程序运行时通过基类指针指向基类或派生类对象,或基类的引用代表的是基类或派生类的对象,确定调用的是基类还是派生类的同名函数。

二、静态多态性的实现

静态多态性的优点:函数调用速度快、效率高;

静态多态性的缺点:编程不够灵活。

静态多态性可以通过函数重载和运算符重载来实现,前面已经涉及了普通函数重载,这里讲解运算符重载。

运算符重载是对已有的运算符赋予多重含义,C++中预定义的运算符的操作对象只能是基本数据类型,而对于用户自定义类型也许要有类似的运算操作,这时就可以对运算符重新定义,赋予已有运算符新的功能。

运算符重载实质就是函数重载,此时的函数名为:operator 运算符,实际调用时,首先把指定的运算符表达式转化为对运算符函数的调用,运算对象转化为运算符函数的实际参数,然后根据实际参数的类型来确定究竟调用哪一个同名运算符函数,这个过程是在编译过程中完成的,因此运算符重载实现的时静态多态性。

1、运算符重载的规则

运算符重载通常作用在一个类上,这样,该类的对象就可以使用这些运算符实现相应操作(如自定义的复数类)。

另外,可以被重载的运算符中,除了赋值运算符“=”以及变形的赋值运算符(如“+=”,“>>=”等)之外,其余在基类重载的运算符都能被派生类继承。

注意:

①只能重载已有的运算符,不能创造新的运算符;

②重载之后,运算符的优先级与结合性都不会改变

③运算符重载是针对新类型数据的实际需要,对原有运算符进行适当的改造,一般来讲重载的功能应该与原有的功能类似;

④重载运算符不能改变原运算符的操作对象个数,同时至少要有一个对象是自定义的类型。

作用在类上的运算符重载可以有两种方式:通过类中成员函数类的友元函数进行重载。

2、用成员函数重载运算符

成员函数重载运算符:将运算符重载定义成一个类的成员函数的形式。

只能重载为成员函数的运算符:“=”、“()”、“[]”、“->”,另外单目运算符以及复合赋值运算符也建议重载为成员函数。

用成员函数重载运算符的一般格式:

                                     <函数类型> operator <运算符> (<形式参数表>)

其中“operator <运算符>”可以理解为函数名。重载单目运算符时,形参列表为空,唯一的操作数为当前对象重载双目运算符时,由形式参数表指定第二操作数(右操作数),第一操作数无需指定,调用运算符函数的当前对象就是第一操作数。

(1)重载单目运算符

<函数类型> operator <运算符> ()
{
    ... //函数体
}

对象1=对象2 <运算符>  <———————等价于——————>对象1=对象2.operator <运算符> ()

如:a1=a2++ <———————等价于——————>a1=a2.operator ++()

(2)重载双目运算符

<函数类型> operator <运算符> (第二操作数形式参数)
{
    ... //函数体
}

对象1=对象2 <运算符> 对象3  <———————等价于——————>对象1=对象2.operator <运算符> (对象3)

如:a1=a2+a3 <———————等价于——————>a1=a2.operator ++(a3)

上例中,a1=a2++和a1=a2+a3都称为隐式调用,而a1=a2.operator ++()和a1=a2.operator ++(a3)都称为显式调用

3、用友元函数重载运算符

三、动态多态性的实现

(一)虚函数

1、虚函数的定义

如果在派生类中重新定义了基类中已经存在的函数,即出现了同名覆盖现象(返回类型、函数名、参数列表完全相同),此时不论基类指针指向基类对象还是派生类对象,调用的都是基类中的同名函数,而派生类中的同名函数只能通过派生类对象来调用。同理,无论基类引用是基类的别名还是派生类的别名,调用的都是基类中的同名函数。

如果希望基类指针指向基类对象时调用的是基类中的同名函数,指向派生类对象时调用的是派生类中的同名函数,则需要将函数声明为虚函数。这样在调用时,具体执行的是哪一个类中的同名函数要等到运行到这条语句时才能决定(只有在运行时才能知道基类指针指向的地址是基类对象还是派生类对象的),这就是动态多态性。

虚函数的定义方法为:

virtual <返回类型> <成员函数名> (形参列表)

虚函数在类体外定义时,前面不能加virtual关键字。虚函数在基类中一定要加virtual声明,在共有派生类中,该原型相同的函数前可以省略virtual关键字,自动默认该成员函数就是虚函数。

基类与派生类需满足如下关系:
①派生类必须公有继承自基类;
②同名虚函数在基类和派生类中的函数原型必须完全一致,即函数的返回值类型、函数名、形参列表完全相同。

在虚函数定义和多态性的应用中,要注意以下几点:
(1)一旦基类中指定某成员函数为虚函数,那么,不管在公有派生类中是否给出virtual声明,派生类(甚至是派生类的派生类。。。)中重新定义的原型一样的成员函数均自动为虚函数。为了增强可读性,通常在派生类中仍然加上关键字virtual。
(2)在实际编程中,只需在类的声明文件(即头文件)中用virtual声明虚函数,在类的定义文件中不能再加关键字virtual。
(3)只有类非静态成员函数才可以是虚函数(因为静态成员函数不属于某一个对象)。因为,通过虚函数表现多态性是一个类的派生关系,普通函数不具备这种派生关系。
(4)派生类必须以公有方式继承基类,这是赋值兼容规则的前提,派生类只有从基类公有继承,才允许基类的指针指向派生类,基类的引用才允许是派生类的别名。
(5)内联函数不能声明为虚函数。内联函数的执行代码是明确的,没有多态性的特征。
(6)构造函数不能是虚函数,构造函数在对象创建时调用,完成对象的初始化,此时对象还没有完全建立,所以将构造函数声明为虚函数没有意义。
(7)析构函数可以是虚函数,且往往被声明为虚函数。

2、虚析构函数

如果析构函数声明为虚函数,则该类的所有派生类析构函数也自动成为虚函数而无需显式声明。当删除指向派生类的基类指针时,就会调用该指针指向的派生类的析构函数,而派生类的析构函数又自动调用基类的析构函数,这样整个派生类的对象被完全释放。如果析构函数不是虚函数,则编译器实施静态绑定,在删除基类指针时,只调用指针所属的基类的析构函数而不是派生类的析构函数,这会导致析构不完全。所以当派生类中存在指针数据成员,又通过该指针进行了动态空间申请时,将析构函数声明为虚函数是非常有必要的。

3、虚函数与同名覆盖

一个函数,如果在基类和其公有派生类中拥有相同的函数名,但是函数返回值类型不同,或者形参列表不同,即使在基类中被声明为虚函数,也不具备多态性。

(二)纯虚函数与抽象类

1、纯虚函数

纯虚函数只给出了函数原型声明而没有具体的实现内容,声明是在虚函数原型的后面赋0,一般形式如下:

virtual <返回类型> <成员函数> (<形式参数表>) = 0;

①纯虚函数是一种没有函数体的特殊虚函数,在定义时,将“=0”写在虚函数原型最后,表示这是一个纯虚函数;
②纯虚函数不能被掉调用,只有在派生类中被具体定义后才可调用;
③纯虚函数的作用在于基类给派生类提供一个标准的函数原型与统一接口,为实现动态多态性打下基础,派生类将根据需要给出虚函数的具体实现代码。

2、抽象类

抽象类是指该类中至少包含一个纯虚函数。

①抽象类不能生成对象,因为该类中的纯虚函数无实现代码;
②可以定义抽象类指针或引用,用来实现动态多态性。但是不能用抽象类作为参数类型、函数返回值类型或显式转换的类型;
③抽象类的基类不能是普通类;
④抽象类除了必须至少有一个虚函数外,还可以定义普通成员函数或虚函数。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值