1 / 58

目 录

第 1 章 C++ 概述 第 2 章 数据类型、运算符和表达式 第 3 章 简单的输入 / 输出 第 4 章 C++ 的流程控制 第 5 章 函数 第 6 章 编译预处理 第 7 章 数组 第 8 章 结构体、共同体和枚举类型 第 9 章 指针和引用 第 10 章 类和对象 第 11 章 类和对象的其他特性 第 12 章 继承和派生 第 13 章 多态性 第 14 章 输入 / 输出流 第 15 章 模板. 目 录. 第一部分 面向过程的程序设计.

gallia
Download Presentation

目 录

An Image/Link below is provided (as is) to download presentation Download Policy: Content on the Website is provided to you AS IS for your information and personal use and may not be sold / licensed / shared on other websites without getting consent from its author. Content is provided to you AS IS for your information and personal use only. Download presentation by click this link. While downloading, if for some reason you are not able to download a presentation, the publisher may have deleted the file from their server. During download, if you can't get a presentation, the file might be deleted by the publisher.

E N D

Presentation Transcript


  1. 第1章 C++概述 第2章 数据类型、运算符和表达式 第3章 简单的输入/输出 第4章 C++的流程控制 第5章 函数 第6章 编译预处理 第7章 数组 第8章 结构体、共同体和枚举类型 第9章 指针和引用 第10章 类和对象 第11章 类和对象的其他特性 第12章 继承和派生 第13章 多态性 第14章 输入/输出流 第15章 模板 目 录 • 第一部分 面向过程的程序设计 • 第二部分 面向对象的程序设计

  2. 第13章 多态性

  3. 13.1 函数重载 13.2 运算符重载 13.3 静态联编 13.4 动态联编和虚函数 13.5 纯虚函数和抽象类

  4. 第 13 章 多态性 • 多态性是实现 OOP 的关键技术之一。 • 在C++中,多态性分为两种: ①静态多态 ②动态多态 • 函数重载和运算符重载属于静态多态。 • 函数重载:相同函数名可以完成不同功能。 • 运算符重载:相同运算符完成不同功能。 • 动态多态是指:程序执行过程中确定的关系, 如动态确定函数的调用关系。 • 运行时的多态(动态多态) 是通过类的继承和虚函数来实现的。

  5. 13.1 函数重载 参见5.6节(略) 13.2 重载运算符 • C++中所有的运算符都已预先定义了用法及意义。 如:+ - * / = 等,适用于已有的数据类型。 • 已定义的用法及意义是不允许用户改变的。 • 如果用户定义了新的数据类型,希望已定义的运算符 能适应新的数据类型, • 如: 前述 Complex 类的加法运算 Complex c1, c2, c3; c3 = add(c1, c2); //以前通过函数实现加法运算 c2.add(c3); // 或通过成员函数实现 c3 = c1 + c2;//能否直观地写成这样? 能 !运算符的重载可以达到此目的。

  6. 13.2.1 运算符重载的几点说明 • 允许重载 和 不允许重载的运算符见13-1和13-2表。 • 重载运算符的限制 (1)只能对已有运算符重载,不可臆造新的运算符。 (2)不允许改变运算符的优先级和结合性。 (3)不允许改变运算符的语法结构, 如二元运算符只能重载成二元运算符, 一元运算符只能重载成一元运算符。

  7. 13.2.2 运算符重载的两种方式 特殊的成员函数名 1.重载为类的成员函数 在类内定义运算符重载函数的格式为: <函数返回值类型> operator <重载运算符>( [<参数列表>] ) { … } 在类外定义运算符重载函数的格式为: <函数返回值类型> <类名>::operator <重载运算符> ( [<参数列表>] ) {…} 以 operator 为关键字,编译器可以很容易将 运算符重载函数与其他成员函数区别开来。 例13.2实现复数类的“+”,“-”等重载运算 关键部分见下页

  8. 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); } ……

  9. 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 个。分别实现:一元、二元 运算符的重载。 阅读教材上程序全文(讲解略)

  10. 2.重载为友元函数 在类内定义友元重载函数的格式为: friend <返回值类型> operator <重载运算符> ( [<参数列表>] ) {…} 在类外定义友元重载函数的格式为: <返回值类型> operator <重载运算符> ( [<参数列表>] ) {…} 例13.3用友元函数实现复数类的“+”和“-” 重载运算 关键部分见下页

  11. 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)

  12. 成员实现:将 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.两种重载方式的比较

  13. 因此: • 对一般的二元运算符重载为友元函数比重载为成员函数更优越。 • 但是对于赋值运算符,将其重载为成员函数较好,因为赋值运算符是一个二元运算符, 其语法格式为 <变量>=<表达式>, 第一个运算量必须是对象(变量也是对象),通过对象调用成员函数比较自然。 若重载为友元,则可能会出现5.6=c这样的表达式,与赋值表达式的语义不一致。

  14. 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); } …… };

  15. 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)

  16. 5. 何时必须重载 = 和 += 运算符? • 相同类型的对象之间是可以直接赋值的,C++将赋值处理成两个对象的各个成员直接赋值。两个对象的对应数据成员逐一赋值。 对于Complex类,如有Complex c1(2, 3), c2; 则自动将 c2=c1; 处理成: c2.Real = c1.Real; c2.Image = c1. Image; 一般不会出现问题。 [例13.5] 在类中,用字符数组实现字符串。 见 “第13章 多态性(例子).doc”

  17. 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; 。 • 但是如果对象的成员中有成员指向动态分配的 • 数据空间就会出现问题。

  18. 例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; }

  19. ~Student( ) { // 析构函数,释放指针指向的空间 if(Nump)delete [ ]Nump; if(Namep)delete [ ]Namep; } void Show( ) { if(Nump && Namep) cout<<"Num="<<Nump<<'\t' <<"Name="<<Namep<<'\t' <<"Score="<<Score<<endl; } };

  20. "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 ,出问题! 同一对象被撤销两次。

  21. 解决办法,在类中增加赋值 = 重载函数: 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是对象自身 }

  22. "01201" "01201" "Mary" "Mary" stud2存储空间 stud1存储空间 Nump指针 Namep指针 Score整数 Nump指针 Namep指针 Score整数 在赋值时,为目的对象的指针重新分配指向的字符串空间。 增加赋值 = 重载函数后,对象赋值后的存储空间如下: 这样,程序结束时,分别撤销两个对象,程序正确运行!

  23. 6.对于+=(或=)运算符,重载函数的返回值为void类型或本类类型对象的区别 [例13.7] 见 “第13章 多态性(例子).doc” • 若重载为返回void类型,则本类对象不可连续赋值。 • 若重载为返回本类类型,则本类对象可以连续赋值。

  24. 7.对于+=(或=)运算符,返回本类对象与返回本类对象的引用的区别 比较下面两例,先看第一个例子: Complex Complex ::operator+=(const Complex &c) { Real+=c.Real; Image+=c.Image; return *this; } 此函数的返回值为本类对象,C++的处理是:用返回的对象值*this初始化内存临时对象(调用拷贝构造函数),从本函数返回后,用内存临时对象作为调用函数的结果。

  25. 再看第二个例子: Complex Complex ::operator+=(const Complex &c) { Real+=c.Real; Image+=c.Image; Complex temp = *this ; return temp; } C++的处理是:调用拷贝构造函数,用返回的对象值temp初始化内存临时对象,内存临时对象作为调用函数的结果。

  26. 从上面两个小例子可以看出,若重载函数返回对象值,则返回*this和返回temp均可。从上面两个小例子可以看出,若重载函数返回对象值,则返回*this和返回temp均可。 但要注意,因为返回对象值时需要调用拷贝构造函数初始化内存临时对象,因此若对象有动态分配的存储空间,就必须定义拷贝构造函数。

  27. 第三个例子: Complex & Complex ::operator+=(const Complex &c) { Real+=c.Real; Image+=c.Image; return *this; } 本例的返回值为本类对象的引用,不需初始化内存临时对象,返回值是对象自身,即*this。因为不需要初始化内存临时对象,效率较高,所以像 +=、= 等改变对象值的重载函数最好返回对象的引用。

  28. 第四个例子: Complex & Complex ::operator+=(const Complex &c) { Real+=c.Real; Image += c.Image; Complex temp = *this ; return temp; } 此时,出现问题。因为返回的是对象的引用(即对象自身,不借助于内存临时对象),系统要求在执行流程返回到调用处时,返回值是存在的。但是本例返回值是函数内部的局部对象temp,而局部对象在函数返回前,其空间是被撤消的。

  29. 结论: • 返回本类对象时,可以用对象自身和局部对象做为返回值(有时需要定义拷贝构造函数)。 • 返回对象的引用时,不能用局部对象做为返回值。 [例13.8]对于字符串类,重载 = 运算符,返回对象自身的引用。就本例而言,可以不定义拷贝构造函数,程序能正确运行。 见 “第13章 多态性(例子).doc”

  30. 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 ←调用了拷贝构造函数 ←未调用拷贝构造函数

  31. 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; }

  32. 在类内定义类型转换函数的一般格式为: operator <目标数据类型> ( ) { ... } 在类外定义类型转换函数的一般格式为: <类名> :: operator <目标数据类型> ( ) { ... } 该函数不能有参数, 不能写返回值类型, 因返回值类型已确定。 定义类型转换函数: 功能:将 <类名>类对象自动转换成 <目标数据类型>类型对象 ★类型转换函数只能用成员函数实现, 不能用友元函数实现。

  33. 系统自动处理为 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) ;

  34. 使用 类型自动转换 较方便、自然。 例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; …… }

  35. int 用于区分,没有实际意义, 可给出实参,也可以不给出实参。 13.2.4 其他运算符的重载 1.重载++、--运算符 重载前置 ++运算符的成员函数的一般格式为: <函数返回值> <类名>::operator++ ( ) { ... } 重载后置 ++ 运算符的成员函数的一般格式为: <函数返回值> <类名>::operator++ (int) { ... }

  36. int 用于区分,没有实际意义, 可给出实参,也可以不给出实参。 重载前置 ++运算符的友元函数的一般格式为: friend <函数返回值> operator++ (<类名> &obj ) { ... } 重载后置++运算符的友元函数的一般格式为: friend <函数返回值> operator++ (<类名> &obj, int) { ... } 例13.12实现++和 -- 的前置和后置运算符重载, 程序见 “第13章 多态性(例子).doc”

  37. 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++提供的运算符重载实现。

  38. 例13.18定义字符串类String,并测试重载的运算符以及成员函数例13.18定义字符串类String,并测试重载的运算符以及成员函数 程序见 “第13章 多态性(例子).doc”,或阅读教材上的程序。 重点讲解: (1) 说明:函数名后的 const (2) (拷贝)构造函数,在主函数中如何使用? (3) 重载赋值 = 运算符 (4) 重载 + 运算符 (5) 类型转换函数 operator const char * (6) 删除子串图示见下页

  39. 删除子串: String operator - (const String &s1, const char *s2 ) s1.Strp p1 p2 s2 len t.Strp

  40. 13.3 静态联编 • 联编是指一个计算机程序彼此关联的过程。 • 在本章中指函数间调用关系的确定。 • 按照联编所确定的时刻不同,可分为两种: 静态联编和动态联编。 • 静态联编是指联编出现在编译连接阶段, 即函数调用关系的确定是在程序执行之前。 • 这种联编又称早期联编, 通过这种联编可实现静态多态。

  41. 例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 } 在编译连接阶段,就能根据参数的个数和类型确定调用的是哪一个函数。

  42. 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++提供的动态联编和 虚函数应完成的工作。

  43. 13.4 动态联编和虚函数 • 运行阶段才能确定函数的调用关系,这就是动态联编 • 动态联编又称滞后联编、晚期联编, • 动态联编技术能实现动态多态。 • 必须将类的成员函数定义成虚函数, 才可以实现动态联编。 13.4.1 虚函数 将成员函数定义成虚函数的格式为: virtual <函数返回值类型> <函数名>( [<参数列表>] ) {…}

  44. 例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 行保留相关的 三个虚函数入口地址 。 在程序的运行阶段, 根据实参的类型来确定 调用哪一个虚函数。

  45. 有关虚函数的说明: (1)派生类的虚函数必须与基类虚函数同名, 且参数的类型、个数、顺序必须一致, 否则,属于函数重载,而不是虚函数。 (2)基类中虚函数前的关键字virtual不能缺省。 (3)必须通过基类对象的指针或引用调用虚函数, 才能实现动态多态。 (4)虚函数必须是类的成员函数,不能是友元函数, 也不能是静态成员函数。 (5)不能将构造函数定义为虚函数, 但可将析构函数定义为虚函数。 (6)调用虚函数速度较慢。因为,要实现动态多态, 在函数调用处必须保留多个虚函数的入口地址。

  46. 我们注意,在成员函数中调用成员函数时, 系统都是通过对象自身的指针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( )函数

  47. 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( ); } // 在成员函数中调用虚函数 };

  48. 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 ★构造函数调用虚函数,调用的是类本身的虚函数。 ★成员函数调用虚函数,遵循动态多态性原则。

  49. 13.4.2 虚析构函数 如果类的构造函数中有动态申请的存储空间, 在析构函数中应释放该空间。 此时,建议将析构函数定义为虚函数, 以便实现通过基类的指针或引用 撤消派生类对象时的多态性。

  50. 例:析构函数不是虚函数的情况 #include <iostream.h> class A { char *Aptr; public: A( ) { Aptr = new char[100]; } ~A( ) //析构函数不是虚函数 { delete [ ]Aptr; cout<<"Delete [ ]Aptr"<<endl; } };

More Related