多态是面向对象开发过程中一个非常重要的概念。
11.1 多态概述
11.1.1 什么是多态
多态(polymorphism),从字面理解是“多种形态,多种形式”,是一种将不同的特殊行为泛化为当个特殊记号的机制。
多态从实现的角度可划分为两类:
(1)编译时的多态:编译过程中确定了同名操作的具体操作对象
(2)运行时的多态:程序运行时才动态确定操作所针对的具体对象
11.1.2 多态的引入
先尝试理解以下程序:
#include<iostream>
using namespace std;class Animal
{public:void sleep(){cout<<"Animal sleep"<<endl;}void breathe(){cout<<"Animal breath"<<endl;}
};
class Fish:public Animal
{public:void breathe(){cout<<"Fish double"<<endl;}
};
int main()
{Fish fh;Animal *an=&fh;an->breathe();return 0;
}
运行结果:
分析过程如下:
如何解决这个问题呢?可以通过多态来解决。
11.1.3 联编
联编是确定操作的具体对象的过程,只计算机程序自身彼此关联的过程,即把一个标识符名和一个存储地址联系在一起的过程,
根据进行阶段的不同,可以分为静态联编与动态联编,这两种联编过程分别对应多态的两种实现方式,如下图:
11.2 函数重载
函数重载与运算符重载可以实现编译时的多态性。以下只介绍函数重载,运算符重载见第12章。
函数重载也称为多态函数
(1)作用:使程序能够用同一个名字来访问一组相关的函数,提高程序灵活性。
(2)含义:函数名相同,但函数所带的参数个数或数据类型不同(编译系统会根据参数来决定调用哪个同名函数)
函数重载表现为两种情况:
(1)参数个数或类型有所差别的重载
(2)函数的参数完全相同但属于不同类
第一种情况与构造函数重载类似,不作追诉,只介绍第二种情况。当函数的参数完全相同但属于不同类时,为了让编译能正确区分调用哪个类的同名函数,可用以下两种方法别:
(1)用对象名区别,在函数名前加上对象名来限制(对象名.函数名)
(2)用类名和作用域运算符区别,在函数名前加“类名::”来限制(类名::函数名)
示例代码:
#include<iostream>
using namespace std;class Point
{int x,y;public:Point(int a,int b){x=a;y=b;}double area(){return x*y;}};
class Circle:public Point
{int r;public:Circle(int a,int b,int c):Point(a,b){r=c;}double area(){return 3.1416*r*r;}
};
int main()
{Point pob(15,15);Circle cob(20,20,10);cout<<"pob.area()="<<pob.area()<<endl<<endl;cout<<"cob.area()"<<cob.area()<<endl<<endl;cout<<"cob.Point::area()"<<cob.Point::area()<<endl<<endl;return 0;
}
运行结果:
通过函数重载方式,可以是同名函数实现不同功能,即实现了静态多态性。
11.3 虚函数
虚函数是实现运行时多态的一个重要方式,是重载的另一种形式。也就是动态编联。
11.3.1 定义虚函数
虚函数的定义是在基类中进行的,即把基类中需要定义为虚函数的成员函数声明为virtual。虚函数定义的一般形式如下:
virtual <函数类型><函数名>(参数表)
{函数体;
}
virtual void breathe()
{cout<<"Animal breathe"<<endl;
}
当基类中的某个成员函数被声明为虚函数后,就可以在派生类中重新定义。在派生类中重新定义时,其函数原型包括返回类型、函数名、参数个数和类型,参数的顺序都必须与基类中的原型完全一致。
示例代码:(与第一个代码做对比)
#include<iostream>
using namespace std;class Animal
{public:void sleep(){cout<<"Animal sleep"<<endl;}virtual void breathe(){cout<<"Animal breath"<<endl;}
};
class Fish:public Animal
{public:void breathe(){cout<<"Fish double"<<endl;}
};
int main()
{Fish fh;Animal *an=&fh;an->breathe();return 0;
}
运行结果:
代码将基类中的成员函数breathe()定义为虚函数,即加上virtual关键字,然后在主函数main()中定义Animal对象指针指向Fish的对象fh,调用breathe()函数后,得到的结果就是预期输出Fish bubble的结果。具体分析过程如下:
在使用派生类对象指针时应该注意:
(1)不能指向私有派生类的对象;
(2)指向公有派生类对象时,只能访问派生类中从基类继承下来的成员,不能直接访问公有派生类中定义的成员。
(3)指向派生类对象的指针不能指向基类的对象;
虚函数可以很好的实现多态,在使用虚函数是应注意的问题如下:
(1)虚函数的声明只能出现在类函数原型的声明中,不能出现在函数体实现时;
(2)基类中只有保护成员或公有成员才能被声明为虚函数;
(3)在派生类中重新定义虚函数时,关键字virtual可以写也可以不写,但在容易引起混乱时,应该写上关键字;
(4)动态编联只能通过成员函数来调用或通过指针、引用来访问虚函数。
在派生类中程序定义基类中的虚函数,是函数重载的另一种形式,但它与函数重载又有区别:
(1)一般函数重载,要求其函数的参数数量或参数类型必须有所不同,函数的返回值也可以不同;
(2)重载一个虚函数时,要求函数名、返回类型、参数个数、参数的类型和参数的顺序必须与基类中的虚函数的原型完全相同。
11.3.2 多级继承和虚函数
多级继承可以看作是多个单继承的组合,多级继承的虚函数与单继承的虚函数的调用相同。即不同类创建的对象调用的函数是不一样的。
示例代码:
#include<iostream>
using namespace std;class Base
{public:virtual void func(){cout<<"Base output"<<endl;}};
class Derived1:public Base
{public:void func(){cout<<"Derived1 output"<<endl;}
};
class Derived2:public Derived1
{public:void func(){cout<<"Derived2 output"<<endl;}
};
void test(Base &b)
{b.func();
}int main()
{Base bObj;Derived1 d1Obj;Derived2 d2Obj;test(bObj);cout<<endl;test(d1Obj);cout<<endl;test(d2Obj);cout<<endl;return 0;
}
运行结果:
上述代码中定义了一个多级继承,在基类中定义了虚函数func(),在主函数main()中调用该函数时,不同类创建的对象调用的函数是不一样的,即实现了多态的“一个接口,多种实现”。
11.4 纯虚函数与抽象类
抽象类是一种包含纯虚函数的特殊类。建立抽象类时为了多态地使用抽象类的成员函数。
11.4.1 纯虚函数
在基类中不能为虚函数给出一个有意义的实现时,可以将其声明为纯虚函数。纯虚函数的实现可以留给派生类来完成。纯虚函数的作用是为派生类提供一个一致的接口。一般来说,一个抽象类带有至少一个纯虚函数。纯虚函数的一般定义如下:
纯虚函数与普通函数定义的不同在于书写形式加了“=0”,说明在基类中不用定义改函数的函数体。
示例代码:纯虚函数的函数体由派生类定义;
#include<iostream>
using namespace std;class Point
{protected:int x0,y0;public:Point(int i=0,int j=0){x0=i;y0=j; } virtual void set()=0;//声明纯虚函数
};
class Line:public Point
{protected:int x1,y1;public:Line(int i=0,int j=0,int m=0,int n=0):Point(i,j){x1=m;y1=j;}void set()//定义接口函数{cout<<"Line::set() called \n";}
};void setobj(Point *p)
{p->set();
}int main()
{Line *lineobj = new Line;setobj(lineobj);return 0;
}
运行结果:
11.4.2 抽象类
抽象类是包含纯虚函数的一种特殊类,是为了抽象和设计而建立的,处于继承层次结果的教上层。抽象类是不能建立对象的,为了强调一个类是抽象类,可讲该类的构造函数声明为保护的访问控制权限。抽象类的主要作用如下:
(1)将有关的类组织在一个继承层层次结构中,由抽象类来为它们提供一个公共的根,相关的子类是从这个根派生出来的。
(2)抽象类只描述这组子类沟通的操作接口,而完整的实现留给子类。
使用抽象类是应注意:
(1)抽象类只能用做其他类的基类,不能创建抽象类的对象
(2)抽象类不能用做参数类型、函数的返回类型或显示转换的类型
(3)可以声明抽象类的对象指针或对象引用,从而可以访问派生类对象成员,实现多态编联
(4)若派生类中没有给出抽象类的所有纯虚函数的函数体,派生类仍然是一个抽象类,若派生类中给出抽象类的所有纯虚函数的函数体,则这个派生类不再是抽象类,可以创建自己的对象。
示例代码:
#include<iostream>
using namespace std;class Vehicle
{protected:float speed;int total;public:Vehicle(float speed,int total){Vehicle::speed=speed;Vehicle::total=total;}virtual void ShowMember()=0;
};
class Car:public Vehicle
{protected:int aird;public:Car(int aird,float speed,int total):Vehicle(speed,total){Car::aird=aird;}void ShowMember(){cout<<"the speed is:"<<speed<<endl;cout<<"the total is:"<<total<<endl;cout<<"the aird is:"<<aird<<endl;}
};int main()
{//Vehicle a(100,4);Car b(250,150,4);b.ShowMember();return 0;
}
运行结果:
声明抽象类Vehicle的对象a(100,4)时报错,不能为抽象类声明对象。