多态性和虚函数 封装性是基础,继承性是关键,多态性是补充 多态性是指发出同样的消息被不同类型的对象接收时导致完全不同的行为: 函数和运算符重载 虚函数、动态联编
1 函数重载 函数重载就是赋给同一个函数名多个含义 1 函数重载 函数重载就是赋给同一个函数名多个含义 C++中允许在相同的作用域内以相同的名字定义几个不同实现的函数,可以使成员函数,也可以是非成员函数 要求函数的参数或者至少有一个类型不同,或者个数不同 参数个数和类型都相同,仅仅返回值不同的重载函数是非法的
[例] 构造函数进行重载 #include <iostream.h> #include <string.h> [例] 构造函数进行重载 #include <iostream.h> #include <string.h> class string { public: string(char *s); string(string &s1); string(int size = 80); ~string( ){delete sptr;} int getlen( ){return length;} void print( ){cout << sptr << endl;} private: char *sptr; int length; };
string :: string(char *s) { length = strlen(s); sptr = new char[length + 1]; strcpy(sptr, s); } string :: string(string &s1) length = s1.length; strcpy(sptr, s1.sptr); string :: string(int size) length = size; sptr = new char[length+1]; *sptr = '\0';
Ans: void main( ) { string str1("This is a string."); str1.print( ); cout << str1.getlen( ) << endl; char *s1 = "That is a program."; string str2(s1); string str3(str2); str3.print( ); cout << str3.getlen( ) << endl; } Ans: This is a string. 17 That is a program. 18
函数重载应注意问题: 不要使用重载函数来描述毫无相干的函数 构造函数可以重载,普通成员函数也可以重载 在重载函数中使用缺省函数参数时应注意调用的二义性,例如: void print(int a, int b) { cout << "a=" << a << ",b=" << b << endl; } void print(int a, int b, int c = 50) 下列函数调用,由于二义性将通不过: print(10,100);
2 运算符重载 运算符重载就是赋予已有的运算符多重含义 C++中通过重新定义运算符,使它能够用于特定类的对象执行特定的功能 2 运算符重载 运算符重载就是赋予已有的运算符多重含义 C++中通过重新定义运算符,使它能够用于特定类的对象执行特定的功能 增强了C++语言的扩充能力
2.1 运算符重载的概念 为了介绍运算符重载的概念,先看一个运算符重载的引例。 引例: 用“+”运算符完成两个实数、两个复数、两个字符串的相关运算。 (1)实数 设有两个实数:x1=10 ,x2=20,则两个实数相加的结果是: x1+x2=10+20=30。 (2)复数 设有两个复数:x1=10+10i,x2=20+20i,则两复数相加结果是: x1+x2=30+30i 。 (3)字符串 设有两个字符串:x1=“ABCD”,x2=“EFGH” ,则两字符串连接的结果: x1+x2="ABCDEFGH"
2.1 运算符重载的概念 由上例可以看出,同一个运算符“+”可用于完成实数加法、复数加法及字符串连接等不同的运算,得到完全不同的结果。这就是“+”运算符的重载。因此,所谓运算符重载就是用同一个运算符完成不同的运算操作。 在C++这类面向对象的程序设计语言中,运算符重载可以完成两个对象的复杂操作(如:两个复数的算术运算等)。而运算符重载是通过运算符重载函数来完成的。当编译器遇到重载运算符,如复数加法:x1+x2 中的加号运算符“+”时,自动调用“+”运算符的重载函数完成两个复数对象的加法操作。 由于二元运算符的重载函数与一元运算符的重载函数有较大区别,因此分开介绍。先介绍二元运算符重载函数,再介绍一元运算符重载函数。
2.2 二元运算符重载函数 运算符重载函数可以是类的成员函数或者是友元函数。
2.2 二元运算符重载函数 1.运算符重载函数为类的成员函数 运算符重载函数为类的成员函数一般定义格式为: 2.2 二元运算符重载函数 1.运算符重载函数为类的成员函数 运算符重载函数为类的成员函数一般定义格式为: <类型><类名>::<operator><重载运算符>(形参表) {函数体} 其中,类型为运算符重载函数的返回类型。类名为成员函数所属类的类名。关键词“operator”加上“重载运算符”为重载函数名,即:重载函数名= operator重载运算符。形参常为参加运算的对象或数据。
2.2 二元运算符重载函数 例定义一个复数类,重载“=”运算符,使这个运算符能直接完成复数的赋值运算。
# include <iostream.h> class Complex { private: float Real,Image; public: Complex(float r=0,float i=0) { Real=r;Image=i;} // 缺省构造函数 void Show(int i) //显示输出复数 { cout<<"c"<<i<<"=" <<Real<<"+"<<Image<<"i"<<endl;} void operator =(Complex &c) //“=”运算符重载函数完成复数赋值操作 { Real=c.Real; Image=c.Image; } };
void main(void) { Complex c1(25,50),c2; c1.Show(1); c2=c1; c2.Show(2); } 程序执行后输出: c1=25+50i c2=25+50i 在程序中,定义了一个赋值运算符“=”的重载函数: void operator =(Complex &c) { Real=c.Real; Image=c.Image;
2.2 二元运算符重载函数 该重载函数的函数名为“operator =”,返回类型为void,形参为复数类对象的引用Complex &c。当程序执行主函数中的赋值语句c2=c1而遇到赋值运算符“=”时,自动调用赋值运算符“=”重载函数“operator =()”,并将“=” 运算符右边的操作数c1作为实参,左边操作数c2作为调用重载函数的对象,即:作了一次 c2.operator=(c1) 的函数调用。在函数的调用过程中,实参c1传给形参c,在函数体内完成了复数实部与虚部的赋值操作: Real=c1.Real; Image=c1.Image;
2.2 二元运算符重载函数 在上例定义复数类中,重载“+”、“-”运算符,使这二个运算符能直接完成复数的加、减运算。
# include <iostream.h> class Complex { private: float Real,Image; public: Complex(float r=0,float i=0) { Real=r;Image=i;} void Show(int i) //显示输出复数 { cout<<"c"<<i<<"=" <<Real<<"+"<<Image<<"i"<<endl;} Complex operator + (Complex &c); //“+”运算符重载函数完成两个复数加法 Complex operator - (Complex &c); //“-”运算符重载函数完成两个复数减法 Complex operator + (float s); //“+”运算符重载函数完成复数实部加实数 void operator +=(Complex &c); //“+=”运算符重载函数完成复数=复数+c void operator =(Complex &c); //“=”运算符重载函数完成两个复数赋值 };
Complex Complex::operator + (Complex &c) { Complex t; t.Real=Real+c.Real; t.Image=Image+c.Image; return t; } Complex Complex::operator - (Complex &c) t.Real=Real - c.Real; t.Image=Image - c.Image;
Complex Complex::operator + (float s) { Complex t; t.Real=Real+s; t.Image=Image; return t; } void Complex::operator += (Complex &c) { Real=Real+c.Real; Image=Image+c.Image; void Complex::operator = (Complex &c) { Real=c.Real; Image=c.Image;
void main(void) { Complex c1(25,50),c2(100,200) ,c3,c4; c1.Show(1); c2.Show(2); c3=c1+c2; //c3=(25+50i)+(100+200i)=125+250i c3.Show(3); c4=c2 - c1; //c4=(100+200i)-(25+50i)=75+150i c4.Show(4); c2+=c1; //c2=c2+c1= (100+200i)+ (25+50i)=125+250i c1=c1+200; //c1=(25+50i)+200=225+50i }
执行程序后输出: c1=25+50i c2=100+200i c3=125+250i c4=75+150i c2=125+250i c1=225+50i
2.2 二元运算符重载函数 在上例中的主函数中可以看出,经重载后运算符的使用方法与普通运算符一样方便。如复数c1加c2赋给c3的加法运算: 2.2 二元运算符重载函数 在上例中的主函数中可以看出,经重载后运算符的使用方法与普通运算符一样方便。如复数c1加c2赋给c3的加法运算: c3=c1+c2 与普通实数加法形式上完全相同。但实际执行过程中确是完全不同的。实现复数加法运算是通过调用加法运算符重载函数来完成,而对加法运算符重载函数的调用是由系统自动完成的。如表达式:c3=c1+c2;编译器先将c1+c2解释为对“+”运算符重载函数:c1.operator+(c2) 的调用。再将该表达式解释为对“=”运算符重载函数:c4.operator=(c1.operator+(c2))的调用。由c1.operator+(c2)成员函数求出复数c1+c2的值t,并返回一个计算结果t,然后再由成员函数c3.operator=(t),完成复数c3=t的赋值运算,将运算结果赋给c3。
运算符重载的几个问题 (1)运算符重载函数名必须为:operator <运算符> (2)运算符的重载是通过调用运算符重载函数实现的,调用函数时,左操作数为调用重载函数的对象,右操作数作为函数的实参,实参可以是对象、实数等其它类型。 (3)形参说明 若重载函数为成员函数,则参加二元运算的左操作数为调用重载函数的对象。因此,重载函数为成员函数的参数通常为一个,即右操作数。如在上例中,二元加法运算:c1+c2被解释为对重载成员函数c1.operator+(c2)的调用,此时重载函数只有一个参数。
运算符重载的几个问题 (4)运算符重载函数的返回类型 若二个同类对象进行二元运算后的结果类型仍为原类型,则运算符重载函数的返回类型应为原类型。 如在上例中,由于两个复数运算的结果仍为复数,因此上述运算符重载函数的返回类型均为复数类型Complex。
运算符重载的几个问题 (5)除了下面几个外,其余的运算符都可以用作重载。 下列运算符不允许重载: 运算符 运算符的含义 不允许重载的原因 ?: 三目运算符 在C++中没有定义一个三目运算符的语法 · 成员操作符 为保证成员操作符对成员访问的安全性,故不允许重载 * 成员指针操作符 同上 :: 作用域运算符 因该操作符左边的操作数是一个类型名,而不是一个表达式 sizeof 求字节数操作符 其操作数是一个类型名,而不是一个表达式
运算符重载的几个问题 (6)运算符的重载实际上是函数的重载,对运算符重载的选择,遵循着函数重载的选择原则。重载运算符有以下各种限制: 不可臆造新的运算符 不能改变运算符操作数的个数; 不能改变运算符原有的优先级; 不能改变运算符原有的结合性; 不能改变运算符原有的语法结构
2.运算符重载函数为友元函数 运算符重载函数为友元函数的一般定义格式为: <类型> <operator><重载运算符>(形参1,形参2) {函数体} 其中,类型为运算符重载函数的返回类型。operator<重载运算符>为重载函数名。形参1与形参2常为参加运算的两个对象的引用。
例:用友元运算符重载函数实现复数的加、减运算。 # include <iostream.h> class Complex { private: float Real,Image; public: Complex(float r=0,float i=0) {Real=r;Image=i;} void Show(int i) { cout<<"c"<<i<<"=" <<Real<<"+"<<Image<<"i"<<endl;} friend Complex operator + (Complex & ,Complex &); //“+”重载函数为友元函数 friend Complex operator - (Complex &, Complex &); //“-”重载函数为友元函数 friend Complex operator + (Complex &,float); };
Complex operator + (Complex &c1, Complex &c2) { Complex t; t.Real=c1.Real+c2.Real; t.Image=c1.Image+c2.Image; return t; } Complex operator - (Complex &c1, Complex &c2) t.Real=c1.Real - c2.Real; t.Image=c1.Image - c2.Image; Complex operator + (Complex &c,float s) t.Real=c.Real+s; t.Image=c.Image;
void main(void) { Complex c1(25,50),c2(100,200),c3,c4; c1.Show(1); c2.Show(2); c3=c1+c2; //c3=(25+50i)+(100+200i)=125+250i c3.Show(3); c4=c2-c1; //c4=(100+200i)-(25+50i)=75+150i c4.Show(4); c1=c1+200; //c1=25+50i+200=225+50i }
2.运算符重载函数为友元函数 程序执行后输出: c1=25+50i c2=100+200i c3=125+250i c4=75+150i
2.运算符重载函数为友元函数 从主函数可以看出,用成员函数与友元函数为运算符重载函数,就运算符的使用来讲是一样,但编译器处理方法是不同的,例如对表达式:c3=c1+c2;的处理是,先将c1+c2变换为对友元函数的调用:operator+(c1,c2);再将函数返回结果即两复数的和t赋给复数c3,因此表达式c3=c1+c2; 实际执行了c3= operator+(c1,c2) 的函数调用及赋值工作。 友元函数与成员函数作为二元运算符重载函数的另一个区别是: 当重载函数为成员函数时,二元运算符的左操作数为调用重载函数的对象。右操作数为实参。 当重载函数为友元函数时,二元运算符的左操作数为调用重载函数的第一个实参。右操作数为第二个实参。
2.3 一元运算符的重载 所谓一元运算符是只有一个操作数的运算符,如自加运算符“++”,自减运算符“--”等等。与二元运算符的重载类似,一元运算符重载函数也分为类的成员函数与友元函数两类。
2.3 一元运算符的重载 1.一元运算符重载函数为类的成员函数 格式: <类型><类名>::operator <一元运算符>(形参) { 函数体} 现对比较典型的一元运算符“++”、“--”进行讨论。对于一元运算符“++”、“--”存在前置与后置问题,因此定义函数时会有所区别。
2.3 一元运算符的重载 (1)“++”为前置运算符时,函数格式为: <类型><类名>::operator ++( ) {函数体} (2)“++”为后置运算符时,函数格式为 <类型><类名>::operator ++( int ) 由于是用运算符重载函数来实现“++”运算,所以这里的“++”是广义上的增量运算符。在后置运算符重载函数中,形参int仅用作区分前置还是后置,并无实际意义,可以给一个变量名,也可不给出变量名。
例:定义一个描述时间计数器的类,其三个数据成员分别用于存放时、分和秒。用成员函数重载“++”运算符,实现计数器对象的加1运算。 # include <iostream.h> class TCount { private: int Hour,Minute,Second; public: TCount (int h=0,int m=0,int s=0) { Hour=h;Minute=m;Second=s;} TCount operator ++( ); //定义“前置++”运算符重载成员函数 TCount operator ++( int ); //定义“后置++”运算符重载成员函数 void Show(int i ) //定义显示时:分:秒的成员函数 {cout<<"t"<<i<<"="<<Hour<<":"<<Minute<<":"<<Second<<endl;} };
TCount TCount ::operator ++ () { Second++; if (Second==60) { Second=0; Minute++; if (Minute==60) { Minute=0; Hour++; if (Hour==24) { Hour=0;} } return *this;
TCount TCount::operator++ (int ) { TCount temp=*this; Second++; if (Second==60) { Second=0; Minute++; if (Minute==60) { Minute=0; Hour++; if (Hour==24) { Hour=0;} } return temp;
void main(void) { TCount t1(10,25,50),t2,t3; //定义时间计数器对象t1=10:25:50 t1.Show(1); t2=++t1; //先加后用,即:先将t1自加,然后将t1赋给t2 t2.Show(2); t3=t1++; //先用后加,即:先将t1赋给t3,然后将t1自加 t3.Show(3); }
2.3 一元运算符的重载 程序执行后输出: t1=10:25:50 t1=10:25:51 t2=10:25:51 t1=10:25:52
2.3 一元运算符的重载 说明: (1)“前置++”运算符重载成员函数的说明 在主函数中执行t2=++t1语句时,先将t1自加,然后将t1赋给t2。该语句操作是通过调用“前置++”运算符重载成员函数来实现的。在执行t2=++t1语句时,编译系统将t2=++t1解释为对重载函数的调用: t2=t1.operator ++ (); 由于重载函数为对象t1成员函数,所以函数体对Hour、Minute、Second的自加操作就是对t1的数据成员Hour、Minute、Second的自加操作,因而可完成对计数器对象t1的加1操作。 为了实现前置“++”运算,应将加1后的对象值t1作为返回值,即用return t1语句返回当前对象t1值。但在重载函数体内并不能直接使用对象t1,因而无法使用return t1语句。这时必须使用指向当前对象t1的指针this。由于*this=t1,所以用return *this 语句可将自加后的t1值返回给调用函数,并赋给对象t2。由于将对象值t1值作为函数返回值,所以重载函数的类型应与t1的类型相同,为TCount类型。
2.3 一元运算符的重载 (2)“后置++”运算符重载成员函数的说明 在主函数中执行t3=t1++语句时,先将t1赋给t3,然后将t1自加。该语句操作是通过调用“后置++”运算符重载成员函数来实现的。在执行t3=t1++语句时,编译系统将t3=t1++解释为对重载函数的调用: t3=t1.operator ++ (1); 为了实现后置“++”运算,应将加1前的对象值t1作为返回值,这时应使用指向当前对象t1的指针this。在后置重载函数中先用TCount类定义一个临时对象temp,并将t1值(即*this值)赋给temp,在函数最后用return temp语句将加1前的t1值返回给函数,并赋给对象t2。
2.3 一元运算符的重载 (3)用成员函数实现一元运算符的重载时,运算符的左操作数或右操作数为调用重载函数的对象。因为要用到隐含的this指针,所以运算符重载函数不能定义为静态成员函数,因为静态成员函数中没有this指针。
2.3 一元运算符的重载 2.一元运算符重载函数为友元函数 一般格式为: <类型> operator <一元运算符>(类名 &对象) {函数体} 对于“++”、“――”运算符存在前置运算与后置运算的问题,因此,运算符重载函数必须分为两类。以“++”运算符为例,用友元函数来实现“++”运算符的重载时: 前置“++”运算符重载的一般格式为: <类型> operator ++ ( 类名 &); 后置“++”运算符重载的一般格式为: <类型> operator ++ ( 类名 &,int ); 其中:形参为要实现“++”运算的对象,int 只是用于区分是前置还是后置运算符,并无整型数的含义。
【例】用一个类来描述时间计数器,用三个数据成员分别存放时、分和秒。用友元函数重载“++”运算符,实现计数器对象的加1运算符。 # include <iostream.h> class TCount { private: int Hour,Minute,Second; public: TCount() { Hour=Minute=Second=0;} TCount (int h,int m,int s) { Hour=h;Minute=m;Second=s;} friend TCount operator ++(TCount &t ); //定义“前置++”运算符重载友元函数 friend TCount operator ++( TCount &t ,int ); //定义“后置++”运算符重载友元函数 void Show(int i ) { cout<<"t"<<i<<"="<<Hour<<":"<<Minute<<":"<<Second<<endl;} };
TCount operator ++ (TCount & t) { t.Second++; if (t.Second==60) { t.Second=0; t.Minute++; if (t.Minute==60) { t.Minute=0; t.Hour++; if (t.Hour==24) { t.Hour=0;} } return t;
TCount operator++ (TCount & t,int ) { TCount temp=t; t.Second++; if (t.Second==60) { t.Second=0; t.Minute++; if (t.Minute==60) { t.Minute=0; t.Hour++; if (t.Hour==24) { t.Hour=0;} } return temp;
void main(void) { TCount t1(10,25,50),t2,t3; //t1=10:25:50 t1.Show(1); t2=++t1; //先加后用 t2.Show(2); t3=t1++; //先用后加 t3.Show(3); }
程序执行后输出: t1=10:25:50 t1=10:25:51 t2=10:25:51 t1=10:25:52 t3=10:25:51
说明: (1)对“前置++”运算符重载友元函数的说明 在主函数中t2=++t1语句的含义是:先将t1自加,然后将自加后的t1值赋给t2。该语句操作是通过调用“前置++”运算符重载友元函数来实现的。在执行t2=++t1语句时,编译系统将t2=++t1解释为对重载函数的调用: t2=operator ++ (t1); 为了实现对t1的自加操作,重载函数的形参t必须与实参t1占用同一内存空间,使对形参t的自加操作变为对实参t1的自加操作。为此,形参t必须定义为时间计数器类TCount的引用,即:TCount & t。此外,为了能将t自加的结果通过函数值返回给t2,重载函数的返回类型必须与形参t相同,即为时间计数器类TCount的引用。故“前置++”运算符重载友元函数定义为: TCount & operator ++ (TCount & t) //函数返回类型与形参t相同,均为TCount & { t.Second++; … return t; } 当系统自动调用“前置++”运算符重载友元函数时,对形参t与实参t1自加后,用return t语句将自加的结果通过函数返回并赋给t2。从而实现对t1先加后赋值给t2的操作。
(2)对“后置++”运算符重载友元函数的说明 在主函数中t3=t1++语句的含义是:先将t1当前值赋给t3,然后再对t1自加。该语句操作是通过调用“后置++”运算符重载友元函数来实现的。在执行t3=t1++语句时,编译系统将t3=t1++解释为对重载函数的调用: t3=operator ++ (t1,1); 为了实现对t1的自加操作,重载函数的形参t必须与实参t1占用同一内存空间,使对形参t的自加操作变为对实参t1的自加操作。为此,形参t必须定义为时间计数器类TCount的引用,即:TCount & t。此外,为了能将t自加前的结果通过函数值返回给t3,在重载函数内第一条语句定义了TCount 类的临时对象temp,并将自加前t值赋给temp,在函数的最后用return temp语句返回自加前的t值。重载函数的返回类型必须与对象temp相同,即为TCount类型。故“后置++”运算符重载友元函数定义为: TCount operator ++ (TCount & t,int) //函数返回类型与temp相同,均为TCount类型 { TCount temp=t; t.Second++; … return temp; } 当系统自动调用“后置++”运算符重载友元函数时,对形参t与实参t1自加后,用return temp语句将自加前的结果通过函数返回并赋给t3。从而实现先将t1赋给t3后将t1自加的操作。
2.3 一元运算符的重载 【例】用一个类来描述人民币币值,用两个数据成员分别存放元和分。重载“++”运算符,用运算符重载成员函数实现对象的加1运算。
# include <iostream.h> # include <math.h> class Money { private: float Dollars,Cents; //定义数据成员元与分 public: Money() //定义默认的构造函数 { Dollars=Cents=0;} Money(float,float); //定义双参数构造函数 Money(float); //定义单参数构造函数 Money operator ++( ); //定义前置“++”运算符重载成员函数 Money operator ++( int ); //定义后置“++”运算符重载成员函数 float GetAmount(float & n) //通过形参n返回元,通过函数返回分 { n=Dollars; return Cents; } ~ Money( ) { }; //缺省的析构函数 void Show( ) //定义显示元与分的成员函数 { cout<<Dollars<<"元"<<Cents<<"分"<<endl;} };
Money::Money(float n) //初始值n中整数部分为元,小数部分为分 { float Frac,num; Frac=modff(n,&num); // modff(n,&num)将实数n分为解为整数与小数两部分,返回小数值给Frac,整数值送到num单元中 Cents=Frac*100; //存分值 Dollars=num; //存元值 } Money::Money(float d,float c) //d以元为单位(如d=10.5元), c以分为单位(如c=125分) { float sum,dd,cc; sum=d+c/100; //将d与c转换为以元为单位,并存入sum(如sum=10.5+125/100=11.75) cc=modff(sum,&dd); //将整数(即:元)存入dd,小数(即:分)存入cc Dollars=dd; //元存入Dollars Cents=cc*100; //分存入Cents
Money Money::operator ++ () //定义前置“++”重载函数 { Cents++; //分加1 if (Cents>=100) //若分大于100,则元加1,分减100 { Dollars++; Cents=Cents-100; } return *this; //返回自加后的人民币对象值 Money Money::operator++ (int ) { Money temp=*this; //将自加前人民币对象值存入临时对象temp Cents++; //分加1 if (Cents>=100) //若分大于100,则元加1,分减100 Cents-=100; return temp;
void main( void) { Money m1(25,50),m2(105.7),m3(10.5,125); //m1=25元50分,m2=105元70分,m3=10.5+125/100=11.75元 Money c,d; float e1,f1,e2,f2; m1.Show(); c= ++m1; //先加后用,即:先将m1加1,然后将m1赋给c (c=m1=25元51分) d= m1++; //先用后加,即:先将m1赋给d (d=m1=25元51分)。 然后将m1加1 (m1=25元52分) c.Show(); d.Show(); c=++m2; //c=m2=105元71分 d=m2++; //d=105元71分,m2=105元72分 e1=m2.GetAmount(f1); // m2=105元72分,f1=105 e1=72 e2=m3.GetAmount(f2); // m3=11元75分 ,f2=11, e2=75 cout<<f1+f2<<"元"<<'\t'<<e1+e2<<"分"<<endl; //f1+f2=105+11=116 //e1+e2=72+75=147 }
程序执行后输出: 25元50分 25元51分 105元71分 116元147分
说明: (1)Money为描述人民币的类,其数据成员Dollars、Cents分别代表元与分。 (2)在单参数的构造函数中,使用标准函数modff(n,&num)将实数n分为解为整数与小数两部分,返回小数值,整数值送到num所指单元中。最后将整数存入元Dollars中,小数部分乘100后存入分Cents中。 (3)前置“++”运算符重载函数中,先对人民币的分加1运算,分加1存在进位问题,当分加满100后,将分Cents减100(即分清零),再将元Dollars加1,最后通过return *this语句返回自加后的人民币币值。 (4)后置“++”运算符重载函数中,先将当前人民币币值赋给临时对象temp,然后对人民币的分加1运算,当分加满100后,将分Cents减100,再将元Dollars加1,最后通过return temp 返回自加前的人民币币值。 (5)主函数中 c= ++m1语句应解释为对前置重载函数的调用:c=m1.opreator(); d= m1++语句应解释为对后置重载函数的调用:d=m1.opreator(1);
【例】定义描述三维空间点(x,y,z)的类,用友元函数实现“++”运算符的重载。 # include <iostream.h>
class ThreeD { float x,y,z; public: ThreeD( float a=0,float b=0,float c=0) { x=a;y=b;z=c;} ThreeD operator + (ThreeD & t) //二个点坐标相加的“+”运算符重载成员函数 { ThreeD temp; temp.x=x+t.x; temp.y=y+t.y; temp.z=z+t.z; return temp; } friend ThreeD & operator ++(ThreeD &); //坐标点前置“++”运算符重载友元函数 friend ThreeD operator ++(ThreeD &,int);//坐标点后置“++”运算符重载友元函数 ~ ThreeD() {} void Show() { cout<<"x="<<x<<'\t'<<"y="<<y<<'\t'<<"z="<<z<<endl;} };
ThreeD & operator ++ (ThreeD & t) { t.x++; t.y++;t.z++; return t; } ThreeD operator ++ (ThreeD & t,int i) { ThreeD temp =t; t.x++; t.y++;t.z++; return temp; void main(void) { ThreeD m1(25,50,100),m2(1,2,3),m3; m1.Show(); ++m1; m2++; m2.Show(); m3=++m1+m2++; m3.Show();
程序执行后输出: x=25 y=50 z=100 x=26 y=51 z=101 x=2 y=3 z=4 x=29 y=55 z=106
程序中定义的类ThreeD描述一个空间点的三维坐标,对对象执行“++”运算,即对该点坐标的三个分量(x,y,z)分别完成加1运算。主函数中: ++m1 语句被解释为对前置++运算符重载函数的调用: opreator++(m1); 运算后m1=(26,51,101)。 m2++ 语句被解释为对后置++运算符重载函数的调用: opreator++(m2,1);运算后m2=(2,3,4)。 m3=++m1+m2++语句的执行将分为三步, 第一步执行对m1的前置++运算:++m1 运算结果是返回m1自加后的一个对象,若将此对象记作t1,则t1=++m1=(27,52,102)。 第二步执行对m2的后置++运算:m2++ 运算结果将返回m2自加前的对象,若将此对象记作t2,则t2=m2++=(2,3,4)。 第三步执行将二个对象t1与t2的“和”赋给m3运算:m3=t1+t2 该运算被解释为对“+”运算符重载函数的调用: m3=t1.preator+(t2); 运算的结果为m3=(27,52,102)+(2,3,4)=(29,55,106)。
两种重载形式的比较 单目运算符最好被重载为成员函数;对双目运算符最好被重载为友元函数 考虑一个表达式: 5.67 + c 重载为友元函数时,该表达式将被解释为: operator+(complex (5.67), c) V 重载为成员函数时,该表达式将被解释为: 5.67.oprator+(c) X 有的双目运算符重载为成员函数为好,例如,赋值运算符
2.4 字符串类运算符重载 C++系统提供的字符串处理能力比较弱,字符串复制、连接、比较等操作不能直接通过“=”、“+”、“>”等运算操作符完成,而必须通过字符处理函数来完成。 例如,有字符串s1=”ABC”,s2=”DEF”,要完成s=s1+s2=“ABCDEF”的工作,则需要调用字符串处理函数:strcpy(s,s1)与strcat(s,s2)才能完成两个字符串的拼接工作。 通过C++提供的运算符重载机制,可以提供对字符串直接操作的能力,使得对字符串的操作与对一般数据的操作一样方便。 如字符串s1与s2拼接成字符串s的工作,用“+”与“=”运算符组成的表达式:s=s1+s2即可完成。 下面通过例题说明字符串运算符重载函数的编写方法,及重载后字符串运算符的使用方法。
【例】编写字符串运算符“=”、“+”、“>”的重载函数,使运算符“=”、“+”、“>”分别用于字符串的赋值、拼接、比较运算,实现字符串直接操作运算。 分析:字符串可用指向字符串的指针Sp及字符串长度Length来描述,如图所示。因此描述字符串类的数据成员为字符指针Sp及其长度Length。设计缺省构造函数、拷贝构造函数及初始化构造函数。再设计“=”、“+”、“>”运算符重载函数,分别完成字符串赋值、拼接、比较运算。在主函数中先定义字符串对象,并调用构造函数完成初始化工作。然后使用“=”、“+”、“>”运算符,直接完成字符串的赋值、拼接、比较运算。程序设计如下:
# include <iostream.h> # include <string.h> class String //定义字符串类 { protected : int Length; char *Sp; public: String() //定义缺省的构造函数 {Sp=0;Length=0;} String(const String &); //定义拷贝构造函数 String(const char *s) //定义初始化构造函数 { Length=strlen(s); Sp=new char[Length +1]; strcpy(Sp,s); } ~String() //定义析构函数 { if (Sp) delete [] Sp;} void Show() //定义显示字符串函数 { cout<<Sp<<endl;} void operator = (String &); //定义字符串赋值成员函数 friend String operator + ( const String &, const String &); //定义字符串拼接友元函数 int operator > (const String &) ; //定义字符串比较成员函数 };
String::String(const String &s) { Length=s.Length; if (s.Sp) { Sp=new char [Length+1]; strcpy(Sp,s.Sp); } else Sp=0; void String::operator=(String &s) { if (Sp) delete []Sp; Length=s.Length;
String operator + (const String &s1,const String &s2) { String t; t.Length=s1.Length+s2.Length; t.Sp=new char [t.Length+1]; strcpy(t.Sp,s1.Sp); strcat(t.Sp,s2.Sp); return t; } int String::operator >(const String &s) { if (strcmp(Sp,s.Sp)>0 ) return 1; else 0; void main (void) { String s1("software"),s2("hardware"),s3("design"); String s4(s1),s5,s6,s7; s5=s2; s6=s4+s3; s7=s5+s3; s6.Show(); s7.Show(); if (s4>s5) s4.Show(); else s5.Show();
程序执行后输出: software design hardware design software
2.4 字符串类运算符重载 关于上述程序有几点说明如下: (1)定义初始化构造函数中: String(const char *s) { Length=strlen(s); Sp=new char[Length +1]; strcpy(Sp,s); } 形参为字符串指针变量s,为了防止在构造函数内修改实参字符串的值,特在形参类型前加关键词const,表示在构造函数内,s所指字符串是不能修改的。初始化构造函数体内,先用字符串函数strlen求出字符串s的长度,并赋给Length。然后用new运算符动态建立字符数组,将字符数组首地址赋给字符串指针Sp,最后用字符串拷贝函数strcpy将字符串s拷贝到Sp所指字符串中。完成String类对象数据成员Length与Sp的初始化工作。
2.4 字符串类运算符重载 (2)字符串赋值“=”运算符重载成员函数中: void String::operator=(String &s) { if (Sp) delete []Sp; Length=s.Length; if (s.Sp) { Sp=new char [Length+1]; strcpy(Sp,s.Sp); } else Sp=0; 形参为String类的引用s。在函数体内先删除当前字符串内容。然后将形参字符串长度赋给当前对象的字符串长度Length。将形参字符串内容赋给当前对象。
2.4 字符串类运算符重载 (3)因为字符串“+”运算符重载函数为友元函数,因此参加运算的两个字符串必须以形参方式输入函数体内,所以重载函数的形参为两个String类型的对象的引用。函数体内先定义一个String类型的临时对象t,用于存放两个字符串拼接的结果。再将两个字符串的长度之和赋给t的长度t.Length,用new运算符动态分配长度为t.Length+1的内存空间,并将其地址赋给t.Sp。再用strcopy()函数将s1拷贝到t,用strcat()将s2拼接到t中,完成t=s1+s2的字符串拼接工作,最后将t返回给调用对象。由于函数返回值为对象t,所以,重载函数的返回类型为String。
2.4 字符串类运算符重载 (4)在主函数中: 字符串赋值运算语句s5=s2;被编译器解释为对“=”运算符重载函数的调用:s5.opreator=(s2); 字符串拼接运算语句s6=s4+s3;被编译器解释为对“+”与“=”运算符重载函数的调用:s6.opreator=(opreator+(s4,s3)); 字符串比较运算语句s4>s5;被编译器解释为对“>”运算符重载函数的调用:s4.opreator>(s5);
2.5 赋值运算符和赋值运算符重载 相同类型的对象之间是可以直接赋值的,在前面的程序例子中已多次使用。但当对象的成员中使用了动态数据类型时,就不能直接相互赋值,否则在程序执行期间会出现运算错误。 【例】对象间直接赋值导致程序执行的错误。 # include <iostream.h> # include <string.h>
class String { private: char *ps; public: String() {ps=0;} String(char * s) { ps =new char [strlen(s)+1]; strcpy(ps,s); } ~String() { if (ps) delete [] ps;} char * GetS() { return ps;} };
void main (void ) { String s1("China!"),s2("Computer!"); cout<<"s1="<<s1.GetS() <<'\t'; cout<<"s2="<<s2.GetS() <<'\n'; s1=s2; char c; cin>> c; } 程序执行后输出: s1=China! s2=Computer! s1=Computer! s2=Computer! 程序执行到cin>>c语句输入任意字符(如:a)时发生错误,这是因为执行s1=s2后 ,使s1、s2中的ps均指向字符串“Computer!”,当系统撤消s2时调用析构函数回收了ps所指的字符串存储空间,当撤消s1调用析构函数时,已无空间可回收,出现错误。
解决上述错误的方法是用重载运算符“=”。在上例的类String中应增加如下的赋值运算符重载函数: String & operator= (String &b) { if (ps) delete [] ps; if (b.ps) { ps= new char [strlen(b.ps)+1]; strcpy(ps,b.ps); } else ps=0; return * this;
2.6 其它运算符的重载举例 [例] 重载下标运算符 #include <iostream.h> class CharArray 2.6 其它运算符的重载举例 [例] 重载下标运算符 #include <iostream.h> class CharArray { public: CharArray(int l) Length = l; Buff = new char[Length]; } ~CharArray( ){delete Buff;} int GetLength( ){return Length;} char & operator[ ](int i); private: int Length; char *Buff; };
char &CharArray :: operator[ ](int i) { static char ch=0; if( i < Length && i >= 0) return Buff[i]; else cout << "\nIndex out of range."; return ch; } void main( ) int cnt; CharArray string1(6); char *string2 = "string"; for(cnt = 0; cnt < 8; cnt++) string1[cnt] = string2[cnt]; cout << "\n"; cout << string1[cnt]; cout << string1.GetLength( ) << endl;
Ans: Index out of range. string 6
[例] 重载()运算符 Ans: 数学函数的抽象:f(x,y) = (x+5)*y #include <iostream.h> class F { public: double operator ( ) (double x, double y) const; }; double F :: operator ( )(double x, double y) const return (x + 5) * y; // (1.5+5)*2.2 } void main( ) ( F f; cout << f( 1.5, 2.2) << endl; Ans: 14.3
3 虚函数 3.1多态性技术 以上所述函数重载与运算符重载就是C++中的多态性技术,所谓多态性技术是指调用同名函数完成不同的函数功能,或使用同名运算符完成不同的运算功能。它常用重载技术与虚函数来实现。在C++中,将多态性分为两类:编译时的多态性和运行时的多态性。
3.1多态性技术 编译时的多态性 编译时的多态性是通过函数的重载或运算符的重载来实现的。函数的重载是根据函数调用时,给出不同类型的实参或不同的实参个数,在程序执行前就能确定调用哪一个函数。对于运算符的重载,是根据不同的运算对象在编译时就可确定执行哪一种运算。 如在前例中的“+”运算:c3=c1+c2 与 a3=a1+a2,根据参加“+”运算对象是复数c1、c2还实数a1、a2,就可确定是调用复数加法重载函数还是进行普通实数的加法运算。
3.1多态性技术 运行时的多态性 运行时的多态性是指在程序执行前,根据函数名和参数无法确定应该调用哪一个函数,必须在程序执行过程中,根据具体执行情况来动态地确定。这种多态性是通过类的继承关系和虚函数来实现的,主要用于实现一些通用程序的设计。
3 虚函数 3.2 虚函数 在基类中用关键字 virtual修饰的成员函数称为虚函数,虚函数的定义格式为: {函数体} 用虚函数实现“运行时多态性”的方法是:在派生类中定义与基类虚函数同名同参数同返回类型的成员函数,在派生类中定义的这个成员函数也称为虚函数。虽然基类中的虚函数与各派生类中虚函数同名同参数,但由于各虚函数的函数体是不同,因而可用同名虚函数在运行时完成对不同对象的操作,从而实现“运行时多态性”。 下面举例说明用虚函数实现“运行时多态性”的方法:
3.2 虚函数 【例】定义基类High,其数据成员为高H,定义成员函数Show()为虚函数。然后再由High派生出长方体类Cuboid与圆柱体类Cylinder。并在两个派生类中定义成员函数Show()为虚函数。在主函数中,用基类High定义指针变量p,然后用指针p动态调用基类与派生类中虚函数Show(),显示长方体与圆柱体的体积。
# include <iostream.h> class High { protected: float H; public: High(float h) { H=h;} virtual void Show() //在基类中定义虚函数Show() { cout<<"High="<<H<<endl;} };
class Cuboid:public High { private: float Length,Width; public: Cuboid(float l=0,float w=0,float h=0):High(h) { Length=l; Width=w;} void Show() //在长方体派生类中定义虚函数Show() { cout<<"Length="<<Length<<'\t'; cout<<"Width="<<Width<<'\t'; cout<<"High="<<H<<'\n'; cout<<"Cuboid Volume="<<Length*Width*H<<endl; } };
class Cylinder:public High { private: float R; public: Cylinder(float r=0,float h=0):High(h) {R=r;} void Show() //在圆柱体派生类中定义虚函数Show() { cout<<"Radius="<<R<<'\t'; cout<<"High="<<H<<'\n'; cout<<"Cylinder Volume="<<R*R*3.1415*H<<endl; } };
void main(void) { High h(10),*p; Cuboid cu(10,10,10); Cylinder cy(10,10); h.Show(); cu.Show(); cy.Show(); p=&h; p->Show(); p=&cu; p=&cy; }
3.2 虚函数 执行程序后输出: High=10 Length=10 Width=10 High=10 Cubiod Volume=1000 Radius=10 High=10 Cylinder Volume=3141.5 Length=10 Width=10 High=10 Radius=10 High=10
3.2 虚函数 在主函数中通过调用三个不同对象的成员函数 h.Show(); cu.Show(); cy.Show(); 分别输出高、长方体、圆柱体的值。因在编译时,根据对象名,就可确定调用哪一个成员函数,所以这是编译时的多态性。
3.2 虚函数 在主函数中将三个不同类型的对象起始地址依次赋给基类的指针变量p, p=&h; p->Show(); p=&cu; p=&cy; 这在C++中是允许的,即可将由基类所派生出来的派生类对象的地址&cu与&cy赋给基类类型的指针变量p。当基类指针变量指向不同的对象时,尽管调用的形式完全相同,均为p->Show(); 但确调用了不同对象中函数。因此输出了不同的结果,所以这是运行时的多态性。
3.2 虚函数//////////////// 关于虚函数有几点说明如下: (1)当基类中将成员函数定义为虚函数后,在其派生类中定义的虚函数必须与基类中虚函数同名同参数同返回类型,如上例中基类与派生类中的虚函数名均为Show,均无参数,返回类型均为void。在定义派生类中的虚函数时,可不加关键词virtual。 (2)实现动态的多态性时,必须使用基类类型的指针变量,使该指针指向不同派生类的对象,并通过调用指针所指向的虚函数才能实现动态的多态性。
3.2 虚函数 (3)虚函数必须是类的一个成员函数,不能是友元函数,也不能是静态的成员函数。 (4)在派生类中没有重新定义虚函数时,与一般的成员函数一样,当调用这种派生类对象的虚函数时,则调用基类中的虚函数。 (5)可将析构函数定义为虚函数,但不能将构造函数定义为虚函数。通常在释放基类中和派生类中动态申请的存储空间时,也要将析构函数定义为虚函数,以便实现撤消对象时的多态性。
3.2 虚函数 (6)虚函数与一般函数相比较,调用时执行速度要慢一些。为了实现多态性。在每一个派生类中均要保持相应虚函数的入口地址表,函数调用机制也是间接实现的。因此除了要编写一些通用的程序并一定要使用虚函数才能完成其功能要求外,通常不必使用虚函数。
[例] 虚析构函数 #include <iostream.h> class A { public: [例] 虚析构函数 #include <iostream.h> class A { public: virtual ~A( ){cout << "A::~A( ) called.\n";} }; class B : public A 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); Ans: B::~B( ) called. A::~A( ) called. 不用虚函数:
[例] 分析程序,回答问题 #include <iostream.h> class A { public: virtual void act1( ); void act2( ) {act1( );} }; void A::act1( ) cout << "A::act1( ) called." << endl; }
class B : public A { public: void act1( ); }; void B :: act1( ) cout << "B::act1( ) called" << endl; } void main ( ) B b; b.act2( );
回答下列问题: 1. 该程序执行后的输出结果是什么?为什么? B::act1( ) called. 1. 该程序执行后的输出结果是什么?为什么? B::act1( ) called. 因为B是A的派生类,act1( )是类A中的虚函数,类B 中的act1( )自然是虚函数。 在main( )函数中,b.act2( ); 调用类B中的act2( )函数,B是派生类实际上调用 A::act2( )函数,则A::act2( )函数的实现中调用 act1( ),由于有两个act1( )函数,并且是虚函数, 于是便产生动态联编,根据运行时的情况选择了 B::act1( )。所以输出结果是:B::act1( ) called.
2. 如果将A::act2( )的实现改为: void A::at2( ) { this->act1( ); } 输出结果如何? 输出结果与前面相同 因为加了this的限定,与没加是一样的,this是 指向操作该成员函数的对象的指针
3. 如果将A::act2( )的实现改为: void A::act2( ) { A::act1( ); } 输出结果如何? 输出结果如下: A::act1( ) called. 由于加了成员名的限定,在对act1( )函数的调用进 行的是静态联编,结果A::act1( )函数被调用,因 此产生上述结果。
构造函数中调用虚函数 构造函数调用的虚函数是自己的类中实现的虚函数 如果自己类中没有实现这个虚函数,则调用基类中的虚函数 而不是任何派生类中实现的虚函数
[例] 构造函数调用虚函数 #include <iostream.h> class A { public: A( ){ } [例] 构造函数调用虚函数 #include <iostream.h> class A { public: A( ){ } virtual void f( ){cout << "A::f( ) called.\n";} }; class B::public A B( ) {f( );} void g( ) {f( );}
Ans: class C::public B { public: C( ) { } virtual void f( ){cout << "C::f( ) called.\n";} }; void main ( ) C c; c.f( ); } Ans: A::f( ) called. //C c; --> B( ) --> B::f( ) --> A::f( ) 基类虚函数 C::f( ) called. //C.f( ) --> C::f( )
[例]派生类与基类的虚函数类型 #include <iostream.h> class A { public: virtual void print(int x, int y){cout << x << "," << y << endl;} }; class B:public A public: // 派生与基类的虚函数类型相同,否则强制转换 virtual void print(int x, float y){cout << x << "," << y <<endl;} void show(A &a) a.print(3, 8); a.print(6, 5.9); } void main( ) B b; show(b);
Ans: 3,8 6,5
3.3 纯虚函数 在定义一个基类时,若无法定义基类中虚函数的具体操作,虚函数的具体操作完全取决于其不同的派生类。这时,可将基类中的虚函数定义为纯虚函数。定义纯虚函数的一般格式为: virtual <类型> <纯虚函数名>(形参表)=0;
3.3 纯虚函数 由纯虚函数的定义格式可以看出如下几点: (1)由于纯虚函数无函数体,所以在派生类中没有重新定义纯虚函数之前,是不能调用这种函数的。 (2)将函数名赋值为0的含义是,将指向函数体的指针值赋初值0。 (3)将至少包含一个纯虚函数的类称为抽象类。这种类只能作为派生类的基类,不能用来说明对象。其理由很明显;因为虚函数没有实现部分,所以不能产生对象。但可定义指向抽象类的指针,即指向这种基类的指针。当用这种基类指针指向其派生类的对象时,必须在派生类中重载纯虚函数,否则会产生程序的运行错误。
3.3 纯虚函数 【例】定义抽象基类High,其数据成员为高H,定义Show()为纯虚函数。然后再由High派生出长方体类Cuboid与圆柱体类Cylinder。并在两个派生类中重新定义虚函数Show()。在主函数中,用基类High定义指针变量p,然后用指针p动态调用派生类中虚函数Show(),显示长方体与圆柱体的体积。
# include <iostream.h> class High { protected: float H; public: High(float h) {H=h;} virtual void Show()=0; //在基类中定义纯虚函数Show() };
class Cuboid:public High { private: float Length,Width; public: Cuboid(float l=0,float w=0,float h=0):High(h) {Length=l; Width=w;} void Show() //在长方体派生类中定义虚函数Show() { cout<<"Length="<<Length<<'\t'; cout<<"Width="<<Width<<'\t'; cout<<"High="<<H<<'\n'; cout<<"Cuboid Volume="<<Length*Width*H<<endl; } };
class Cylinder:public High { private: float R; public: Cylinder(float r=0,float h=0):High(h) { R=r;} void Show() //在圆柱体派生类中定义虚函数Show() { cout<<"Radius="<<R<<'\t'; cout<<"High="<<H<<'\n'; cout<<"Cylinder Volume="<<R*R*3.1415*H<<endl; } };
void main(void) { High *p; Cuboid cu(10,10,10); Cylinder cy(10,10); p=&cu; p->Show(); p=&cy; }
3.3 纯虚函数 执行程序后输出: Length=10 Width=10 High=10 Cubiod Volume=1000 Radius=10 High=10 Cylinder Volume=3141.5 若在主函数中增加说明: High h; 则因为抽象类High不能产生对象,编译时将给出错误信息。
[例] 使用纯虚函数 #include <iostream.h> class point { public: point(int i = 0, int j = 0){x0 = i; y0 = j;} virtual void set( ) = 0; virtual void draw( ) = 0; protected: int x0, y0; };
class line : public point { public: line(int i=0, int j=0, int m=0, int n=0) : point(i, j) { x1 = m; y1 = n; } void set( ){cout << "line::set( ) called.\n";} void draw( ){cout << "line::draw( ) called.\n";} protected: int x1, y1; }; class ellipse : public point ellipse(int i=0, int j=0, int p=0, int q=0) : point(i,j) x2 = p; y2 = q; void set( ){cout << "ellipse::set( ) called.\n";} void draw( ){cout << "ellipse::draw( ) called.\n";} int x2, y2;
void drawobj(point *p) { p->draw( ); } void setobj(point *p) p->set( ); void main( ) line *lineobj = new line; ellipse *elliobj = new ellipse; drawobj(lineobj); drawobj(elliobj); cout << endl; setobj(lineobj); setobj(elliobj); cout << "\nRedraw the object …\n";
Ans: line::draw( ) called. ellipse::draw( ) called. line::set( ) called. ellipse::set( ) called. Redraw the object…
3.3 纯虚函数 (4)在以抽象类作为基类的派生类中必须有纯虚函数的实现部分,即必须有重载纯虚函数的函数体。否则,这样的派生类也是不能产生对象的。 综上所述,可将纯虚函数归结为:抽象类的唯一用途是为派生类提供基类,纯虚函数的作用是作为派生类的成员函数的基础,并实现动态多态性。 下面通过例子来说明抽象类的简单应用。
【例】建立一个双向链表,要完成插入一个结点、删除一个结点、查找某一个结点操作,并输出链表上各结点值。设结点只有一个整数。 分析:因链表的插入、删除、查找等操作都是相同的,只是结点上的信息随着不同的应用有所不同,所以可将实现链表操作部分设计成通用的程序。一个结点的数据结构用两个类来表示。类IntObj的数据成员描述结点信息,成员函数完成两个结点比较,输出结点数据等。类Node的数据成员中,包括要构成双向链表时,指向后一个结点的后向指针Next,指向前一个结点的前向指针Prev,指向描述结点数据的指针Info。另外定义一个类List,把它作为类Node的友元,它的成员数据包括指向链表的首指针Head,指向链尾的指针Tail,成员函数实现链表的各种操作,如插入一个结点,删除一个结点等。由于类List是类Node的友元,因此它的成员函数可以访问Node的所有成员。
# include <iostream.h> # include <string.h> class Object //定义一个抽象类,用于派生描述结点信息的类 { public: Object(){} //缺省的构造函数 virtual int IsEqual(Object &)=0; //实现两个结点数据比较的纯虚函数 virtual void Show()=0; //输出一个结点上数据的纯虚函数 virtual ~Object() {} //析构函数定义为虚函数 };
class Node //定义结点类 { private: Object *Info; //指向描述结点的数据域 Node *Pre, *Next; //用于构成链表前、后指针 public: Node() //定义缺省的构造函数 { Info=0;Pre=0;Next=0;} Node (Node &node) //完成拷贝功能的构造函数 { Info=node.Info; Pre=node.Pre; Next=node.Next; } void FillInfo(Object *obj) //使Info指向数据域 { Info=obj;} friend class List; //定义List为Node的友元类 };
class List //实现双向链表操作的类 { private: Node *Head, *Tail; //定义链表首和链表尾指针 public: List() //置空链表 { Head=Tail=0;} ~List() { DelList();} //释放链表所占的存储空间 void AddNode(Node *); //在链表尾增加一个结点的成员函数 Node * DelNode(Node *); //删除链表中指定结点的成员函数 Node * LookUp(Object &); //在链表中查找指定结点的成员函数 void ShowList (); //输出整条链表上的数据的成员函数 void DelList(); //删除整条链表的成员函数 };
void List::AddNode(Node * node) { if (Head==0) //链表为空表时 { Head=Tail=node; //链表首、尾指针指向结点 node->Next=node->Pre=0; //该结点前、后指针置为空 } else // 链表非空 { Tail->Next=node; //将结点加入到链表尾 node->Pre=Tail; Tail=node; node->Next=0;
Node * List::DelNode(Node * node) //删除指定结点 { if (node==Head) //删除链表首结点 if (node==Tail) //链表只有一个结点 Head=Tail=0; else { Head=node->Next; // 删除链表首结点 Head->Pre=0; } { node->Pre->Next=node->Next; //删除非首结点 if (node!=Tail) node->Next->Pre=node->Pre; else Tail=node->Pre; node->Pre=node->Next=0; return(node);
Node * List::LookUp(Object &obj) //从链表中查找一个结点 { Node *pn=Head; while (pn) { if (pn->Info->IsEqual(obj)) return pn; pn=pn->Next; } return 0; void List::ShowList() //输出链表上各结点的数据值 { Node *p=Head; while (p) { p->Info->Show(); p=p->Next;
void List::DelList() //删除整条链表 { Node *p, *q; p=Head; while(p) { delete p->Info; q=p; p=p->Next; delete q; }
class IntObj: public Object //由抽象类派生出描述结点数据的类 { private: int data; public: IntObj(int x=0) { data=x;} void SetData(int x) int IsEqual(Object &); void Show() { cout<<"Data="<<data<<'\t';} //重新定义虚函数 };
int IntObj::IsEqual(Object &obj) //重新定义比较两个结点是否相等的虚函数 { IntObj & temp=(IntObj &) obj; return (data==temp.data); } void main(void) { IntObj *p; Node *pn , *pt, node; List list; for (int i=0;i<5;i++) //建立包括五个结点的双向链表 { p=new IntObj(i+100); //动态建立一个IntOb类的对象 pn=new Node; //建立一个新结点 pn->FillInfo(p); //填写结点的数据域 list.AddNode(pn); //将新结点加到链表尾 list.ShowList(); //输出链表上各结点 cout<<endl;
IntObj da; da.SetData(102); //给要查找的结点置数据值 pn=list.LookUp(da);//从链表上查找指定的结点 if (pn) pt=list.DelNode(pn); //若找到,则从链表上删除该结点 list.ShowList(); //输出已删除结点后的链表 cout<<endl; if (pn) list.AddNode(pt); //将结点加入链表尾 list.ShowList(); //输出已增加一个结点的链表 }
执行程序后输出 Data=101 Date=102 Data=103 Date=104 Date=101 Data=103 Date=104 Data=101 Date=103 Data=104 Date=102
该例子只提供了双向链表的基本操作:连续五次将新建的结点加到链表尾,显示整个链表上各结点的数据值,从链表上查找一个指定的结点,再删除之,并显示删除这个结点后的链表结点数据,再将删除的这个结点加到链表尾并显示新的链表。 例中的类IntObj是由抽象类Object派生而来的,可以根据实际数据结构的需要来定义从基类中继承来的虚函数IsEqual()和Show()的具体实现。在上例中,链表上的结点只有一个整数,所以只要判断两个结点上整数是否相同。由抽象类Object派生出来的不同的派生类均可重新定义这两个纯虚函数,这样就可以实现对不同类的对象使用相同的接口实现不同的操作。
4 转换函数 转换函数又称为类型强制转换成员函数,它是类中的一个非静态成员函数 定义格式如下: class <类型说明符1> { 4 转换函数 转换函数又称为类型强制转换成员函数,它是类中的一个非静态成员函数 定义格式如下: class <类型说明符1> { public: operator <类型说明符2> ( ); …… } 由<类型说明符1>到<类型说明符2>之间的映射关系
[例] #include <iostream.h> class Rational { public: Rational(int d, int n) { den = d; num = n; } operator double( ); private: int den, num; };
Ans: Rational::operator double( ) { return double(den) / double(num); } void main( ) Rational r(5, 8); double d = 4.7; d += r; cout << d << endl; Ans: 5.325
定义转换函数时应注意如下几点: 转换函数是用户定义的成员函数,但它要是非静态的; 转换函数不可以有返回值类型; 转换函数也不带有任何参数; 转换函数还不能定义为友元函数。