580 likes | 737 Views
第 1 章 C++ 概述 第 2 章 数据类型、运算符和表达式 第 3 章 简单的输入 / 输出 第 4 章 C++ 的流程控制 第 5 章 函数 第 6 章 编译预处理 第 7 章 数组 第 8 章 结构体、共同体和枚举类型 第 9 章 指针和引用 第 10 章 类和对象 第 11 章 类和对象的其他特性 第 12 章 继承和派生 第 13 章 多态性 第 14 章 输入 / 输出流 第 15 章 模板. 目 录. 第一部分 面向过程的程序设计.
E N D
第1章 C++概述 第2章 数据类型、运算符和表达式 第3章 简单的输入/输出 第4章 C++的流程控制 第5章 函数 第6章 编译预处理 第7章 数组 第8章 结构体、共同体和枚举类型 第9章 指针和引用 第10章 类和对象 第11章 类和对象的其他特性 第12章 继承和派生 第13章 多态性 第14章 输入/输出流 第15章 模板 目 录 • 第一部分 面向过程的程序设计 • 第二部分 面向对象的程序设计
13.1 函数重载 13.2 运算符重载 13.3 静态联编 13.4 动态联编和虚函数 13.5 纯虚函数和抽象类
第 13 章 多态性 • 多态性是实现 OOP 的关键技术之一。 • 在C++中,多态性分为两种: ①静态多态 ②动态多态 • 函数重载和运算符重载属于静态多态。 • 函数重载:相同函数名可以完成不同功能。 • 运算符重载:相同运算符完成不同功能。 • 动态多态是指:程序执行过程中确定的关系, 如动态确定函数的调用关系。 • 运行时的多态(动态多态) 是通过类的继承和虚函数来实现的。
13.1 函数重载 参见5.6节(略) 13.2 重载运算符 • C++中所有的运算符都已预先定义了用法及意义。 如:+ - * / = 等,适用于已有的数据类型。 • 已定义的用法及意义是不允许用户改变的。 • 如果用户定义了新的数据类型,希望已定义的运算符 能适应新的数据类型, • 如: 前述 Complex 类的加法运算 Complex c1, c2, c3; c3 = add(c1, c2); //以前通过函数实现加法运算 c2.add(c3); // 或通过成员函数实现 c3 = c1 + c2;//能否直观地写成这样? 能 !运算符的重载可以达到此目的。
13.2.1 运算符重载的几点说明 • 允许重载 和 不允许重载的运算符见13-1和13-2表。 • 重载运算符的限制 (1)只能对已有运算符重载,不可臆造新的运算符。 (2)不允许改变运算符的优先级和结合性。 (3)不允许改变运算符的语法结构, 如二元运算符只能重载成二元运算符, 一元运算符只能重载成一元运算符。
13.2.2 运算符重载的两种方式 特殊的成员函数名 1.重载为类的成员函数 在类内定义运算符重载函数的格式为: <函数返回值类型> operator <重载运算符>( [<参数列表>] ) { … } 在类外定义运算符重载函数的格式为: <函数返回值类型> <类名>::operator <重载运算符> ( [<参数列表>] ) {…} 以 operator 为关键字,编译器可以很容易将 运算符重载函数与其他成员函数区别开来。 例13.2实现复数类的“+”,“-”等重载运算 关键部分见下页
class Complex { float Real, Image; public: ... Complex operator +(const Complex &c); Complex operator +(double); Complex operator-(const Complex &c); Complex operator-(double); Complex operator-(void); ...... }; Complex Complex::operator +(const Complex &c) { return Complex (Real+c.Real, Image+c.Image); } Complex Complex::operator +(double r) { return Complex(Real+r, Image); } Complex Complex::operator-(void) { return Complex(-Real, -Image); } ……
void main( ) { Complex c1(2, 3), c2(4, -2), c3; c3 = c1+c2 ; …… c3 = c1+5 ; c3 = - c1; …… } 编译器将 c1+c2解释为:c1.operator+(c2) 将 c1+5解释为:c1.operator+(5) 将 - c1 解释为:c1.operator-( ) 第1个运算量是对象,第2个运算量是参数。 • 当用成员函数实现运算符的重载时,重载函数的 参数个数只能是 0 个或 1 个。分别实现:一元、二元 运算符的重载。 阅读教材上程序全文(讲解略)
2.重载为友元函数 在类内定义友元重载函数的格式为: friend <返回值类型> operator <重载运算符> ( [<参数列表>] ) {…} 在类外定义友元重载函数的格式为: <返回值类型> operator <重载运算符> ( [<参数列表>] ) {…} 例13.3用友元函数实现复数类的“+”和“-” 重载运算 关键部分见下页
class Complex{ float Real, Image; public: ... friend Complex operator +(const Complex &c1, const Complex &c2); friend Complex operator-(const Complex &c); ...... }; Complex operator +(const Complex &c1, const Complex &c2)//二元运算 { Complex t; t.Real=c1.Real+c2.Real; t.Image=c1.Image+c2.Image; return t; } Complex operator-(const Complex c) //一元运算 { return Complex(-c.Real, -c.Image) ; } 不是类的成员函数 在 main( )函数中, 若有 Complex c1,c2; 则编译器将 c1+c2 解释为: operator+(c1, c2) 将 -c1 解释为:operator-(c1)
成员实现:将 c1+c2 解释为:c1.operator +(c2) 友元实现:将 c1+c2 解释为:operator +(c1, c2) 成员实现:将 c1+5.6 解释为:c1.operator +(5.6) 友元实现:将 c1+5.6 解释为:operator +(c1, 5.6) 成员实现:将 5.6 + c1解释为: 5.6.operator + (c1) 友元实现:将 5.6 + c1解释为:operator + ( 5.6, c1) • 当用友元函数实现运算符的重载时,重载函数的 参数个数只能是1 个或 2 个。 分别实现:一元运算符重载、二元运算符重载 阅读教材上程序全文(讲解略) 3.两种重载方式的比较
因此: • 对一般的二元运算符重载为友元函数比重载为成员函数更优越。 • 但是对于赋值运算符,将其重载为成员函数较好,因为赋值运算符是一个二元运算符, 其语法格式为 <变量>=<表达式>, 第一个运算量必须是对象(变量也是对象),通过对象调用成员函数比较自然。 若重载为友元,则可能会出现5.6=c这样的表达式,与赋值表达式的语义不一致。
4.使用非成员、非友元实现运算符的重载 定义友元的目的是在友元函数中直接访问类的私有成员, 实际上,也可以通过公有函数接口访问类的私有成员, 所以实现运算符重载,可以即不用成员函数, 也不用友元函数。 例13.4: class Complex { float Real, Image; public: Complex(double r=0, double i=0) { Real=r; Image=i; } void SetReal(double Real){ Complex::Real = Real; } void SetImage(double Image) { Complex::Image = Image; } double GetReal( ){ return(Real); } double GetImage( ){ return(Image); } …… };
Complex operator +(Complex &c1, Complex &c2) { //二元运算 Complex t; t.SetReal( c1.GetReal( )+c2.GetReal( ) ); t.SetImage( c1.GetImage( )+c2.GetImage( ) ); return t; } void main( ) { Complex c1(2, 3), c2(4, 8), c3; c3 = c1+c2; c3.Show( ); } 编译器将 c1+c2 解释为: operator +(c1, c2)
5. 何时必须重载 = 和 += 运算符? • 相同类型的对象之间是可以直接赋值的,C++将赋值处理成两个对象的各个成员直接赋值。两个对象的对应数据成员逐一赋值。 对于Complex类,如有Complex c1(2, 3), c2; 则自动将 c2=c1; 处理成: c2.Real = c1.Real; c2.Image = c1. Image; 一般不会出现问题。 [例13.5] 在类中,用字符数组实现字符串。 见 “第13章 多态性(例子).doc”
stud1存储空间 stud2存储空间 Num数组 Name数组 Score整数 Num数组 Name数组 Score整数 程序中A行使用赋值运算符进行对象整体赋值, C++将其处理成各个成员逐一赋值,如下图所示: : C++默认的处理是:strcpy(stud2.Num, stud1.Num); strcpy(stud2.Name, stud1.Name); stud2.Score = stud1.Score; 。 • 但是如果对象的成员中有成员指向动态分配的 • 数据空间就会出现问题。
例13.6 在类中,用指针实现字符串, 即字符串的空间是动态分配的。 class Student { char *Nump; //学号,注意:用指针实现 char *Namep; //姓名,注意:用指针实现 int Score; //成绩 public: Student(char *nump=NULL, char *namep=NULL, int score=0) { if(nump) //构造函数 { Nump = new char[strlen(nump)+1]; strcpy(Nump, nump); //动态分配存储空间 } else Nump=NULL; if(namep) { … } Score=score; }
~Student( ) { // 析构函数,释放指针指向的空间 if(Nump)delete [ ]Nump; if(Namep)delete [ ]Namep; } void Show( ) { if(Nump && Namep) cout<<"Num="<<Nump<<'\t' <<"Name="<<Namep<<'\t' <<"Score="<<Score<<endl; } };
"01201" "Mary" stud2存储空间 stud1存储空间 Nump指针 Namep指针 Score整数 Nump指针 Namep指针 Score整数 void main( ) { Student stud1("01201", "Mary", 88), stud2; stud2=stud1; //A stud1.Show( ); stud2.Show( ); cout.flush( ); //B } 编译器将A行处理成: 首先撤消对象stud2,然后撤消对象stud1 ,出问题! 同一对象被撤销两次。
解决办法,在类中增加赋值 = 重载函数: Student & operator=(const Student &stud) // 返回值为对象自身的引用 { if(Nump)delete [ ]Nump; // C 释放对象自身的原串空间 if(Namep)delete [ ]Namep; // D 释放对象自身的原串空间 if(stud.Nump) // 根据赋值对象的空间大小给被赋值对象分配空间 { Nump = new char[strlen(stud.Nump)+1]; strcpy(Nump, stud.Nump); } else Nump=NULL; if(stud.Namep) // 根据赋值对象的空间大小给被赋值对象分配空间 { Namep = new char[strlen(stud.Namep)+1]; strcpy(Namep, stud.Namep); } else Namep=NULL; Score=stud.Score; return *this; // *this是对象自身 }
"01201" "01201" "Mary" "Mary" stud2存储空间 stud1存储空间 Nump指针 Namep指针 Score整数 Nump指针 Namep指针 Score整数 在赋值时,为目的对象的指针重新分配指向的字符串空间。 增加赋值 = 重载函数后,对象赋值后的存储空间如下: 这样,程序结束时,分别撤销两个对象,程序正确运行!
6.对于+=(或=)运算符,重载函数的返回值为void类型或本类类型对象的区别 [例13.7] 见 “第13章 多态性(例子).doc” • 若重载为返回void类型,则本类对象不可连续赋值。 • 若重载为返回本类类型,则本类对象可以连续赋值。
7.对于+=(或=)运算符,返回本类对象与返回本类对象的引用的区别 比较下面两例,先看第一个例子: Complex Complex ::operator+=(const Complex &c) { Real+=c.Real; Image+=c.Image; return *this; } 此函数的返回值为本类对象,C++的处理是:用返回的对象值*this初始化内存临时对象(调用拷贝构造函数),从本函数返回后,用内存临时对象作为调用函数的结果。
再看第二个例子: Complex Complex ::operator+=(const Complex &c) { Real+=c.Real; Image+=c.Image; Complex temp = *this ; return temp; } C++的处理是:调用拷贝构造函数,用返回的对象值temp初始化内存临时对象,内存临时对象作为调用函数的结果。
从上面两个小例子可以看出,若重载函数返回对象值,则返回*this和返回temp均可。从上面两个小例子可以看出,若重载函数返回对象值,则返回*this和返回temp均可。 但要注意,因为返回对象值时需要调用拷贝构造函数初始化内存临时对象,因此若对象有动态分配的存储空间,就必须定义拷贝构造函数。
第三个例子: Complex & Complex ::operator+=(const Complex &c) { Real+=c.Real; Image+=c.Image; return *this; } 本例的返回值为本类对象的引用,不需初始化内存临时对象,返回值是对象自身,即*this。因为不需要初始化内存临时对象,效率较高,所以像 +=、= 等改变对象值的重载函数最好返回对象的引用。
第四个例子: Complex & Complex ::operator+=(const Complex &c) { Real+=c.Real; Image += c.Image; Complex temp = *this ; return temp; } 此时,出现问题。因为返回的是对象的引用(即对象自身,不借助于内存临时对象),系统要求在执行流程返回到调用处时,返回值是存在的。但是本例返回值是函数内部的局部对象temp,而局部对象在函数返回前,其空间是被撤消的。
结论: • 返回本类对象时,可以用对象自身和局部对象做为返回值(有时需要定义拷贝构造函数)。 • 返回对象的引用时,不能用局部对象做为返回值。 [例13.8]对于字符串类,重载 = 运算符,返回对象自身的引用。就本例而言,可以不定义拷贝构造函数,程序能正确运行。 见 “第13章 多态性(例子).doc”
8.调用拷贝构造函数 和 调用赋值运算符重载函数的时机 class Complex { double Real, Image; public: Complex(double r=0, double i=0) // 构造函数 { Real=r; Image=i; } Complex(Complex &c) // 拷贝构造函数 { Real=c.Real; Image=c.Image; cout<<"Call copy "<<Real<<' '<<Image<<"\n"; } }; void main( ) { Complex c1(2, 3), c2(4, -2); Complex c3=c1; c1=c2; } • 只有在产生新对象时,调用构造函数。 • 用已有对象初始化新产生的对象时, • 调用拷贝构造函数。 • 赋值运算 = , 不调用拷贝构造函数。 程序的运行结果是? Call copy 2 3 ←调用了拷贝构造函数 ←未调用拷贝构造函数
13.2.3 类型转换函数-将本类对象转换成其他类对象 对于①,通过以前所学的构造函数实现类型转换, 将其他类型的数据转换成本类数据。 对于②,使用本节将介绍的类型转换运算符重载函数, 将本类数据转换成其他类型的数据。 例: Complex c(3, 2); double x=6.2; 如果有: c=x; //类型不一致① 或:x=c; //类型不一致② 则系统自动处理为: c = Complex(x); //需作类型转换① x = double(c); //需作类型转换② 构造函数: Complex::Complex(double r) { real = r ; image = 0; }
在类内定义类型转换函数的一般格式为: operator <目标数据类型> ( ) { ... } 在类外定义类型转换函数的一般格式为: <类名> :: operator <目标数据类型> ( ) { ... } 该函数不能有参数, 不能写返回值类型, 因返回值类型已确定。 定义类型转换函数: 功能:将 <类名>类对象自动转换成 <目标数据类型>类型对象 ★类型转换函数只能用成员函数实现, 不能用友元函数实现。
系统自动处理为 x = c.operator double( ); 例13.10类型转换函数的定义和使用 #include <iostream.h> class Complex { double Real, Image; public: Complex(double r=0, double i=0) { Real=r; Image=i; } operator double ( ) // A 类型转换函数, // 将 Complex 类转换成 double 类 { return Real; } }; void main( ) { Complex c(3,2); double x; x = c; // B cout << "x=" << x << endl; } // 即 x = double (c) ;
使用 类型自动转换 较方便、自然。 例13.11 成员函数和类型转换函数的比较 见 “第13章 多态性(例子).doc” 其中主函数为: void main(void) { RMB r(23, 8, 6); int r1, r2, r3; r1= r ; //处理成 r1 = r.operator int( ); r2= int(r); //处理成 r2 = r.operator int( ) ; r3= r.GetFen( ); cout<<"r1="<<r1<<endl; cout<<"r2="<<r2<<endl; cout<<"r3="<<r3<<endl; …… }
int 用于区分,没有实际意义, 可给出实参,也可以不给出实参。 13.2.4 其他运算符的重载 1.重载++、--运算符 重载前置 ++运算符的成员函数的一般格式为: <函数返回值> <类名>::operator++ ( ) { ... } 重载后置 ++ 运算符的成员函数的一般格式为: <函数返回值> <类名>::operator++ (int) { ... }
int 用于区分,没有实际意义, 可给出实参,也可以不给出实参。 重载前置 ++运算符的友元函数的一般格式为: friend <函数返回值> operator++ (<类名> &obj ) { ... } 重载后置++运算符的友元函数的一般格式为: friend <函数返回值> operator++ (<类名> &obj, int) { ... } 例13.12实现++和 -- 的前置和后置运算符重载, 程序见 “第13章 多态性(例子).doc”
13.2.5 字符串类 C++ 提供的字符串处理能力较弱,如不能用运算符直接对字符串对象进行加、减等操作, 而必须通过字符串处理函数来实现拷贝、连接等操作。 如:char s1[10]="abc", s2[10]="123"; strcpy(s1, s2); //不能写成 s1= s2; strcat(s1, s2); //不能写成 s1= s1+s2; 能否定义一个字符串类:String 实现:String s1("abc"), s2("123"), s3; s1 = s2; s3 = s1 + s2; 能! 可以利用C++提供的运算符重载实现。
例13.18定义字符串类String,并测试重载的运算符以及成员函数例13.18定义字符串类String,并测试重载的运算符以及成员函数 程序见 “第13章 多态性(例子).doc”,或阅读教材上的程序。 重点讲解: (1) 说明:函数名后的 const (2) (拷贝)构造函数,在主函数中如何使用? (3) 重载赋值 = 运算符 (4) 重载 + 运算符 (5) 类型转换函数 operator const char * (6) 删除子串图示见下页
删除子串: String operator - (const String &s1, const char *s2 ) s1.Strp p1 p2 s2 len t.Strp
13.3 静态联编 • 联编是指一个计算机程序彼此关联的过程。 • 在本章中指函数间调用关系的确定。 • 按照联编所确定的时刻不同,可分为两种: 静态联编和动态联编。 • 静态联编是指联编出现在编译连接阶段, 即函数调用关系的确定是在程序执行之前。 • 这种联编又称早期联编, 通过这种联编可实现静态多态。
例13.20 普通函数的静态联编 #include <iostream.h> int add(int a, int b) //重载函数1 { return(a+b); } double add(double a, double b) //重载函数2 { return(a+b); } void main( ) { cout<<add(1, 2)<<'\t'; //编译时确定调用重载函数1 cout<<add(1.1, 2.2)<<'\n'; //编译时确定调用重载函数2 } 在编译连接阶段,就能根据参数的个数和类型确定调用的是哪一个函数。
Point Rectangle Circle 用派生类实参初始化基类型参, p只能引用基类的成员。 例13.21 读书上程序 重点讲解: double CalcArea( Point &p ) { return(p.Area( )); } //A 编译连接时确定调用函数 1 void main( ) { Rectangle r(0, 0, 1, 1); Circle c(0, 0, 1); cout<<CalcArea( r )<<'\t'; cout<<CalcArea( c )<<'\n'; } 能否找到一种机制, 让CalcArea( )函数变成一个 通用的求面积的函数。 这就是C++提供的动态联编和 虚函数应完成的工作。
13.4 动态联编和虚函数 • 运行阶段才能确定函数的调用关系,这就是动态联编 • 动态联编又称滞后联编、晚期联编, • 动态联编技术能实现动态多态。 • 必须将类的成员函数定义成虚函数, 才可以实现动态联编。 13.4.1 虚函数 将成员函数定义成虚函数的格式为: virtual <函数返回值类型> <函数名>( [<参数列表>] ) {…}
例13.22 阅读书上程序 重点讲解: double CalcArea(Point &p) { return( p.Area( ) ); } //A Area( )是虚函数 void main( ) { Point p(1, 2); Rectangle r(0, 0, 1, 1); Circle c(0, 0, 1); cout<<CalcArea( p )<<'\t‘ <<CalcArea( r )<<'\t‘ <<CalcArea( c )<<'\n'; } ←通用的求面积函数 Area( )是虚函数 , C++ 规定, 在 A 行保留相关的 三个虚函数入口地址 。 在程序的运行阶段, 根据实参的类型来确定 调用哪一个虚函数。
有关虚函数的说明: (1)派生类的虚函数必须与基类虚函数同名, 且参数的类型、个数、顺序必须一致, 否则,属于函数重载,而不是虚函数。 (2)基类中虚函数前的关键字virtual不能缺省。 (3)必须通过基类对象的指针或引用调用虚函数, 才能实现动态多态。 (4)虚函数必须是类的成员函数,不能是友元函数, 也不能是静态成员函数。 (5)不能将构造函数定义为虚函数, 但可将析构函数定义为虚函数。 (6)调用虚函数速度较慢。因为,要实现动态多态, 在函数调用处必须保留多个虚函数的入口地址。
我们注意,在成员函数中调用成员函数时, 系统都是通过对象自身的指针this调用的, A类中的fun2( )的实际被处理成如下形式: void fun2( ) { cout << "A::fun2" << '\t'; this->fun3( ); //E } 例13.23 在成员函数中调用虚函数 程序见 “第13章 多态性(例子).doc”,或阅读教材上的程序。 B b; b.fun1( ); // 调用A类的fun1( )和fun2( ),在A类的fun2( )函数中, // 在E行, this 是指向b的指针, 所以调用B的fun3( )函数
A B C 例13.24 在构造函数中调用虚函数 class A { public: A( ) { fun( ); } virtual void fun( ) { cout << "A::fun" << '\t'; } }; class B: public A { public: B( ) { fun( ); } void fun( ) { cout << "B::fun" << '\t'; } void g( ) { fun( ); } // 在成员函数中调用虚函数 };
C B A class C: public B { public: C( ) { fun( ); } void fun( ) { cout << "C::fun" << '\n'; } }; void main( ) { C c; //依次调用A、B、C三类的缺省构造函数 c.g( ); } 运行结果: A::fun B::fun C::fun C::fun ★构造函数调用虚函数,调用的是类本身的虚函数。 ★成员函数调用虚函数,遵循动态多态性原则。
13.4.2 虚析构函数 如果类的构造函数中有动态申请的存储空间, 在析构函数中应释放该空间。 此时,建议将析构函数定义为虚函数, 以便实现通过基类的指针或引用 撤消派生类对象时的多态性。
例:析构函数不是虚函数的情况 #include <iostream.h> class A { char *Aptr; public: A( ) { Aptr = new char[100]; } ~A( ) //析构函数不是虚函数 { delete [ ]Aptr; cout<<"Delete [ ]Aptr"<<endl; } };