
C程序设计第8章1.ppt
44页本周习题:本周习题: P307 习题习题8.6 实践教程:实践教程:P89 实验二十四实验二十四 2本周实验:本周实验: 习题上机调试习题上机调试 预习:预习:第第9章流类库与输入章流类库与输入/输出输出 9.1~9.3((1)理解)理解C++的基本流类体系;的基本流类体系;((2)提高标准输入)提高标准输入/输出的健壮性;输出的健壮性;((3)掌握提取与插入运算符的重载)掌握提取与插入运算符的重载((4)掌握文件的输入)掌握文件的输入/输出:文件的打开与关闭,文本输出:文件的打开与关闭,文本 文件与二进制文件文件与二进制文件5)掌握在构造函数中通过文件建立对象,在析构函数)掌握在构造函数中通过文件建立对象,在析构函数 中由文件保存对象的技术中由文件保存对象的技术类似用实型变量描述整数,这条规则称类似用实型变量描述整数,这条规则称赋值兼容规则赋值兼容规则赋值兼容容包括以下三种情况:包括以下三种情况: 派生类与基类(赋值兼容)派生类与基类(赋值兼容)1.派生类对象赋值给基类的对象。
反之不行如:派生类对象赋值给基类的对象反之不行如: Person p; Student s; p=s;2.派生类对象的地址赋给基类指针反之不行如:派生类对象的地址赋给基类指针反之不行如: Person *pointP; pointP=&s; 3.派生类对象可以初始化基类的引用反之不行如:派生类对象可以初始化基类的引用反之不行如: Person &refP=s;常见用法是函数形参为基类引用,实参是派生类对象常见用法是函数形参为基类引用,实参是派生类对象注意:注意:以上赋值后都只能间接使用到以上赋值后都只能间接使用到继承来继承来的成员#include
同基类数据成员的多份同名成员 二义性二义性解决办法:解决办法: 定义虚基类,使得在继承间接共同基类时只保留一份定义虚基类,使得在继承间接共同基类时只保留一份成员 为保证虚基类在派生类中只继承一次,应当在该基类为保证虚基类在派生类中只继承一次,应当在该基类的所有直接派生类中声明为虚基类否则仍然会出现对的所有直接派生类中声明为虚基类否则仍然会出现对基类的多次继承基类的多次继承声明虚基类格式:声明虚基类格式: class 派生类名:派生类名:virtual 继承方式继承方式 基类名基类名虚基类的初始化:虚基类的初始化:class A{ public: A(int i){ }};class B::virtual public A{ public: B(int n)::A(n){ }};class C::virtual public A{ public: C(int n)::A(n){ }};class D::public B , public C{ public: D(int n)::A(n),B(n),C(n){ }};注意:注意:1. 1.前介绍,在派生类的构造函数中只需对其直接基类初始前介绍,在派生类的构造函数中只需对其直接基类初始化,再由其直接基类负责对间接基类初始化。
化,再由其直接基类负责对间接基类初始化2. 2. 现在,由于虚基类在派生类中只有一份数据成员,所以现在,由于虚基类在派生类中只有一份数据成员,所以这份数据成员的初始化必须由派生类直接给出这份数据成员的初始化必须由派生类直接给出 派生类不仅要负责对其直接基类初始化还要负责对虚派生类不仅要负责对其直接基类初始化还要负责对虚基类初始化基类初始化在派生类对象的在派生类对象的创建创建中:中:首先是虚基类的构造函数并按它们声明的顺序构造首先是虚基类的构造函数并按它们声明的顺序构造第二批是非虚基类的构造函数按它们声明的顺序调用第二批是非虚基类的构造函数按它们声明的顺序调用第三批是成员对象的构造函数第三批是成员对象的构造函数最后是派生类自己的构造函数被调用最后是派生类自己的构造函数被调用构造函数执行次序:构造函数执行次序:析构的次序与构造的次序相反析构的次序与构造的次序相反例:例:通过构造函数的初始化表对虚基类进行初始化通过构造函数的初始化表对虚基类进行初始化include 过构造函数的初始化表对虚基类进行初始化void main( ){ D d1(100,200,300,400,500); d1.Print(); d1.x=400; d1.Print();}输出输出::x=0 y=100 // //基类构造函数使用了默认值做参数基类构造函数使用了默认值做参数x=0 z=300 // //基类构造函数使用了默认值做参数基类构造函数使用了默认值做参数m=500x=400 y=100 // //主函数改变了基类参数值主函数改变了基类参数值x=400 z=300 // //主函数改变了基类参数值主函数改变了基类参数值m=500D析构析构C析构析构B析构析构A析构析构D(int a,int b,int d,int e,int f):A( ),B(a,b),C(d,e){ m=f; }8.7 8.7 多态性与虚函数多态性与虚函数多态性是面向对象程序设计的关键技术之一指的是多态性是面向对象程序设计的关键技术之一指的是调用同样的函数实现不同的功能调用同样的函数实现不同的功能。 在在C++中有两中有两种多态种多态编译时的编译时的多态性多态性 (静态多态性)(静态多态性)运行时的运行时的多态性多态性 在程序执行前无法确定调用哪一在程序执行前无法确定调用哪一个函数,必须在程序执行过程中,个函数,必须在程序执行过程中,根据执行的具体情况来动态地确根据执行的具体情况来动态地确定通过类继承关系和虚函数来通过类继承关系和虚函数来实现的目的:建立一种通用的实现的目的:建立一种通用的程序通过函数的重载和运通过函数的重载和运算符的重载实现算符的重载实现多态性是多态性是“一个接口,多种方法一个接口,多种方法”: : 通过继承产生了相关的不同的派生类,和基类成员同通过继承产生了相关的不同的派生类,和基类成员同名的成员在不同的派生类中有不同的含义名的成员在不同的派生类中有不同的含义虚函数的作用虚函数的作用: : 允许在派生类中重新定义与基类同名的函数,并且允许在派生类中重新定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和派生类中的同名可以通过基类指针或引用来访问基类和派生类中的同名函数虚函数是一个类的成员函数,定义虚函数的格式如下:虚函数是一个类的成员函数,定义虚函数的格式如下: virtual 返回类型返回类型 函数名(参数表);函数名(参数表); 注:注:virtual仅用于类定义中,若虚函数在类外定义,不能仅用于类定义中,若虚函数在类外定义,不能 加加virtual(类中函数说明要加类中函数说明要加virtual )。 当某一个类的一个类成员函数被定义为虚函数,则由当某一个类的一个类成员函数被定义为虚函数,则由该类派生出来的所有派生类中,该函数始终保持虚函数的该类派生出来的所有派生类中,该函数始终保持虚函数的特征 8.7.1 8.7.1 虚函数的定义虚函数的定义在派生类中重新定义虚函数在派生类中重新定义虚函数(称称超载超载或或覆盖覆盖) )时,不必再加关键时,不必再加关键字字virtual,,但但函数头一定要完全相同函数头一定要完全相同虚函数的使用虚函数的使用: (1) 在基类用在基类用virtual声明成员函数为虚函数声明成员函数为虚函数 (2) 在派生类中重新定义此函数,要求函数名、函数类型、在派生类中重新定义此函数,要求函数名、函数类型、函数参数个数和类型全部与基类的虚函数相同,并根据函数参数个数和类型全部与基类的虚函数相同,并根据派生类的需要重新定义函数体派生类的需要重新定义函数体3) 定义一个指向基类对象的指针变量,并使它指向同一定义一个指向基类对象的指针变量,并使它指向同一类族中需要调用该函数的对象类族中需要调用该函数的对象4) 通过该指针变量调用此虚函数,此时调用的就是指针通过该指针变量调用此虚函数,此时调用的就是指针变量指向的对象的同名函数。 变量指向的对象的同名函数例:例:#include 计算学分 由本科生类派生出研究生类由本科生类派生出研究生类GradeStudent,但它们各自,但它们各自的从课程学时数折算为学分数的算法是不同的,本科生是的从课程学时数折算为学分数的算法是不同的,本科生是1616个学时个学时一学分,而研究生是一学分,而研究生是2020个学时一学分个学时一学分class Student{char coursename[20];// //课程名课程名int classhour;// //学时学时int credit; // //学分学分, ,未考虑未考虑0.50.5学分学分public:Student();;// //构造函数,将类成员赋值为构造函数,将类成员赋值为0 0void Calculate();;// // 计算学分(考虑该函数的设置)计算学分(考虑该函数的设置)void SetCourse(char *str,int hour);;// //设置课程和学时设置课程和学时int GetHour();;// //获取学时数获取学时数void SetCredit(int cred);; // //获取学分获取学分void Print();;// //显示学时和学分显示学时和学分};class Student{ char coursename[20]; // //课程名课程名 int classhour; // //学时学时 int credit; // //学分学分public: Student(){ coursename[0]='\0'; classhour=0; credit=0; } virtual void Calculate(){ credit=classhour/16; } void SetCourse(char *str,int hour){ // //设置课程和学时设置课程和学时 strcpy(coursename,str); classhour=hour; } int GetHour(){ return classhour; } // //获取学时数获取学时数 void SetCredit(int cred){ credit=cred; } // //获取学分获取学分 voidPrint(){ // //显示学时和学分显示学时和学分 cout< 使用基类引用去指向不同对象,同样可实现运行时多态性void Calfun(Student &ps,char *str,int hour){ps.SetCourse(str,hour);ps.Calculate();ps.Print();}void main(){Student s;GradeStudent g;cout<<"本科生本科生:";Calfun(s,"物理物理",80);cout<<"研究生研究生:";Calfun(g,“物理物理”,80);}【【例例8.7】】计算学分,计算学分,基类与派生类定义同基类与派生类定义同【【例例8.6】】(基类派生类(基类派生类函数不变),增加函数:函数不变),增加函数:几点提示:几点提示:5.虚函数重构不同于重载虚函数重构不同于重载派生类中定义虚函数必须与基类中派生类中定义虚函数必须与基类中的虚函数同名外,还必须同参数表,同返回类型否则被认为是重的虚函数同名外,还必须同参数表,同返回类型否则被认为是重载,而不是虚函数载,而不是虚函数 1.实现动态多态性需要三个条件:派生类体系、虚函数、指针实现动态多态性需要三个条件:派生类体系、虚函数、指针或引用。 或引用2.析构函数可定义为虚函数,构造函数不能定义虚函数析构函数可定义为虚函数,构造函数不能定义虚函数在基在基类及其派生类都有动态分配的内存空间时,应当考虑把析构函数定类及其派生类都有动态分配的内存空间时,应当考虑把析构函数定义为虚函数,以便能够用基类指针实现撤消对象的多态性义为虚函数,以便能够用基类指针实现撤消对象的多态性3. 虚函数执行速度稍慢为了实现多态性,每一个派生类中均虚函数执行速度稍慢为了实现多态性,每一个派生类中均要保存相应虚函数的入口地址表,函数的调用机制也与一般函数不要保存相应虚函数的入口地址表,函数的调用机制也与一般函数不同这是多态性为实现通用性付出的代价这是多态性为实现通用性付出的代价4. 静态成员函数不能作为虚函数,因为它不属于某个对象,而静态成员函数不能作为虚函数,因为它不属于某个对象,而是为所有同类对象共有是为所有同类对象共有内联函数也不能作为虚函数内联函数也不能作为虚函数声明虚函数的成员函数主要考虑以下几点声明虚函数的成员函数主要考虑以下几点: :(1)(1)首先看成员函数所在的类是否会作为基类然后看成首先看成员函数所在的类是否会作为基类然后看成(2)(2)员函数在类的继承后有无可能被更改功能,如果希望更改员函数在类的继承后有无可能被更改功能,如果希望更改(3)(3)其功能的,一般应该将它声明为虚函数。 其功能的,一般应该将它声明为虚函数2) (2) 如果成员函数在类被继承后功能不需修改,或派生类如果成员函数在类被继承后功能不需修改,或派生类用不到该函数,则不要把它声明为虚函数用不到该函数,则不要把它声明为虚函数3) (3) 应考虑对成员函数的调用是通过对象名还是通过基类应考虑对成员函数的调用是通过对象名还是通过基类指针或引用去访问,如果是通过基类指针或引用去访问指针或引用去访问,如果是通过基类指针或引用去访问的,则应当声明为虚函数的,则应当声明为虚函数虚析构函数虚析构函数析构函数的作用:析构函数的作用: 在对象撤销之前做必要的在对象撤销之前做必要的“清理现场清理现场”的工作当派生的工作当派生类的对象从内存中撤销时一般先调用派生类的析构函数,类的对象从内存中撤销时一般先调用派生类的析构函数,然后再调用基类的析构函数然后再调用基类的析构函数 如果用如果用new运算符建立了临时对象,若基类中有析构运算符建立了临时对象,若基类中有析构函数,并且定义了一个指向该基类的指针变量在程序用函数,并且定义了一个指向该基类的指针变量在程序用带指针参数的带指针参数的delete运算符撤销对象时,会出现运算符撤销对象时,会出现: 系统会只系统会只执行基类的析构函数,而不执行派生类的析构函数。 执行基类的析构函数,而不执行派生类的析构函数解决办法:解决办法: 把析构函数定义为虚函数,实现撤消对象时的多态性把析构函数定义为虚函数,实现撤消对象时的多态性虚虚析构函数声明:析构函数声明: virtual ~~类名类名( );#include 可使所有派生类的把基类的析构函数声明为虚函数可使所有派生类的析构函数自动成为虚函数(即使函数名不同)若程序中析构函数自动成为虚函数(即使函数名不同)若程序中显式地用了显式地用了delete运算符准备删除一个对象,而运算符准备删除一个对象,而delete运算运算符的操作对象用了指向派生类对象的基类指针,系统会调符的操作对象用了指向派生类对象的基类指针,系统会调用相应类的析构函数用相应类的析构函数1. 1. 虚函数是动态关联的基础虚函数是动态关联的基础2. 2. 虚函数是非静态的成员函数虚函数是非静态的成员函数 3. 3. virtual 只用来说明类声明中的原型,不能用在函数实只用来说明类声明中的原型,不能用在函数实 现时4. 4. 具有继承性,基类中声明了虚函数,派生类中无论是具有继承性,基类中声明了虚函数,派生类中无论是 否说明,同原型函数都自动为虚函数否说明,同原型函数都自动为虚函数5. 5. 不是重载声明而是覆盖不是重载声明而是覆盖6. 6. 通过基类指针或引用,执行时会根据通过基类指针或引用,执行时会根据指针指向的对象指针指向的对象的类的类,决定调用哪个函数。 决定调用哪个函数【【例例8.5_1】】把把【【例例8.1】】析构函数改造为虚函数析构函数改造为虚函数 一般为了将类设计成通用的,必须把析构函数定义为虚函数一般为了将类设计成通用的,必须把析构函数定义为虚函数在动态分配内存时所有在动态分配内存时所有C++的标准库函数都采用这种格式的标准库函数都采用这种格式class Person{ // //数据成员略数据成员略public: virtual ~Person(); // //其他成员函数略其他成员函数略 }; 在主函数中添加以下内容:在主函数中添加以下内容: Person *per4; Student *stu4=new Student; *stu4=stu1; //把把stu1的数据拷入的数据拷入*stu4 stu4->PrintStudentInfo(); per4=stu4; delete per4; // //基类指针撤销派生类对象,必须显式撤销基类指针撤销派生类对象,必须显式撤销 例:例: PointArea()Show()double x,y; CircleArea()Show()doubl radius;class Point{public: ……. double Area(){return 0;} …..};class Circle :public Point{public: …… double Area(){ return PI*radius*radius; } …… };基类函数为空或可以不要,在实现部分仍然要写出函数体,纯虚函数可以解决--接口基类函数为空或可以不要,在实现部分仍然要写出函数体,纯虚函数可以解决--接口8.7.2 8.7.2 纯虚函数纯虚函数1. 纯虚函数不同于空函数,纯虚函数不能调用。 纯虚函数不同于空函数,纯虚函数不能调用2.派生类中必须重新定义纯虚函数的函数体派生类中必须重新定义纯虚函数的函数体纯虚函数纯虚函数((pure virtual function)当基类中某虚函数无法具)当基类中某虚函数无法具体实现,可定义为纯虚函数,具体实现依赖于派生类体实现,可定义为纯虚函数,具体实现依赖于派生类定义格式为:定义格式为:定义纯虚函数要注意:定义纯虚函数要注意: virtual 返回类型返回类型 函数名(参数表)函数名(参数表)=0;;含有纯虚函数的基类不能用来定义对象,称为含有纯虚函数的基类不能用来定义对象,称为抽象类抽象类抽象类的意义在于定义框架的意义在于定义框架注:注:①①纯虚函数没有函数体;纯虚函数没有函数体;②②最后面的最后面的“=0=0”并不表示函数返回值为并不表示函数返回值为0 0,它只起形式,它只起形式 上的作用,告诉编译系统上的作用,告诉编译系统“这是纯虚函数这是纯虚函数”; ; ③③这是一个声明语句,最后应有分号这是一个声明语句,最后应有分号 含有纯虚函数的基类是不能用来定义对象的纯虚含有纯虚函数的基类是不能用来定义对象的。 纯虚函数没有实现部分,不能产生对象,含有纯虚函数的类函数没有实现部分,不能产生对象,含有纯虚函数的类是抽象类是抽象类抽象类抽象类带有纯虚函数的类称为抽象类带有纯虚函数的类称为抽象类: class 类名类名 { virtual 类型类型 函数名函数名(参数表参数表)=0; //纯虚函纯虚函数数 ... }作用:作用:1. 1.抽象类为抽象和设计的目的而建立,将有关的数据和抽象类为抽象和设计的目的而建立,将有关的数据和 行为组织在一个继承层次结构中,保证派生类具有要行为组织在一个继承层次结构中,保证派生类具有要 求的行为求的行为2.2.对于暂时无法实现的函数,可以声明为纯虚函数,留对于暂时无法实现的函数,可以声明为纯虚函数,留 给派生类去实现给派生类去实现注意注意l抽象类只能作为抽象类只能作为基类基类来使用l不能声明抽象类的对象不能声明抽象类的对象l构造函数不能是虚函数,析构函数可以是虚函数构造函数不能是虚函数,析构函数可以是虚函数#include 象基类2) (2) 派生类如果没有实现基类中的全部派生类如果没有实现基类中的全部纯虚函数,则该派纯虚函数,则该派 生类仍然是抽象类;生类仍然是抽象类;(3) (3) 在类的层次结构中,顶层或最上面的几层可以是抽象在类的层次结构中,顶层或最上面的几层可以是抽象 基类抽象基类体现了本类族中各类的共性,把各类基类抽象基类体现了本类族中各类的共性,把各类 中共有的成员函数集中在抽象基类中声明中共有的成员函数集中在抽象基类中声明4) (4) 抽象基类是本类族的公共接口抽象基类是本类族的公共接口5) (5) 区别静态关联和动态关联区别静态关联和动态关联 通过对象调用虚函数--静态关联通过对象调用虚函数--静态关联 通过基类指针调用虚函数--动态关联通过基类指针调用虚函数--动态关联(6) (6) 如果在基类声明了虚函数,则在派生类中凡是与该函如果在基类声明了虚函数,则在派生类中凡是与该函 数有相同的函数名、函数类型、参数个数和类型的函数有相同的函数名、函数类型、参数个数和类型的函 数,均为虚函数数,均为虚函数( (不论在派生类中是否用不论在派生类中是否用virtualvirtual声明声明) )。 把类的声明与类的使用分离对于设计类库的软件开把类的声明与类的使用分离对于设计类库的软件开发商来说尤为重要开发商设计了各种各样的类,但不向发商来说尤为重要开发商设计了各种各样的类,但不向用户提供源代码,用户可以不知道类是怎样声明的,但是用户提供源代码,用户可以不知道类是怎样声明的,但是可以使用这些类来派生出自己的类可以使用这些类来派生出自己的类7) (7) 使用虚函数提高了程序的可扩充性使用虚函数提高了程序的可扩充性class Person{ int MarkAchieve; // //业绩分业绩分 char Name[20];public: Person(char *name){ strcpy(Name,name); MarkAchieve=0; } void SetMark(int mark){ MarkAchieve=mark; } virtual void CalMark()=0;void Print(){ cout< 学校对在册人员进行奖励,依据是业绩分 业绩分的计算方法只能对具体人员进行,各类人员算法不同,业绩分的计算方法只能对具体人员进行,各类人员算法不同,所以将在册人员类定义为抽象类,业绩计算方法为纯虚函数所以将在册人员类定义为抽象类,业绩计算方法为纯虚函数class Student:public Person{ int credit,grade; // //学历和成绩学历和成绩public: Student(char *name,int cred,int grad):Person(name){ credit=cred; grade=grad; } void CalMark(){ SetMark(credit*grade); } };class Teacher:public Person{ int classhour,studnum; // //授课学时和学生人数授课学时和学生人数public: Teacher(char *name,int ch,int sn):Person(name){ classhour=ch; studnum=sn; }void CalMark(){ int K=(studnum+15)/30; // //工作量系数工作量系数,30,30人一班人一班,15,15人以下不开课人以下不开课 switch(K){ case 1: SetMark(classhour*studnum);break; case 2: SetMark(classhour*(30+(studnum-30)*8/10));break; case 3: SetMark(classhour*(30+24+(studnum-60)*6/10));break; case 4: SetMark(classhour*(30+24+18+(studnum-90)*4/10)); break; case 5: SetMark(classhour*(30+24+12+(studnum-120)*2/10)); break; default:SetMark(classhour*(30+24+12+6+(studnum-150)*1/10)); } } };int main(){Person *pp;Student s1("张成张成",20,80);Teacher t1("范英明范英明",64,125),t2("李凯李凯",80,85);pp=&s1;pp->CalMark();pp->Print();pp=&t1;pp->CalMark();pp->Print();pp=&t2;pp->CalMark();pp->Print();return 0;}class Simpson{ double Intevalue,a,b; //Intevalue积分值,积分值,a积分下限,积分下限,b积分上限积分上限public: virtual double fun(double x)=0; // //被积函数声明为纯虚函数被积函数声明为纯虚函数 Simpson(double ra=0,double rb=0){a=ra;b=rb;Intevalue=0;} void Integrate(){ double dx; int i; dx=(b-a)/2000; Intevalue=fun(a)+fun(b); for(i=1;i<2000;i+=2) Intevalue+=4*fun(a+dx*i); for(i=2;i<2000;i+=2) Intevalue+=2*fun(a+dx*i); Intevalue*=dx/3; }【【例例8.9】】用虚函数来实现辛普生法求函数的定积分。 用虚函数来实现辛普生法求函数的定积分P210 例例6.11)) void Print(){ cout<<"积分值积分值="<












