什么是多态性?
概念
书上的表示是——向不同的对象发送同一个消息,不同的对象在接受时会有不同的反应,产生不同的动作。
我们在上课时,老师是这么解释的:在一个学校里,每个人都有不同的工作,当校长发出号令“开工”,每个人就开始工作(属于自己的那份工作)。
具体一点:多态性是指用一个名字定义不同的函数,这些函数执行不同但又类似的操作,从而可以使用相同的口令来调用相应函数——一个接口,多个方法。
分类
多态性可以分为——参数多态,包含多态,重载多态,强制多态。
参数多态:函数模板,类模板
由函数模板实例化的各个函数具有相同的操作,而这些函数的参数类型却各不相同;
由类模板实例化的各个类具有相同的操作,而操作对象的类型各不相同。
包含多态:定义于不同类之间的同名成员函数,主要通过虚函数来实现。
重载多态:函数重载,运算符重载
函数重载有就是在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数、类型、类型顺序)不同。
运算符重载是对已有的运算符赋予多重含义,使同一个运算符作用于不同类型时导致不同的行为。
强制多态:是指将一个变元的类型加以变化,以符合一个函数(或者操作)的要求
如加法运算符在进行浮点数于整形数相加时,首先进行类型强制转换。把整型数变为浮点数再相加的情况。
向上类型转换
向上类型转换就是把一个派生类对象当成基类对象来用
注意:
向上类型转换是安全的
向上类型转换可以自动完成
向上类型转换的过程会丢失子类信息
#include<iostream> using namespace std; class point { public: point(double a, double b):x(a), y(b){} void area(void) { cout << "point:" << 0 << endl; } double x; double y; }; class rectangle:public point { public: rectangle(double a,double b,double c,double d):point(a,b),x1(c),y1(d){} void area(void) { cout << "rectangle:" << (x-x1)*(y-y1)<< endl; } private: double x1; double y1; }; class circle :public point { public: circle(double a,double b,double c):point(a,b),r(c){} void area(void) { cout << "circle:" <<r*r*3.14<< endl; } private: double r; }; void calcarea(point& p) { p.area(); } int main(void) { point p(0, 0); rectangle r(0, 0, 1, 1); circle c(0, 0, 1); cout << "直接调用:" << endl; p.area(); r.area(); c.area(); cout << "通过calearea函数调用:" << endl; calcarea(p); calcarea(r); calcarea(c); return 0; }
再caleare函数中,接受point的对象,但也不拒绝point的派生类对象,无需类型转换就可以将rectangle和circle的对象传给calearea。这也就是向上类型转换,可以将派生类转换为基类,这也导致rectangle和circle类的接口变窄。
可以看到通过calearea函数调用时,输出的全是“point:0",显然不是我们希望可看到的,我们希望通过基类的引用直接调用到相应的派生类成员函数。
也就是当calearea函数中的对象是rectangle时调用rectangle中的area函数;
当calearea函数中的对象时circle的对象时调用cricle中的area函数。
为了解决这个问题需要了解功能的早绑定和晚绑定
功能的早绑定和晚绑定
绑定
确定操作具体对象的过程称为绑定。
绑定是指计算机程序自身彼此关联的过程,把一个标识符和一个存储地址联系在一起(把一条消息和一个对象的方法相结合的过程)。
绑定与多态的联系
按照绑定进行的阶段不同分为:功能的早绑定和功能的晚绑定
多态从实现的角度可以分为:编译时多态和运行时多态
两种绑定方法分别对应多态的两种实现方式
编译时多态(功能的早绑定)
编译过程中确定了同名操作的具体操作对象,在程序执行前期,系统就可以确定同名标识要调用那一段程序代码。
有些多态类型(重载多态,强制多态,参数多态)可以通过早绑定确定同名操作的具体操作对象
运行时多态(功能的晚绑定)
程序运行过程中动态的确定操作所针对的具体对象,在编译过程中无法解决绑定问题,等程序开始运行之后再来确定,包含多态就是通过晚绑定来确定具体操作对象的。
一般而言
编译型语言(C,PASCAL)都采用功能的早绑定,
解释型语言(LISP,Prolog)都采用晚绑定
早绑定具有函数调用速度快,效率高,但缺乏灵活性,晚绑定恰恰相反。
C++由C语言发展而来,为了保持C语言的高效性,C++仍采用编译型也就是早绑定,为了解决某些特定情况,发明了虚函数,让C++可部分采用功能的晚绑定。
C++中,编译时的多态性主要通过函数重载和运算符重载实现。运行时的多态主要通过虚函数来实现。
实现功能晚绑定——虚函数
虚函数提供了一种更为灵活的多态性机制。虚函数允许函数调用与函数体之间的联系在运行时才建立,也就是在运行时才决定同名操作的具体操作对象。
虚函数的定义和作用
虚函数定义在基类定义,在成员函数声明前加上virtual,定义语法如下:
virtual 函数类型 函数名(形参表)
{
函数体;
}
在基类中定义虚函数,
在派生类中重写虚函数,重写时必须满足同名,同参数,同返回值。
使用时必须通过指针或者引用来调用
上面的例子基类时point,修改:我们只需要将point类中的area函数前面加上virtual就可以达到想要的效果。
class point { public: point(double a, double b):x(a), y(b){} virtual void area(void) { cout << "point:" << 0 << endl; } double x; double y; };
上面的代码是通过引用调用的,下面我们用指针来调用 ,也就是形参为指针,实参传地址。具体代码如下:
void calcarea(point* p) { p->area(); } int main(void) { point p(0, 0); rectangle r(0, 0, 1, 1); circle c(0, 0, 1); cout << "直接调用:" << endl; p.area(); r.area(); c.area(); cout << "通过calearea函数调用:" << endl; calcarea(&p); calcarea(&r); calcarea(&c); return 0; }
虚函数定义的说明
1.派生类应该从它的基类公有派生
2.必须先在基类中定义虚函数(并不一定是最高层的基类)
3.在派生类中重写虚函数是virtual可以写也可以不写
4.一个虚函数无论被继承多少次,都保持其虚函数特性
5.虚函数必须在其类的成员函数中,不能是友元函数,不能是静态成员函数
6.内联函数不能是虚函数
7.构造函数不能是虚函数
8.析构函数可以是虚函数