1、,第4章 多态性,4.1 多态性概述 多态性的概念:(polymorphisn) 是指多种表现形式,具体地说,就是把同样的消息发给不同类型的对象后可能导致完全不同的行为,即“一个对外接口,对应多个内在实现方法”。多态性连同前面所讲过的封装性和继承性一起,构成了面向对象程序设计的三大基本特征。 多态性的实现: 函数重载 运算符重载 虚函数 从系统实现角度来看,多态性包括静态多态性(编译时多态性)和动态多态性(运行时多态性)两大类。静态多态性:是通过函数重载和类属机制来实现的。在程序编译时系统就能确定调用哪个函数,因此静态函数又称编译时的多态性。例如:函数重载和运算符重载就属于这种情况。 动态多态
2、性:是通过虚函数、继承机制,以及动态绑定机制来实现的。在程序运行过程中才动态地确定操作指针所指的对象,主要通过虚函数和重写来实现。,4.2 运算符重载,4.2.1 运算符重载的概念 运算符重载:是指将同一个运算符作用于不同的运算对象时,可以实现不同的操作。其意义在于,使程序的表达方式更加符合人们平时的表达习惯,易于接受。 运算符具有函数的特性,我们可以将一个运算符看成是一个函数名,我们使用一个运算符,就相当于是在调用一个函数。比如:3+2可以理解成+(3,2),即调用函数+,并给出两个实参3和2。 为此,运算符重载和函数重载本质上就是相同的。 4.2.2 运算符重载的规则 1、有5种运算符不能
3、重载,它们是类属关系运算符“.”,成员指针运算符“*”,作用域分辨符“:”,sizeof运算符和条件运算符“?:”。 2、重载后的运算符有四个“不能改变”:不能改变运算符原有的优先级;不能改变运算符原有的结合性;不能改变运算符原有的语法结构;不能改变运算符操作数的个数。 3、至少要有一个操作对象是自定义类型,否则就不必重载了。,4.2.3 运算符重载的语法 返回类型 类名:operator 操作符(形参表) 4.2.4 运算符重载的方法 1、用类成员函数实现运算符重载:其中=、 、( )、-等运算符必须采用这种方法实现重载。 用类成员函数实现重载时,参数个数=原操作数个数-1(后置+、-除外)
4、。 比如:要重载一个双目运算符P为类成员函数,使之能够实现表达式 oprd1 P oprd2的运算,其中 假定oprd1 为A 类的对象,则P就应该被重载为 A 类的成员函数,形参类型应该是oprd2所属的类型。经重载后,执行表达式 oprd1 P oprd2就相当于是执行oprd1.operatorP(oprd2)。,又如:要重载前置单目运算符P为类成员函数,使之能够实现表达式P oprd的运算,其中假定oprd 为A类的对象,则P就应该被重载为 A 类的成员函数,没有形参。经重载后,执行表达式P oprd就相当于执行oprd.operatorU( ) 。 再如:要重载后置单目运算符P(+或
5、-)为类成员函数,使之能够实现表达式 oprd P,其中假定oprd 为A类的对象,则P就应该被重载为 A 类的成员函数,且具有一个int类型的形参。经重载后,执行表达式oprd P就相当于执行oprd.operator P(0) 。这里的参数个数与重载前置单目运算符时不相同,以此区分前置的P。,4.2.5 运算符重载的例子,例1 请用类成员函数重载“+”、“-”两个双目运算符,使之可以用于复数的运算。 #include using namespace std;/可用#include来替换这两行 class complex /复数类声明 public: /外部接口complex(double
6、r=0.0,double i=0.0)real=r;imag=i; complex operator+ (complex c2); /+重载为成员函数complex operator- (complex c2); /-重载为成员函数void display(); /输出复数 private: /私有数据成员,自己内部使用double real; /复数实部double imag; /复数虚部 ;,complex complex:operator+(complex c2) complex c;c.real=c2.real+real;c.imag=c2.imag+imag;return compl
7、ex(c.real,c.imag); complex complex:operator-(complex c2) complex c;c.real=real-c2.real;c.imag=imag-c2.imag;return complex(c.real,c.imag); void complex:display() coutreal“+(“imag“)i“endl; ,void main() complex c1(2,3),c2(6,8),c3; /声明复数类的对象cout“c1=“; c1.display();cout“c2=“; c2.display();c3=c1-c2; /使用重载
8、运算符完成复数减法cout“c3=c1-c2=“;c3.display();c3=c1+c2; /使用重载运算符完成复数加法cout“c3=c1+c2=“;c3.display(); 运行结果: c1=2+(3)i c2=6+(8)i c3=c1-c2=-4+(-5)i c3=c1+c2=8+(11)i,例2 请用类成员函数分别重载单目运算符“+” 的前置和后置用法,使之可以用于时间的运算,不断地增加1秒。 #include using namespace std; class Clock /定义一个时钟类 public: Clock(int NewH=0, int NewM=0, int N
9、ewS=0);void ShowTime();Clock,Clock /将这个被前置运算后的对象通过引用返回 ,Clock Clock:operator +(int) /后置单目运算符重载函数 /注意形参表中的这个int参数,可以不写形参名,写了也不用 Clock old=*this;+(*this); /调用前置单目运算符重载函数return old; /其它成员函数的实现 Clock:Clock(int NewH, int NewM, int NewS) Hour=NewH; Minute=NewM; Second=NewS; void Clock:ShowTime() coutHour“
10、:“Minute“:“Secondendl; ,void main() Clock myClock(23,59,59);cout“Time:“;myClock.ShowTime();cout“Show +Time:“;(+myClock).ShowTime();cout“Show Time+:“;(myClock+).ShowTime(); 运行结果: Time:23:59:59 Show +Time:0:0:0 Show Time+:0:0:0,2、用友元函数实现运算符重载。 友元概念的引入: 按照封装性的概念,一个类之外的函数只能访问这个类中的public成员。如果要让这个类之外的函数访问
11、到自己的private或protected成员,就必须打破原有的封装性,方法就是设定该类的友元。应该说,友元和封装是一对相反的概念,一个是要“实现开放”,另一个则是要“限制开放”。 一个类的友元可以是一个普通的函数(即非成员函数)、另一个类的成员函数或者另一个完整的类。 如何设置一个类的友元呢?在类的定义中使用friend保留字进行说明,并在friend之后列出友元的名字。如果是把一个完整的类B作为类A的友元,则类B中所有的成员函数都将被视为类A的友元函数。,用友元函数实现重载时,参数个数=原操作数个数,且至少应该有一个自定义类型的形参。 双目运算符 P重载后,表达式oprd1 P oprd2
12、等同于operatorP(oprd1,oprd2 ) 前置单目运算符P重载后,表达式 P oprd等同于operatorP(oprd ) 后置单目运算符 P重载后,表达式 oprd P等同于operatorP(oprd,0 )例3 请用友元函数重载“+”、“-”两个双目运算符,使之可以用于复数的运算。,#include using namespace std; class complex public: complex(double r=0.0,double i=0.0) real=r; imag=i; friend complex operator+ (complex c1,complex
13、c2); /运算符+被重载为本类的友元函数friend complex operator- (complex c1,complex c2); /运算符-被重载为本类的友元函数void display(); /显示复数的值 private: /私有数据成员double real;double imag; ;,complex operator+(complex c1,complex c2) /友元函数实现运算符重载 return complex(c2.real+c1.real, c2.imag+c1.imag); complex operator-(complex c1,complex c2) /
14、友元函数实现运算符重载 return complex(c1.real-c2.real, c1.imag-c2.imag); / 其他函数和主函数同例1注意:这里的operator+和operator-是两个普通函数(非成员函数)。两种重载方法的比较: 一般说来,单目运算符最好被重载为成员函数;对双目运算符最好被重载为友元函数,双目运算符重载为友元函数比重载为成员函数更方便,但是,有的双目运算符还是重载为成员函数为好,例如,赋值运算符。因为,它如果被重载为友元函数,将会出现与赋值语义不一致的地方。,4.3 用虚函数实现多态性,4.3.1 绑定方式 绑定(binding):是指对于具有多种解释的名
15、字,将名字与它的某个含义相关联的过程。对于函数而言,就是将函数调用与某个函数体对应起来。 根据进行关联的时机不同,可将绑定分为早期绑定(又称静态绑定)和晚期绑定(又称动态绑定)。 静态绑定:绑定过程出现在编译阶段,在编译时就用对象名或者类名来限定要调用的函数。 动态绑定:绑定过程出现在运行阶段,在程序运行时才确定将要调用的函数。 在C+中,函数调用的默认绑定方式是静态绑定,只有通过基类类型的引用或指针调用被指定为虚函数的成员函数时才进行动态绑定,实现运行时多态性。 为此,可以看出,运行时多态性是通过虚函数、继承机制,以及动态绑定机制来实现的。要实现运行时多态性必须同时满足下面4个条件:,要有一
16、个继承层次; 在基类中要定义虚函数; 在公有派生类中要对基类中定义的虚函数进行重定义; 要通过基类指针(或基类引用)来调用虚函数。4.3.2 虚函数 虚函数:是指在类定义体中使用保留字virtual来声明的成员函数。这个含有虚函数的类,称为多态类。 注意: 1、virtual 只能用在类的声明中、函数原型之前,不能用在函数实现时。 2、派生类对基类中的虚函数进行重定义,既不是要实现重载也不是要实现隐藏,而是要实现覆盖。 例4 一个静态绑定的例子,#include using namespace std; class Point public:Point(double i, double j)
17、x=i; y=j;double Area() const return 0.0;private:double x, y; ; class Rectangle:public Point public:Rectangle(double i, double j, double k, double l);double Area() const return w*h;private:double w,h; ; Rectangle:Rectangle(double i, double j, double k, double l) :Point(i,j) w=k; h=l; ,void fun(Point
18、/派生类的对象 运行结果: Area=0,例5 一个动态绑定的例子,#include using namespace std; class Point public:Point(double i, double j) x=i; y=j;virtual double Area( ) const return 0.0;private:double x, y; ; class Rectangle:public Point public:Rectangle(double i, double j, double k, double l);virtual double Area( ) const retu
19、rn w*h;private:double w,h; ; Rectangle:Rectangle(double i, double j, double k, double l) :Point(i,j) w=k; h=l; ,void fun(Point /派生类的对象 运行结果: Area=375,3、virtual具有继承性,一旦基类中声明了虚函数,无论派生类中是否再声明,同原型的函数都自动成为虚函数。 例6 #include using namespace std; class B0 /基类B0声明 public: virtual void display() /虚成员函数cout“B0:
20、display()“endl; ; class B1: public B0 /公有派生 public:void display() cout“B1:display()“endl; ; class D1: public B1 /公有派生 public:void display() cout“D1:display()“endl; ;,void fun(B0 *ptr) /普通函数,基类指针 ptr-display(); int main() B0 b0, *p; /声明基类指针p和对象b0B1 b1; /声明派生类对象D1 d1; /声明派生类对象p= /调用派生类D1函数成员 运行结果: B0:
21、display() B1:display() D1:display(),4、什么时候需要虚析构函数 如果某个类不包含虚函数时,那这个类一般不要用作基类。为此,当已知一个类不会用作基类时,其析构函数最好不要设置为虚析构函数。因为它会为这个类增加一个虚函数表,使得对象的体积翻倍,还有可能降低其可移植性。 所以基本的一条是:无故的声明虚析构函数和永远不去声明一样都是错的。实际上,很多人这样总结:当且仅当类里包含至少一个虚函数的时候才去声明虚析构函数。 虚析构函数是为了解决这样的一个问题: 基类的指针指向派生类对象,并用基类的指针删除派生类对象。,例7 使用虚析构函数的例子 #include clas
22、s abstract_base public: virtual abstract_base() cout“abstract_base:abstract_base() is called!“endl; ;,说明: 例子中,由于b是基类的对象,则在delete b时一般情况下应该去调用基类的析构函数,但这样的话,派生类的析构函数就得不到调用,所以要打破原来的这种析构顺序,就必须把基类的析构函数设置为虚拟的,这样在delete b时才会先调用派生类的析构函数,再调用基类的析构函数。,class concrete_derived:public abstract_base public: concret
23、e_derived() cout“concrete_derived:concrete_derived() is called!“endl; ; void main() concrete_derived *a = new concrete_derived; abstract_base *b = a; delete b; ,4.3.3 纯虚函数与抽象类,纯虚函数(pure virtual function):是指基类中的虚函数只在基类中作了函数声明而没有具体的函数定义,要求其所有的派生类都必须定义自己的版本。这种虚函数就叫纯虚函数。 语法: virtual 返回值类型 函数名(形参表)=0; 包含
24、有纯虚函数的类,就称为抽象类(abstract class)。抽象类是要被用作基类的,因此抽象类中必须要有一个虚析构函数。 抽象类的特性: 只能用作其他类的基类; 不能用于直接创建对象; 不能用作函数的形参类型和返回值类型; 不能用于强制类型转换; 可以用于定义抽象类的指针和引用。,例8 纯虚函数与抽象类举例 #include using namespace std; class B0 /抽象基类B0声明 public:virtual void display( )=0; /纯虚函数声明 ; class B1: public B0 /公有派生 public:void display()cout“B1:display()“endl; /在派生类中为基类的纯虚函数定义一个具体的版本,也是一个虚函数 ; class D1: public B1 /公有派生 public:void display()cout“D1:display()“endl; ;,void fun(B0 *ptr) /普通函数 ptr-display(); int main() B0 *p; /声明抽象基类指针B1 b1; /声明派生类对象D1 d1; /声明派生类对象p= /调用派生类D1函数成员 运行结果: B1:display() D1:display(),