在面向对象方法中一般是这样表述多态性的:向不同的对象发送同一个消息,不同的对象在接收时会有不同的反应,产生不同的动作。也就是说,每个对象可以用自己的方式去响应共同的消息。
在
C++
程序设计中
,
多态性
是指用一个名字定义不同的函数,这些函数执行不同但又类似的操作,从而可以使用相同的调用方式来调用这些具有不同功能的同名函数。
实现“一个接口,多种方法”
C++
中的多态性可以分为
4
类:
参数多态、
包含多态、
重载多态、
强制多态
参数多态
如函数模板和类模板。由函数模板实例化的各个函数都具有相同的操作,而这些函数的参数类型却各不相同。同样地,由类模板实例化的各个类都具有相同的操作,而操作对象的类型是各不相同的。
包含多态
是研究类族中定义于不同类中的同名成员函数的多态行为,主要是通过虚函数来实现的。
重载多态
如函数重载、运算符重载等。前面我们学习过的普通函数及类的成员函数的重载都属于重载多态。
强制多态
是指将一个变元的类型加以变化,以符合一个函数(或操作)的要求,例如加法运算符在进行浮点数与整型数相加时,首先进行类型强制转换,把整型数变为浮点数再相加的情况,就是强制多态的实例。
向上类型转换
是指把一个派生类的对象作为基类的对象来使用
。
向上转型中有三点需要我们特别
注意:
向上类型转换是安全的。
向上类型转换可以自动完成。
向上类型转换的过程中会丢失子类型信息。
double ←── float 高
↑
long
↑
unsigned
↑
int ←── char,short 低
将一个非unsigned整型数据赋给长度相同的unsigned型变量时, 内部存储形式不变,但外部表示时总是无符号的。
当发生数据类型转换时。最好显式表示出来,能够从程序上看出来想干什么。同时应该仔细审查因数据类型隐式转换后带来的bug。
多态性从实现的角度来讲可以划分为两类:
编译时的多态性
和
运行时的多态性
。
在
C++
中,
编译时的多态性
主要是
通过函数重载和运算符重载实现
的。
运行时的多态性
主要是
通过虚函数来实现
的。
功能早绑定
(
编译时的多态性
):
绑定是在程序编译阶段完成。
功能早绑定时,系统用实参与形参进行匹配,对于同名的重载函数便根据参数上的差异进行区分,然后进行绑定,从而实现了编译时的多态性。
功能晚绑定
(
运行时的多态性
):
绑定是在程序运行阶段完成的。即当程序调用到某一函数名时,才去寻找和连接其程序代码,对面向对象程序设计而言,就是当对象接收到某一消息时,才去寻找和连接相应的方法。
虚函数的定义是在基类中进行的,在成员函数原型的声明语句之前冠以关键字virtual,从而提供一种接口。
虚成员函数的定义语法是:
virtual函数类型 函数名(形参表)
{
函数体
}
当基类中的某个成员函数被声明为虚函数后,此虚函数就可以在一个或多个派生类中被重新定义。
在派生类中重新定义时
,
其函数原型,包括返回类型、函数名、参数个数、参数类型的顺序,都必须与基类中的原型完全相同。
虚函数的作用
是允许在派生类中重新定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和派生类中的同名函数
。
具体做法是,首先在基类中声明这个成员函数为虚函数,也就是在这个成员函数前面缀上关键字
virtual
,并在派生类中被重新定义,就能实现动态调用的功能。
#include <iostream>
using namespace std;
class Point
{public:
Point(double a = 0, double b = 0) {x = a; y = b; }
virtual double Area( )
{cout << "Call Point's Area function." << endl;
return 0.0; }
protected:
double x, y;};
class Rectangle: public Point
{public:
Rectangle(double a = 0, double b = 0, double c = 0,
double d = 0): Point(a, b)
{ x1 = c; y1 = d; }
double Area( )
{cout << "Call Rectangle's Area function." << endl;
return (x1 - x)*(y1 - y); }
protected:
double x1, y1; };
class Circle: public Point
{public:
Circle(double a = 0, double b = 0,
double c = 0): Point(a, b){ r = c; }
double Area( )
{ cout << "Call Circle's Area function." << endl;
return 3.14*r*r; }
protected:
double r;
};
double CalcArea(Point &ref)
{ return(ref.Area( ) ); }
int main( )
{ Point p(0, 0);
Rectangle r(0, 0, 1, 1);
Circle c(0, 0, 1);
cout << CalcArea(p) << endl;
cout << CalcArea(r) << endl;
cout << CalcArea(c) << endl;
return 0;}
程序运行结果如下:
Call Point's Area function.
0
Call Rectangle's Area function.
1
Call Circle's Area function.
3.14
为什么把基类中的
Area
函数定义为虚函数时,程序的运行结果就正确了呢?这是因为,关键字“
virtual”
指示
C++
编译器,函数调用“
ref. Area( )”
要在运行时确定所要调用的函数,即要对该调用进行功能晚绑定。因此,程序在运行时根据引用
ref
所引用的实际对象,调用该对象的成员函数。
有一个交通工具类vehicle,将它作为基类派生出汽车类motor_vehicle,再将汽车类motor_vehicle作为基类派生出小汽车类car和卡车类truck,声明这些类并定义一个虚函数用来显示各类信息。程序如下:
#include <iostream>
using namespace std;
class vehicle //基类vehicle声明
{public:
virtual void message() //虚成员函数
{ cout<<"vehicle message "<<endl; }
private:
int wheels; //车轮个数
float weight; //车重
};
class motor_vehicle: public vehicle
{public:
void message()
{ cout<<" motor_ vehicle message "<<endl; }
private: int passengers; //承载人数
};
class car: public motor_vehicle
{public:
void message(){ cout<<"car message "<<endl;}
private: float engine; //发动机的马力数
};
class truck: public motor_vehicle
{public:
void message(){cout<<" truck message "<<endl;}
private: int loadpay ; //载重量
};
int main()
{ vehicle v,*p;
motor_vehicle m;
car c;
truck t;
p=&v;
p->message();
p=&m;
p->message();
p=&c;
p->message();
p=&t;
p->message();
return 0;
}
C++
规定,如果在派生类中,没有用
virtual
显式地给出虚函数声明,这时系统就会遵循以下的规则来判断一个成员函数是不是虚函数:
(
1
)该函数与基类的虚函数
有相同的名称
。
(
2
)该函数与基类的虚函数
有相同的参数个数及相同的对应参数类型
。
(
3
)该函数与基类的虚函数
有相同的返回类型或者满足赋值兼容规则的指针、引用型的返回类型
。
说明:
(1)
通过定义虚函数来使用
C++
提供的多态性机制时,派生类应该从它的基类公用派生。之所以有这个要求,是因为我们是在赋值兼容规则的基础上来使用虚函数的,而赋值兼容规则成立的前提条件是派生类从其基类公用派生。
(2)
必须首先在基类中定义虚函数。
由于
“
基类
”
与
“
派生类
”
是相对的,因此,这项说明并不表明必须在类等级的最高层类中声明虚函数。在实际应用中,应该在类等级内需要具有动态多态性的几个层次中的最高层类内首先声明虚函数。
(3)
在派生类对基类中声明的虚函数进行重新定义时,关键字
virtual
可以写也可以不写。
但为了增强程序的可读性,最好在对派生类的虚函数进行重新定义时也加上关键字
virtual
。
如果在派生类中没有对基类的虚函数重新定义,则派生类简单地继承其直接基类的虚函数。
(4)
虽然使用对象名和点运算符的方式也可以调用虚函数,例如语句:
c.message
();
可以调用虚函数
car:: message ()
。但是这种调用是在编译时进行的功能早绑定,它没有充分利用虚函数的特性。只有通过
基类指针
或
基类引用
访问虚函数时才能获得运行时的多态性。
(5)
一个虚函数无论被公用继承多少次,它仍然保持其虚函数的特性。
(6)
虚函数必须是
其所在类的成员函数
,而不能是友元函数,也不能是静态成员函数,因为虚函数调用要靠特定的对象来决定该激活哪个函数。但是虚函数可以在
另一个类
中被声明为
(
7
)内联函数不能是虚函数,因为内联函数是不能在运行中动态确定其位置的。即使虚函数在类的内部定义,编译时仍将其看作是非内联的。
友元函数。
(
8
)构造函数不能是虚函数。因为虚函数作为运行过程中多态的基础,主要是针对对象的,而构造函数是在对象产生之前运行的,因此虚构造函数是没有意义的。
(
9
)析构函数可以是虚函数,
而且通常说明为虚函数
。
在析构函数前面加上关键字
virtual
进行说明,则称该析构函数为虚析构函数。虚析构函数的声明语法为:
virtual
~
类名
();
当基类的析构函数为虚函数时,无论指针指向的是同一类族中的哪一个类对象,系统都会采用动态关联,调用相应的析构函数,对该对象所涉及的额外内存空间进行清理工作。最好把基类的析构函数声明为虚函数,这将使所有派生类的析构函数自动成为虚函数。这样,如果程序中显式地用了
delete
运算符准备删除一个对象,而
delete
运算符的操作对象用了指向派生类对象的基类指针,则系统会首先调用派生类的析构函数,再调用基类的析构函数,这样整个派生类的对象被完全释放。
在一个派生类中重新定义基类的虚函数不同于一般的函数重载:
函数重载处理的是同一层次上的同名函数问题,而虚函数处理的是同一类族中不同派生层次上的同名函数问题,前者是横向重载,后者可以理解为纵向重载。但与重载不同的是
:
同一类族的虚函数的首部是相同的,而函数重载时函数的首部是不同的
(
参数个数或类型不同
)
。
重载函数可以是成员函数或普通函数,而虚函数只能是成员函数;
重载函数的调用是以所传递参数序列的差别作为调用不同函数的依据;虚函数是根据对象的不同去调用不同类的虚函数;
虚函数在运行时表现出多态功能,这是
C++
的精髓;而重载函数则在编译时表现出多态性。
纯虚函数是一个在基类中说明的虚函数,它在该基类中没有定义,但要求在它的派生类中必须定义自己的版本
纯虚函数的定义形式如下:
class 类名
{ …
virtual函数类型 函数名(参数表)=0;
…
};
定义一个公共基类 Shape ,它表示一个封闭平面几何图形。然后,从 Shape 类派生出三角形类 Trianglet 、矩形类 Rectangle 和圆类 Circle ,在基类中定义纯虚函数 show() 和 area() ,分别用于显示图形信息和求相应图形的面积,并在派生类中根据不同的图形实现相应的函数。要求实现运行时的多态性#include <cmath>
#include<iostream>
using namespace std;
const double PI=3.1415926535;
class Shape//形状类
{ public:
virtual void show()=0;
virtual double area()=0;
};
class Rectangle:public Shape //矩形类
{public:
Rectangle(){length=0; width=0; }
Rectangle(double len, double wid)
{ length= len;width = wid; }
double area(){return length*width;} //求面积
void show() { cout<<"length="<<length<<"\twidth="<<width<<endl; }
private:
double length, width; //长宽
};
class Triangle:public Shape//三角形类
{public:
Triangle(){a=0;b=0;c=0;}
Triangle(double x,double y,double z)
{ a = x;b = y;c = z; }
double area()//求面积
{ double s=(a+b+c)/2.0;
return sqrt(s*(s-a)*(s-b)*(s-c));
}
void show() {cout<<"a="<<a<<"\tb="<<b<<"\tc="<<c<<endl;}
private:
double a, b,c; //三角形三边长
};
class Circle :public Shape//圆类
{public:
Circle(){radius = 0; }
Circle(double r){radius = r;}
double area(){return PI*radius*radius;}
void show(){cout<<"radius="<<radius<<endl;}
private:
double radius;
};
int main()
{ Shape *s;
Circle c(10);
Rectangle r(6,8);
Triangle t(3,4,5);
c.show();//静态多态
cout<<"圆面积:"<<c.area()<<endl;
s=&r;//动态多态
s->show();
cout<<"矩形面积:"<<s->area()<<endl;
s=&t;//动态多态
s->show();
cout<<"三角形面积:"<<s->area()<<endl;
return 0;
}
如果一个类至少有一个纯虚函数,那么就称该类为抽象类。
对于抽象类的使用有以下几点规定:
(
1
)由于抽象类中至少包含一个没有定义功能的纯虚函数。因此,抽象类只能作为其他类的基类来使用,不能建立抽象类对象,这只能用来为派生类提供一个接口规范
,其纯虚函数的实现由派生类给出。
(
2
)不允许从具体类派生出抽象类,所谓具体类,就是不包含纯虚函数的普通类。
(
3
)抽象类不能用作参数类型、函数返回类型或显式转换的类型。
(
4
)可以声明指向抽象类的指针或引用,此指针可以指向它的派生类,进而实现动态多态性。
(
5
)如果派生类中没有重新定义纯虚函数,则派生类只是简单继承基类的纯虚函数,则这个派生类仍然是一个抽象类。如果派生类中给出了基类所有纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可以创建对象的具体类。
(
6
)在抽象类中也可以定义普通成员函数或虚函数,虽然不能为抽象类声明对象,但仍然可以通过派生类对象来调用这些不是纯虚函数的函数。
抽象类了,它是一个可以创建对象的具体类。
抽象类是一种特殊的类,它为一族类提供统一的操作接口。抽象类是为了抽象和设计的目的而建立的,可以说,建立抽象类,就是为了通过它多态地使用其中的成员函数。抽象类处于类层次的上层,一个抽象类自身无法实例化,也就是说我们无法声明一个抽象类的对象,而只能通过继承机制,生成抽象类的非抽象派生类,在该派生类实现了抽象类中的所有纯虚函数的情况下才可以实例化。
虚函数的工作原理:
虚函数表
和
虚函数指针
(了解)