520 likes | 666 Views
第八章 多态性. C++ 语言程序设计. 本章主要内容. 多态性 运算符重载 虚函数 纯虚函数 抽象类. 多态性的概念. 多态性是面向对象程序设计的重要特征之一。 多态性是指发出同样的消息被不同类型的对象接收时导致完全不同的行为。 多态从实现的角度划分为: 编译时的多态和运行时的多态 多态的实现: 函数重载 运算符重载 虚函数. 静态联编与动态联编. 静态联编:在编译连接阶段就确定了同名操作的具体对象的情况。重载 动态联编:在程序运行过程中才动态地确定操作所针对的具体对象的情况。虚函数. 问题举例 —— 复数的运算. 运算符重载.
E N D
第八章 多态性 C++语言程序设计
本章主要内容 • 多态性 • 运算符重载 • 虚函数 • 纯虚函数 • 抽象类
多态性的概念 • 多态性是面向对象程序设计的重要特征之一。 • 多态性是指发出同样的消息被不同类型的对象接收时导致完全不同的行为。 • 多态从实现的角度划分为: • 编译时的多态和运行时的多态 • 多态的实现: • 函数重载 • 运算符重载 • 虚函数
静态联编与动态联编 • 静态联编:在编译连接阶段就确定了同名操作的具体对象的情况。重载 • 动态联编:在程序运行过程中才动态地确定操作所针对的具体对象的情况。虚函数
问题举例——复数的运算 运算符重载 class complex //复数类声明 { public: complex(double r=0.0,double i=0.0){real=r;imag=i;}//构造函数 void display( ); //显示复数的值 private: double real; double imag; };
问题举例——复数的运算 运算符重载 • 用“+”、“-”能够实现复数的加减运算吗? • 实现复数加减运算的方法——重载“+”、“-”运算符
运算符重载的实质 运算符重载 • 运算符重载是对已有的运算符赋予多重含义 • 必要性 • C++中预定义的运算符其运算对象只能是基本数据类型,而不适用于用户自定义类型(如类) • 实现机制 • 将指定的运算表达式转化为对运算符函数的调用,运算对象转化为运算符函数的实参。 • 编译系统对重载运算符的选择,遵循函数重载的选择原则。 • class A{……}; A c, a, b; c=a+b; • c=a+b等价于c=a.operator+(b);或c=operator+(a,b);
规则和限制 运算符重载 • 可以重载C++中除下列运算符外的所有运算符:. .* :: ?: sizeof • 只能重载C++语言中已有的运算符,不可臆造新的。 • 书上P33已给出了C++中全部运算符 • 不改变原运算符的优先级和结合性。 • 不能改变操作数个数。 • 经重载的运算符,其操作数中至少应该有一个是自定义类型。
两种形式 运算符重载 • 用户定义类型的重载运算符,要求能访问运算对象的私有成员。为此只能用成员函数或友元函数两种形式定义运算符重载。 • 重载为类成员函数。 • 重载为友元函数。
运算符函数 运算符重载 • 声明形式 函数类型 operator运算符(形参) { ...... } • 重载为类成员函数时 参数个数=原操作数个数-1 (后置++、--除外) • 重载为友元函数时 参数个数=原操作数个数,且至少应该有一个自定义类型的形参。
运算符成员函数的设计 运算符重载 • 双目运算符 B • 如果要重载 B 为A类成员函数,使之能够实现表达式 oprd1 B oprd2,其中oprd1 为A 类对象,则 B 应被重载为 A 类的成员函数,形参类型应该是 oprd2所属的类型。 • 经重载后,表达式oprd1 B oprd2相当于 oprd1.operator B(oprd2)
例 8.1 运算符重载 将“+”、“-”运算符重载为复数类的成员函数。 • 规则: • 实部和虚部分别相加减。 • 操作数: • 两个操作数都是复数类的对象。
#include<iostream.h> class complex //复数类声明 { public: //外部接口 complex(double 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= real + c2.real; c.imag= imag + c2.imag; return complex(c.real,c.imag); } //return c;
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( ) { cout<<"("<<real<<","<<imag<<")"<<endl; }
void main( ) //主函数 { complex c1(5,4),c2(2,10),c3; //声明复数类的对象 cout<<"c1="; c1.display( ); cout<<"c2="; c2.display( ); c3=c1-c2; //使用重载运算符完成复数减法 //等价于c3=c1.operator-(c2); 即operator-(&c1,c2) cout<<"c3=c1-c2="; c3.display( ); c3=c1+c2; //使用重载运算符完成复数加法 //等价于c3=c1.operator+(c2); cout<<"c3=c1+c2="; c3.display( ); }
成员函数operator+( )中的形参用类对象的引用与用类对象有什么不同吗? complex operator+(complex& c); complex operator+(complex c); c=c1+c2;即c=c1.operator+(c2); 程序输出的结果为: c1=(5,4) c2=(2,10) c3=c1-c2=(3,-6) c3=c1+c2=(7,14)
运算符成员函数的设计 运算符重载 • 前置单目运算符 U • 如果要重载 U 为类成员函数,使之能够实现表达式 U oprd,其中oprd 为A类对象,则 U 应被重载为 A 类的成员函数,无形参。 • 经重载后,表达式U oprd相当于 oprd.operator U( )
运算符成员函数的设计 运算符重载 • 后置单目运算符 ++和-- • 如果要重载 ++或--为类成员函数,使之能够实现表达式 oprd++或 oprd--,其中oprd 为A类对象,则 ++或-- 应被重载为 A 类的成员函数,且具有一个 int 类型形参(它只是为了区别前增量与后增量)。 • 经重载后,表达式 oprd++相当于 oprd.operator ++(0) • 而++oprd相当于oprd.operator++( )
例8.2 运算符重载 • 运算符前置++和后置++重载为时钟类的成员函数。 • 前置单目运算符,重载函数没有形参,对于后置单目运算符,重载函数需要有一个整型形参。 • 操作数是时钟类的对象。 • 实现时间增加1秒钟。
#include<iostream.h> class Clock //时钟类声明 { public: //外部接口 Clock(int NewH=0, int NewM=0, int NewS=0); void ShowTime( ); void operator ++( ); //前置单目运算符重载 void operator ++(int); //后置单目运算符重载 private: //私有数据成员 int Hour, Minute, Second; };
void Clock::operator ++( ) //前置单目运算符重载函数 { Second++; if(Second>=60) { Second=Second-60; Minute++; if(Minute>=60) { Minute=Minute-60; Hour++; Hour=Hour%24; } } cout<<"++Clock: "; }
void Clock::operator ++(int) //后置单目运算符重载 { Second++; if(Second>=60) { Second=Second-60; Minute++; if(Minute>=60) { Minute=Minute-60; Hour++; Hour=Hour%24; } } cout<<"Clock++: "; }
//其它成员函数的实现略 void main( ) { Clock myClock(23,59,59); cout<<"First time output:"; myClock.ShowTime( ); myClock++; //等价于myClock.operator++(0); myClock.ShowTime( ); ++myClock; //等价于myClock.operator++( ); myClock.ShowTime( ); }
程序运行结果为: First time output:23:59:59 Clock++: 0:0:0 ++Clock: 0:0:1
运算符友元函数的设计 运算符重载 • 如果需要重载一个运算符,使之能够用于操作某类对象的私有成员,可以将此运算符重载为该类的友元函数。 • 函数的形参代表依自左至右次序排列的各操作数。 • 后置单目运算符 ++和--的重载函数,形参列表中要增加一个int,但不必写形参名。
运算符友元函数的设计 运算符重载 • 双目运算符 B重载后,表达式oprd1 B oprd2 等同于operator B(oprd1,oprd2 ) • 前置单目运算符 B重载后,表达式 B oprd等同于operator B(oprd ) • 后置单目运算符 ++和--重载后,表达式 oprd B等同于operator B(oprd,0 )
例8-3 运算符重载 • 将+、-(双目)重载为复数类的友元函数。 • 两个操作数都是复数类的对象。
#include<iostream.h> class complex //复数类声明 { public: //外部接口 complex(double r=0.0,double i=0.0) { real=r; imag=i; } //构造函数 friend complex operator + (complex c1,complex 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) //运算符重载友元函数实现 { return complex(c1.real-c2.real, c1.imag-c2.imag); } // 其它函数和主函数同例8.1
静态联编与动态联编 • 联编(binding): • 程序自身彼此关联的过程,确定程序中的操作调用与执行该操作的代码间的关系。 • 静态联编(静态束定static binding) • 联编工作出现在编译阶段,用对象名或者类名来限定要调用的函数。 • 动态联编(dynamic binding) • 联编工作在程序运行时执行,在程序运行时才确定将要调用的函数。 例 例
虚函数 • 虚函数是动态联编的基础。 • 它是非静态的成员函数。 • 静态成员函数和友元函数都不能说明为虚函数。 • 在类的声明中,在函数原型之前写virtual。 • virtual只用来说明类声明中的函数原型,不能用在函数实现时。 • 具有继承性,基类中声明了虚函数,派生类中无论是否说明,同原型函数都自动为虚函数。 • 本质:不是重载声明而是覆盖。 • 调用方式:通过基类指针或引用,执行时会根据指针指向的对象的类,决定调用哪个函数。
例 8.4 #include <iostream.h> class B0 //基类B0声明 { public: //外部接口 virtual void display( ) {cout<<"B0::display( )"<<endl;} //虚成员函数 };
class B1: public B0 //公有派生 { public: void display( ) { cout<<"B1::display( )"<<endl; } }; class D1: public B1 //公有派生 { public: void display( ) { cout<<"D1::display( )"<<endl; } };
B0 void fun(B0 *ptr) //普通函数 { ptr->display( ); } void main( ) //主函数 { B0 b0, *p; //声明基类对象和指针 B1 b1; //声明派生类对象 D1 d1; //声明派生类对象 p=&b0; fun(p); //调用基类B0成员函数 p=&b1; fun(p); //调用派生类B1成员函数 p=&d1; fun(p); //调用派生类D1成员函数 } B1 D1
程序的运行结果为: B0::display( ) B1::display( ) D1::display( )
虚函数的限制 • 一个虚函数是属于它所在的类层次结构的,而不是只属于某一个类,只不过它在该类层次结构中的不同类中具有不同的形态。一旦一个函数被声明为虚函数,不管经历多少次派生,仍将保持其虚特性。即一个接口,多个形态。 • 若派生类中没有对基类中说明的虚函数进行重新定义,则它继承基类中的虚函数。 • 构造函数不能是虚函数,因为构造时,对象还是一片未定型的空间。只有在构造完成后,对象才能成为一个类的名副其实的实例。
虚析构函数 • 不能声明虚构造函数,但可声明虚析构函数。 • 虚析构函数的声明语法:virtual ~类名(); • 若一个类的析构函数是虚函数,则由它派生的所有子类的析构函数也是虚函数。 • 析构函数设置为虚函数之后,在使用指针引用时可以动态联编,实现运行时的多态。 • 例:当基类对象和子类对象以不同方式申请了堆空间后 • void fun(Base *p){ delete p;} 其中p是传递过来的一个对象指针,它或者指向基类对象或者指向子类对象。在执行delete p时,要调用析构函数,但执行基类的析构函数?还是执行子类的析构函数?将析构函数声明为虚的,即可。
虚函数的实现——虚表 • C++的多态性就是要通过动态联编的方法正确调用继承家族中那些同名的虚函数。C++使用了一个类似函数指针表的动态联编表(称为虚表),用vtable表示 • 若一个类含有虚函数,则编译器在编译时将为它产生一个虚表。若基类中含有虚函数,则它以及它的每个派生类都将含有自己的虚表。 • 虚表中存放了它所在类的所有虚函数的指针,包括它从基类继承来的虚函数。 • 每个含有虚函数的类对象都在其数据成员中增加一个指针成员vptr,指向该类的虚表(vtable)。
ca对象的内存分配 类C的虚表 vtable A::a C::c *vptr A::funA() (*funA)() (*show)() show() 虚表指针示意图 class A {public: int a; virtual void funA(); virtual void show(); }; class C:public A {int c; public: void funC(); void show(); }; void main() { C ca; A *pa=&ca; pa->show(); } 通过基类指针对虚函数的调用将由编译器转换成一个间接调用。pa->show()将变成 (*(pa->vptr[1]))() 由于pa->vptr[1]==C::show,所以尽管指针pa为A类指针,它所指向的对象却是C类对象,通过它产生的show()调用仍然是C::show(),而非A::show()
抽象类的一般形式 纯虚函数与抽象类 带有纯虚函数的类称为抽象类: class 类名 { virtual类型 函数名(参数表)=0; //纯虚函数 ... }; 纯虚函数是一种特殊的虚函数,它没有具体实现。 其纯虚函数的实现由其派生类给出。
作用 纯虚函数与抽象类 • 抽象类为抽象和设计的目的而建立,将有关的数据和行为组织在一个继承层次结构中,保证派生类具有要求的行为。 • 对于暂时无法实现的函数,可以声明为纯虚函数,留给派生类去实现。 • 例:由形状类派生出矩形类和圆形类,每个类中都有求面积的函数area,在基类中该函数就定义为纯虚函数
注意 纯虚函数与抽象类 • 抽象类一般作为基类来使用,其纯虚函数的实现由其派生类给出。 • 不能声明抽象类的对象。但可声明抽象类的指针或引用。 • 构造函数不能是虚函数,析构函数可以是虚函数。
例 8.5 纯虚函数与抽象类 #include <iostream.h> 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( ); } void main( ) //主函数 { B0 *p; //声明抽象基类指针,不能声明基类对象 B1 b1; //声明派生类对象 D1 d1; //声明派生类对象 p=&b1; fun(p); //调用派生类B1函数成员 p=&d1; fun(p); //调用派生类D1函数成员 }
程序的运行结果为: B1::display( ) D1::display( ) 见显示器类.cpp和形状类.cpp
本章小结 • 多态: • 同样的消息被不同类型的对象接收时导致完全不同的行为,是对类的特定成员函数的再抽象。 • 运算符重载 • 对已有的运算符赋予多重含义,使用已有运算符对用户自定义类型(比如类)进行运算操作。
本章小结 • 联编 • 程序自身彼此关联的过程称为联编,联编确定程序中的操作调用与执行该操作的代码间的关系。 • 静态联编工作出现在编译阶段。 • 动态联编工作在程序运行时执行。 • 虚函数是动态联编的基础。
本章小结 • 纯虚函数 • 在基类中说明的虚函数,它在该基类中可以不给出函数体,要求各派生类根据实际需要编写自己的函数体。 • 抽象类 • 带有纯虚函数的类是抽象类。 • 抽象类的主要作用是通过它为一个类族建立一个公共的接口,使它们能够更有效地发挥多态特性。