C++程序设计基础 主讲人:谢昕 华东交通大学信息工程学院 第十~十二讲 多态性和虚函数 2005年春季学期
主要内容 函数重载 运算符重载 静态联编和动态联编 虚函数 纯虚函数和抽象类 虚析构函数 程序实例
8.1 函数重载 多态性的概念 函数重载
多态性的概念 多态性是面向对象程序设计的重要特征之一。 多态性是指发出同样的消息被不同类型的对象接收时导致完全不同的行为。 多态的实现: 函数重载 运算符重载 虚函数 类模板
函数重载 在C++中,多态定义为不同函数的同一接口。从这个定义出发,函数和操作符的重载也属于多态。 注意: (1)作为重载函数至少在参数个数、参数类型上有所不同。若仅在返回类型上不同,编译器是无法区别的。 (2)重载函数一般应具有相同的功能,否则会破坏程序的可读性。 (3)在重载函数中使用缺省函数参数要注意二义性。 如: void print(int a,int b); void print(int a,int b,int c=50); print(10,100);
8.2 运算符重载 运算符重载的实质与实现机制 运算符重载的两种方式 运算符重载规则 单目运算符重载(前缀和后缀) 双目运算符重载 其它运算符重载:=、()、[]、>>、<<
问题 复数的运算 class complex //复数类声明 { public: 用+、-能够实现复数的加减运算吗? class complex //复数类声明 { public: complex(double r=0.0,double i=0.0) //构造函数 {real=r;image=i;} void display(); //显示复数的值 private: double real; double image; }; 实现复数加减运算的方法:重载“+”、“-”运算符
8.2.1 运算符重载的实质 运算符重载 就是对已有的运算符赋予多重含义。 必要性 8.2.1 运算符重载的实质 运算符重载 就是对已有的运算符赋予多重含义。 必要性 C++中预定义的运算符其运算对象只能是基本数据类型,而不适用于用户自定义类型(如类)。 实现机制 将指定的运算表达式转化为对运算符函数的调用,运算对象转化为运算符函数的实参。 编译系统对重载运算符的选择,遵循函数重载的选择原则。
8.2.2 规则和限制 可以重载C++中除下列运算符外的所有运算符: . .* :: ?: .--> sizeof() 只能重载C++语言中已有的运算符,不可臆造新的。 不能改变原运算符的优先级和结合性。 不能改变操作数个数。 不能改变原有的语法结构。 经重载的运算符,其操作数中至少应该有一个是自定义类型。
friend <函数类型> operator <运算符>(<形参表>) 两种形式 重载为类的成员函数 重载为类的友元函数 一般而言: 单目运算符重载为成员函数 <函数类型> operator <运算符>(<形参表>) { <函数体>;} //成员函数形式 双目运算符重载为友元函数 friend <函数类型> operator <运算符>(<形参表>) { <函数体>; } //友元函数形式
运算符函数 定义形式 函数类型 operator 运算符(形参) { ...... } 重载为类成员函数时 参数个数=原操作数个数-1 (后置++、--除外) 重载为友元函数时 参数个数=原操作数个数,且至少应该有一个自定义类型的形参。
8.2.3 运算符成员函数的设计 双目运算符 B 如果要重载 B 为类成员函数,使之能够实现表达式 oprd1 B oprd2,其中 oprd1 为A 类对象,则 B 应被重载为 A 类的成员函数,形参类型应该是 oprd2 所属的类型。 相应的调用形式为: oprd1.operator B(oprd2)
例 8.2 将“+”、“-”运算重载为复数类的成员函数。 规则: 实部和虚部分别相加减。 操作数: 两个操作数都是复数类的对象。
#include<iostream.h> class complex //复数类声明 { public: //构造函数 complex(double r=0.0,double i=0.0){real=r;image=i;} complex operator + (complex &c); //+重载为成员函数 complex operator - (complex &c); //-重载为成员函数 void display(); //输出复数 private: double real; //复数实部 double image; //复数虚部 };
//重载函数实现 complex complex::operator +(complex &c) { complex c2; c2.real=c.real+real; c2.image=c.image+image; return complex(c2.real,c2.image); }
//重载函数实现 complex complex::operator -(complex c) { complex c2; c2.real=real-c.real; c2.imag=image-c.image; return complex(c2.real,c2.image); }
void complex::display() { cout<<"("<<real<<","<<image<<")"<<endl; } void main() { complex c1(5,4),c2(2,10),c3; //声明复数类的对象 cout<<"c1="; c1.display(); cout<<"c2="; c2.display(); c3=c1-c2; //使用重载运算符完成复数减法 cout<<"c3=c1-c2="; c3.display(); c3=c1+c2; //使用重载运算符完成复数加法 cout<<"c3=c1+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 类的成员函数,无形参。 相应的调用形式为: oprd.operator U()
运算符成员函数的设计 后置单目运算符 ++和— 如果要重载 ++或--为类成员函数,使之能够实现表达式 oprd++ 或 oprd-- ,其中 oprd 为A类对象。 则 ++或-- 应被重载为 A 类的成员函数,且作为双目运算符重载为: oprd++ 0或 oprd-- 0,具有一个 int 类型形参。 相应的调用形式为: oprd.operator ++(0)
8.2.4 单目运算符重载 1、 使用运算符前缀时,对对象(操作数)进行增量修改,然后再返回该对象。所以前缀运算符操作时,参数与返回的是同一个对象。 2、使用运算符后缀时,必须在增量之前返回原有的对象值。为此,需要创建一个临时对象,存放原有的对象,以便对操作数(对象)进行增量修改时,保存最初的值。运算符后缀操作返回的是原有对象的值,而不是原有对象,原有对象已经被增量修改,所以,返回的应该是存放原有对象值的临时对象。
++或--重载的格式 语法格式如下: <函数类型> operator ++(); //前缀运算 <函数类型> operator ++(int)//后缀运算 使用前缀运算符的语法格式如下: ++对象; //对象.operator++(); 使用后缀运算符的语法格式如下: 对象++; //对象.operator++(int);
区分前缀与后缀 #include<iostream.h> //用成员函数实现 class Integer { int a, b; public: Integer(int x, int y) { a=x; b=y;} Integer operator++( ) //前缀方式 { ++a; ++b; return *this;} Integer operator++(int x) //后缀方式 { Integer temp=*this; ++a; ++b; return temp; } void show() { cout<<a<<","<<b<<endl;} };
obj.operator ++( );// ++obj; obj.operator ++(0);// obj++; obj.show();} void main() { Integer obj(0,100); obj.show( ); Integer obj1=++obj; // obj.operator ++(); obj1.show(); obj.show(); Integer obj2=obj++; // obj.operator ++(0); obj2.show(); obj.operator ++( );// ++obj; obj.operator ++(0);// obj++; obj.show();} 输出结果: 0,100 1,101 2,102 4,104
例:时钟类计时程序 运算符前置++和后置++重载为时钟类的成员函数。 前置单目运算符,重载函数没有形参,对于后置单目运算符,重载函数需要有一个整型形参。 操作数是时钟类的对象。 实现时间增加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; };
Clock::Clock(int NewH, int NewM, int NewS) { //构造函数 if(0<=NewH&& NewH <24&& 0<=NewM&& NewM <60&& 0<=NewS&& NewS <60) { Hour=NewH; Minute=NewM; Second=NewS; } else cout << "Time error! "<< endl;
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 ShowTime() { cout<<Hour<<":"<<Minute<<":"<<Second<<endl; } void main() Clock myClock(23,59,59); cout<<"First time output:"; myClock.ShowTime(); myClock++; //匹配后置重载 ++myClock; //匹配前置重载
程序运行结果为: First time output:23:59:59 Clock++: 0:0:0 ++Clock: 0:0:1
8.2.5 运算符友元函数的设计 这时,将没有隐含的参数this指针,对于双目运算符有两个参数,对单目运算符有一个参数。 注意: 重载一个运算符,使之能够用于操作某类对象的私有成员,还可以将此运算符重载为该类的友元函数。 这时,将没有隐含的参数this指针,对于双目运算符有两个参数,对单目运算符有一个参数。 注意: = ( ) [ ] ->不能以友元方式重载。 函数的形参依自左至右次序分别表示各操作数。 后置单目运算符 ++和--的重载函数,形参列表中要增加一个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; image=i; } friend complex operator + (complex &c1,complex &c2); //运算符+重载为友元函数 friend complex operator - (complex &c1,complex &c2); //运算符-重载为友元函数 void display(); private: double real; double image; };
complex operator +(complex &c1,complex &c2) //运算符重载友元函数实现 { return complex(c2.real+c1.real, c2.image+c1.image);} complex operator -(complex &c1,complex &c2) { return complex(c1.real-c2.real, c1.image-c2.image);}
两种重载形式的比较 单目运算符一般被重载为成员函数,双目运算符多数被重载为友员函数。 但在有些情况下,不便于重载为成员函数: 如:考虑 x + 27. 5( x为complex类的一个对象 ) 若重载为友元函数:operator + ( x , 27. 5 ) operator + ( x , complex(27. 5) ) 若重载为成员函数:x. operator + (27. 5) x. operator + (complex(27. 5)) 再考虑 27. 5 + x 若重载为友元函数:operator + ( 27. 5 , x ) operator + ( complex(27. 5) , x ) 若重载为成员函数:27. 5. operator + (x) // error
有些情况下,某些双目运算符不能被重载为友元函数。 例如:各种赋值运算符。(主要有:=、( )、[ ]、->四个) 将赋值运算符‘=’重载为complex类的一个友元函数complex operator = (complex& c1, const complex & c2) { c1. real = c2. real ; c1. image = c2. image ; return c1; } 则:x = y; // operator = (x,y); right 27. 5 = x ; // operator = (complex(27. 5),x) error
8.2.6 复杂运算符的重载 下标运算符[ ] 只能被重载为类的成员函数,且重载时只能显式声明一个参数 函数调用运算符() 可以将函数调用运算符看作下标运算符[ ]的扩展,可以带有零个或多个参数 例: 利用重载函数调用运算符实现 f(x,y)=x*y+5
重载下标运算符[ ] class Array { int *p,size; public: Array(int num) { size=(num>6) ? num:6; p=new int[size]; } ~Array() { delete [] p; } int & operator [](int idx) { if(idx<size) return p[idx]; else{ expend(idx-size+1); return p[idx]; } } void expend(int offset) { int *pi,num; pi=new int[size+offset]; for(num=0;num<size;num++) pi[num]=p[num]; delete[] p; p=pi; size=size+offset; } void contract(int offset) { size=size-offset; } }; //
重载下标运算符[ ] void main( ) { Array a_Array(10); for(int num=0;num<10;num++) a_Array[num]=num; //对象作数组使用 a_Array[10]=10; for(num=0;num<=10;num++) cout<<a_Array[num]<<endl; } 输出结果: 1 2 3 4 5 6 7 8 9 10
赋值运算符=的重载 1、“=”的原有含义:将赋值号右边表达式的结果拷贝给赋值号左边的变量。 2、重载后的意义:将赋值号右边对象的数据依次拷贝到赋值号左边对象的数据中。 说明:在正常情况下,系统会为每一个类自动生成一个默认的完成上述功能的赋值运算符,当然,这种赋值只限于由同一个类类型说明的对象之间赋值。 指针悬挂问题:如果一个类包含指针成员,采用这种默认的按成员赋值,那么当这些成员撤消后,内存的使用将变得不可靠。 解决办法:通过重载运算符“=”来解决。它必须被重载为成员函数。
赋值运算符重载 OK OK::operator=(OK t) { x=t.x; y=t.y; z=t.z; return *this; } class OK { int x,y,z; public: OK operator+(OK t); OK operator=(OK t); OK operator++(); void show(); void assign(int mx,int my,int mz); }; OK OK::operator +(OK t) { OK temp; temp.x=x+t.x; temp.y=y+t.y; temp.z=z+t.z; return temp; } OK OK::operator=(OK t) { x=t.x; y=t.y; z=t.z; return *this; } OK OK::operator ++() { ++x; ++y; ++z; return *this; } void OK::show( ) { cout<<"("<<x<<","<<y<<","<<z<<")"<<endl;} void OK::assign(int mx,int my,int mz) { x=mx; y=my; z=mz; }
void main( ) { OK a,b,c; a.assign(1,2,3); b.assign(10,10,10); a.show(); b.show(); c=a+b; c.show(); c=a+b+c; c.show(); c=b=a; c.show(); b.show(); ++c; } 输出结果: 1,2,3 10,10,10 11,12,13 22,24,26 2,3,4
( )运算符重载 通过重载()来实现函数的抽象: F(x,y)=(x+5)*y Class F { public: double operator ()(double x, double y) const; }; double F::operator ()(double x, double y) const { return (x+5)*y;} void main() { F f; cout<<f(15,2)<<endl;} 函数被解释为:f.operator()(15,2)
输入和输出运算符重载 1、在标准文件iostream.h中,有istream和ostream两个标准的类类型,其中: istream将“>>”重载为输入运算符,它对系统预定义类型(int,char,double等类型)进行了重载。 ostream类也将运算符“<<“重载为输出运算符。 而对于系统非预定义的类型,我们也可以重载运算符”<<“和“>>“来满足自己的要求。 输出运算符的第一个操作数cout,它实际上是ostream类的对象的引用。若用户定义ostream的引用scout,则也可以使用“<<”。如: ostream& scout=cout;//初始化自定义引用scout; scout<<num;
输入和输出运算符重载 class OK { int x,y,z; public: OK(int a,int b,int c) { x=a; y=b; z=c; } friend ostream& operator<<(ostream&scout ,OK obj) { scout<<obj.x<<" "; scout<<obj.y<<" "; scout<<obj.z<<endl; return scout; } friend istream& operator>>(istream&scin ,OK &obj) { cout<<"Enter x,y,z value:"<<endl; scin>>obj.x; scin>>obj.y; scin>>obj.z; return scin; } };
void main() { OK obj1(1,2,3); cout<<obj1; cin>>obj1; } 运行结果: 1 2 3 Enter x,y,z value: 10 20 30 10 20 30
指针悬挂就是new申请的存储空间无法访问,也无法释放。造成指针悬挂的原因是对指向new申请的存储的指针变量进行赋值修改 8.2.7 指针悬挂问题 指针悬挂就是new申请的存储空间无法访问,也无法释放。造成指针悬挂的原因是对指向new申请的存储的指针变量进行赋值修改 void main() { char *p,*q; p=new char[10]; q=new char[10]; strcpy(p,"abcd"); q=p; delete[] p; delete[] q; } a b c d \0 p q 存储空间无法访问, 也无法释放! delete[] p和delete[] q会导致同一块存储空间会释放两次。这是一个非常严重的错误!
指针悬挂问题举例 class String { char*pstr; int sz; public: String(int s) {pstr=new char[sz=s];} ~string() { delete[] pstr;} }; void main() { String str1(10); Stirng str2(20); ........ str2=str1; } //也会导致指针悬挂问题 解决指针悬挂问题的方法是重载运算符“=”。例如: String &operator = (String &s) { if(this= = &s) return *this; //防止自己赋给自己 delete pstr; pstr=new char[strlen(s.p)+1]; strcpy(pstr, s.pstr); return *this; } { p=s.p; } //缺省的“=”函数
8.3 静态联编与动态联编 联编 程序自身彼此关联的过程,确定程序中的操作调用与执行该操作的代码间的关系。 静态联编(静态束定) 联编工作出现在编译阶段,用对象名或者类名来限定要调用的函数。 动态联编 联编工作在程序运行时执行,在程序运行时才确定将要调用的函数。
例 8.8 #include<iostream.h> class Point { public: Point(double i, double j) {x=i; y=j;} double Area() const{ return 0.0;} private: double x, y; }; class Rectangle:public Point Rectangle(double i, double j, double k, double l); double Area() const {return w*h;} double w,h;
{ cout<<"Area="<<s.Area()<<endl; } void main() { Rectangle::Rectangle(double i, double j, double k, double l) :Point(i,j) { w=k; h=l; } void fun(Point &s) { cout<<"Area="<<s.Area()<<endl; } void main() { Rectangle rec(3.0, 5.2, 15.0, 25.0); fun(rec); } ?? 运行结果: Area=0
例 8.9 #include<iostream.h> 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 Rectangle(double i, double j, double k, double l); virtual double Area() const {return w*h;} double w,h; }; //其它函数同例 8.8
void fun(Point &s) { cout<<"Area="<<s.Area()<<endl; } void main() { Rectangle rec(3.0, 5.2, 15.0, 25.0); fun(rec); } 运行结果: Area=375 ?
8.4 虚函数 由上例可知,虚函数是动态联编的基础。 虚函数是非静态的成员函数。其说明格式为: virtual <类型说明符 ><函数名>(<参数表>) 即在类的说明中,在函数原型之前加virtual。 注意: virtual 只用在类定义的原型说明中,不能用在函数实现中。 具有继承性,基类中定义了虚函数,派生类中无论是否说明,同原型函数都自动为虚函数。 本质:不是重载定义而是覆盖定义。 调用方式:通过基类指针或引用,执行时会 根据指针指向的对象的类,决定调用哪个函数。
例 #include <iostream.h> class B0 //基类B0声明 { public: virtual void display() //虚成员函数 {cout<<"B0::display()"<<endl;} };
class B1: public B0 //公有派生 void display() //自动成为虚函数 { cout<<"B1::display()"<<endl; } }; class D1: public B1 //公有派生 { cout<<"D1::display()"<<endl; }
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函数成员 }
程序的运行结果为: B0::display() B1::display() D1::display()
8.5 虚函数与继承 若基类中定义了虚函数,则任何一个派生类都可以直接继承基类的虚函数或重新定义自己的虚函数,但是基类对象不可以调用在派生类中定义的虚函数,因为基类无法访问到这些虚函数。 派生类直接继承基类的虚函数,有时会出现问题:基类中的虚函数引用了该类中的数据成员,而该数据成员又无法为派生类直接使用(即派生类中定义了同名的数据成员,隐藏了基类中的成员),会出现问题。
虚函数的数据封装 若基类的虚函数在public区,而将派生类重新定义的同名虚函数放在protected区,则外界应无法使用派生类的虚函数,但实际上是可以使用的。 为什么虚函数看起来不受数据封装的限制? 虚函数也受数据封装的限制,但是虚函数使用的权限决定于当初调用虚函数的对象指针变量或引用体对应的类中如何定义虚函数的数据封装,而并非决定于该对象指针变量或引用体所真正引用的类。
使用基类对象做实参进行调用,则虚函数的封装性按照基类中权限定义。 思考: 如果将基类中的虚函数定义于protected区,而派生类中的虚函数定义于public区,结果如何? (外部不能使用派生类的虚函数)
动态联编时的析构函数 首先了解在动态联编方式下利用析构函数释放对象占用的存储空间会造成什么问题? [例8.16] #include <iostream.h> class A { public: //析构函数 virtual ~A(){cout<<“A::~A()called.\n”;} };
class B:public A { public: B(int i){buf=new char[i];} //构造函数 virtual ~B() //析构函数 {delete []buf; cout<<“B::~B() called.\n”; } private: char *buf; };
void fun(A *a) { delete a; } void main() A *a=new B(15); fun(a); 不为虚析构函数的输出结果为: A::~A() Called. 为虚析构函数的输出结果为: B::~B() Called. A::~A() Called.
8.6 虚析构函数 用delete语句调用析构函数释放对象,本希望自动根据对象指针的类型调用相对应的析构函数,但若析构函数本身不具有虚函数的性质,则fun函数没有达到动态联编的效果。 因此,需将析构函数定义为虚函数。 注意: 可以声明虚析构函数,但不能声明虚构造函数。
虚析构函数的声明语法是: virtual ~类名( ); 如果一个类的析构函数是虚函数,那么由它派生的子类的析构函数也是虚函数,虽然各派生类的析构函数名字不同,但不需再用virtual声明。 一般将所有继承类中最基类的析构函数设置为虚析构函数。
纯虚函数的作用 问题: 有时设计一个基类是为了被继承,并不需要为其提供成员函数,但是因为虚函数调用要实现动态联编方式,对派生类虚函数进行定义时,也要在基类中为其进行定义。所以若派生类中需要很多虚函数,就要在基类中也定义同样多且无用的虚函数。 解决方案: 在类中定义纯虚函数,不需提供任何实际操作定义.
8.7 纯虚函数 纯虚函数是一个在基类中说明的虚函数,在基类中没有定义具体的操作内容,要求派生类根据需要定义自己的版本。 纯虚函数的声明格式: virtual 函数类型 函数名(参数表) = 0 ; 声明为纯虚函数后,基类中就不再给出函数的实现部分,纯虚函数的函数体由各派生类自己给出。 注意区分函数体为空的虚函数与纯虚函数的区别。
抽象类 带有纯虚函数的类是抽象类。 抽象类是一种特殊的类,为抽象和设计的目的而建立,处于继承层次结构的较上层。 其作用是将有关的数据和行为组织在一个继承层次结构中,保证派生类具有要求的行为。 抽象类描述了一组子类共同的操作接口,而完整的实现留给了子类。 抽象类只能作为基类使用,不能实例化。但是可以声明抽象类的指针和引用,指向并访问派生类的对象。 对于暂时无法实现的函数,可以声明为纯虚函数,留给派生类去实现。
抽象类的一般形式 class 类名 { virtual 类型 函数名(参数表)=0; //纯虚函数 ... }
抽象类使用注意事项 抽象类派生出新的类后,如果派生类给出所有纯虚函数的实现,就不再是抽象类,可以实例化;否则就还是抽象类 当派生类中没有重新定义抽象类中的纯虚函数时,必须继续声明这些函数为纯虚函数 含有纯虚函数的抽象类也可以定义其它非纯虚的虚函数。虽然程序中不能定义该类的对象,但如果派生类中直接继承这些一般虚函数,则还可以通过派生类的对象来调用这些一般的虚函数
例如: #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()
再见 再见 再见 再见 再见 再见 再见 再见 再见 再见 再见 再见 再见 再见 再见 再见 再见 再见 再见 再见 再见 再见 再见