210 likes | 339 Views
Unit 7— 第八章 继承与多态. 8.1 继 承与派生的概念 . 8.4 虚 基类 (选读) . 8.5 派生类应用讨论 . 8.2 派生类的构造函数与析构函数 . 8.3 多重继承与派生类成员标识 ( 选读 ). 8.6 多态性与虚函数 . 8.2 派生类的构造函数与析构函数. 派 生类构造函数的定义:
E N D
Unit 7—第八章 继承与多态 8.1 继承与派生的概念 8.4 虚基类 (选读) 8.5 派生类应用讨论 8.2 派生类的构造函数与析构函数 8.3 多重继承与派生类成员标识(选读) 8.6 多态性与虚函数
8.2 派生类的构造函数与析构函数 派生类构造函数的定义: 派生类名::派生类名(参数总表):基类名1(参数名表1)《,基类名2(参数名表2),……,基类名n(参数名表n)》,《成员对象名1(成员对象参数名表1),……,成员对象名m(成员对象参数名表m)》 { …… //派生类新增或更新成员的初始化; }; 所列成员对象名均为新增的; 指针型成员对象如何处理? 注意: (1)构造函数的声明中,冒号及冒号以后部分必须略去。 (2)基类的构造函数尽管未被继承,但会被派生类构造函数所调用。这里的基类名仅指直接基类,写了更底层基类,编译报错。 (3)参数总表中参数需有类型说明,而各参数名表中参数则无。
8.2 派生类的构造函数与析构函数 派生类构造函数执行过程: Step 1. 调用基类构造函数,按它们在派生类定义中的先后,顺序调用; Step 2.调用成员对象的构造函数,按它们在类定义中声明的先后,顺序调用; Step 3. 派生类的构造函数体中的操作。 注意: (1)派生类构造函数中,只要不打算调用基类无参默认构造函数,都要显式给出基类名和参数表。 (2)若基类没有定义构造函数,则派生类也可不定义,全部采用系统给定的默认构造函数。 (3)若基类定义了带形参表的构造函数时,派生类就应当定义构造函数。
8.2派生类的构造函数与析构函数 析构函数: 1.功能:派生类析构函数的功能依然用于善后。 (1)只需在函数体内把派生类新增的一般成员处理好; (2)新增成员对象和基类的善后,由系统调用成员对象和基类的析构函数来完成。 2. 执行:析构函数各部分执行次序与构造函数相反 Step1. 对派生类新增一般成员善后; Step2. 对新增成员对象析构; Step3. 对基类对象析构。
【H6_6】子女随父姓 • 基类 class father{ protected: string surname;//姓 string firstname;//名 int age; public: father(string & surn,string & firn,int a){ cout<<"father构造函数调用"<<endl; surname=surn; firstname=firn; age=a;}
【H6_6】子女随父姓 • 基类 father(){cout<<"father默认构造函数调用"<<endl;surname="";firstname="";} ~father(){cout<<"father析构函数调用"<<endl; } string & getsurname(){return surname;}//取得姓名 void show(){cout<<surname<<firstname<<" 年龄:"<<age;} };
【H6_6】子女随父姓 借助对象(外部)间接访问保护数据。 可否内部直接访问基类的保护数据?怎么做? • 公有派生子女类 class child:publicfather{ private: father myfather;//成员对象 public: child(){cout<<"child构造函数调用"<<endl;} child(father& fa,string & na,int a):father(), myfather(fa){ cout<<"child构造函数调用"<<endl; surname=fa.getsurname(); firstname=na; age=a; }
【H6_6】子女随父姓 • 公有派生子女类 ~child(){ cout<<"child析构函数调用"<<endl;} void show(){ cout<<"姓名:"; cout<<surname<< firstname<<" 年龄:"<<age; cout<<endl; cout<<"父亲:"; myfather.show(); cout<<endl; } };
【H6_6】子女随父姓 • 测试 int main(){ string fasurn1("欧阳"),fafirn1("东海"); string chfirn1("智超"); father fa1(fasurn1,fafirn1,50); child ch1(fa1,chfirn1,23); cout<<"子女信息结果:"<<endl; ch1.show(); return 0; }
【H6_6】子女随父姓 建立fa1 派生类调用基类默认构造函数 • 测试结果 father构造函数调用 father默认构造函数调用 child构造函数调用 子女信息结果: 姓名:欧阳智超 年龄:23 父亲:欧阳东海 年龄:50 child析构函数调用 father析构函数调用 father析构函数调用 father析构函数调用 成员对象myfather构造,调用了系统默认的复制构造函数,未有显示 派生类构造函数,建立ch1 析构派生类对象ch1 析构myfather成员对象 调用基类的析构函数 析构fa1
8.2 派生类的构造函数与析构函数 注意: (1)例中string类字符串用作father类的成员对象(聚合),字符数组的动态内存分配和释放封中在string的构造和析构函数中,使用方便。而采用动态建立C风格字符串,则要自己解决深复制问题。 (2)提倡完善的类对象封装,不仅封装数据和对数据的操作,而且封装资源的动态分配与释放,形成一个完备的子系统,如string类字符串。 (3)聚合(与组合)是一种完善的封装,因为借助成员对象将资源的动态分配与释放封装其内,大大简化了层次化的类派生体系中资源的动态分配与释放的操作,不必再考虑复杂的多层深复制。
在册人员 教职工(单继承) 学生(单继承) 兼职教师(单继承) 教师(单继承) 工人(单继承) 行政人员(单继承) 研究生(单继承) 行政人员兼教师(多重继承) 研究生助教 (多重继承) 在职研究生 (多重继承) 8.3 多重继承与派生类成员标识(选读) 多重继承与类层次体系实例: 解决手段: 采用作用域分辨符“::” 基类名::成员名;//数据成员 基类名::成员名(参数表); //函数成员 歧义性问题: 由于继承,基类及其派生类均具有编号”(No) 这一成员,标识符相同,那么该如何区分和访问呢? 图8.3 大学在册人员继承关系
假定派生全部为公有派生,并且No全为保护成员,EGStudent类内部访问各No标识:假定派生全部为公有派生,并且No全为保护成员,EGStudent类内部访问各No标识: egstd1.No //在职学号 egstd1.GStudent::No //研究生号 egstd1.GStudent.Student::No //学生号 egstd1.GStudent.Student.Person::No //身份证号 egstd1.Employee::No //工作证号 egstd1.Employee.Person::No//身份证号 no=egstd1.Employee.Person::GetNo(); 假定派生全部为公有派生,并且No全为公有成员,EGStudent类对象egstd1(外部)访问各No标识: egstd1.No //在职学号 egstd1.GStudent::No //研究生号 egstd1.GStudent.Student::No //学生号 egstd1.GStudent.Student.Person::No //身份证号 egstd1.Employee::No //工作证号 egstd1.Employee.Person::No//身份证号 no=egstd1.Employee.Person::GetNo(); 不能连续使用多个“::” 每个继承路线各数据分配了不同的内存空间,是不同的变量,即便它们逻辑上是一回事(如身份证)。 图8.4 在职研究生派生类关系
8.4 虚基类(选读) 图8.4中的两个身份证号显然是不合理的。对此,可以把Person这个共同基类设置为虚基类,这样从不同路径继承来的Person类的同名数据成员(如身份证号)在内存中就是同一个数据。 虚基类(virtual base class)定义: class派生类名:virtual 访问限定符 基类类名{...}; 或者, class派生类名:访问限定符virtual 基类类名{...};
虚拟继承的构造函数: 8.4 虚基类(选读) 派生类名::派生类名(参数总表):基类名1(参数名表1)《,基类名2(参数名表2),……,基类名n(参数名表n)》,《成员对象名1(成员对象参数名表1),……,成员对象名m(成员对象参数名表m)》,底层虚基类名1(参数名表1)《,……, 底层虚基类名r(参数名表r)》{ ……//派生类新增或替换的成员的初始化 }; 注意:在多层虚拟继承构造函数中,不仅要列出直接虚基类,还要列出间接的底层虚基类。
8.5 派生类应用讨论 一、派生类与基类 赋值兼容规则:任何需要基类对象的地方都可以用公有派生类的对象来代替。包括以下情况: 1.派生类对象可以赋值给基类对象:这时是把派生类对象中由基类继承来的成员赋值给基类对象。反过来不行,为什么? 因为派生类对象的新成员将无值可赋。 psl链表类指针对象, Istack链栈对象 psl=&istack; 2. 派生类对象取地址后赋给基类指针对象:只能通过这个指针访问派生类中由基类继承来的成员,不能访问派生类中的新成员。同样也不能反过来做。 psl=istack; 3. 派生类对象初始化基类的引用:引用是别名,但这个别名只包含派生类对象中的由基类继承来的成员。
Person和Student复制构造函数: 例8.5 赋值兼容规则与自定义复制构造函数 Person::Person(Person &ps) { IdPerson = ps.IdPerson; Name = ps.Name; Sex = ps.Sex; Birthday = ps.Birthday; HomeAddress = ps.HomeAddress; } Student::Student(Student &Std) : Person(Std) { //按赋值兼容规则Std可为Person实参 NoStudent=Std.NoStudent; for (int i = 0;i<30;i++ ) { cs[i].coursename = Std.cs[i].coursename; cs[i].grade = Std.cs[i].grade; } }
例8.5 赋值兼容规则与自定义复制构造函数 Person和Student复制赋值操作符: Person & Person::operator=(Person &ps){ IdPerson=ps.IdPerson; Name=ps.Name; Sex=ps.Sex; Birthday=ps.Birthday; HomeAddress=ps.HomeAddress; return *this; } Student & Student::operator = (Student &Std){ this->Person::operator = (Std); //注意标准格式 NoStudent=Std.NoStudent; for ( int i=0; i<30; i++){ cs[i].coursename=Std.cs[i].coursename; cs[i].grade=Std.cs[i].grade; } return *this; } 具体检验,参见课本!
8.5 派生类应用讨论 二、继承与聚合 派生类通过继承可以使用基类的成员;要获得类似的效果,也可以把一个类的对象作为新类的对象成员,即聚合。 概念 B类中包含A类指针对象,称为聚合,而B类中包含A类对象,称为组合,可简单地统称为聚合。 聚合与继承比较 (1)派生类对基类只能直接继承一次,否则,即便使用域分辨符成员名也无法区分;而聚合类中安排多个成员对象也不出现该困惑。 (2)成员对象体现了封装更深层的内涵:在派生类和它的基类中最好不要有内存的动态分配;动态分配和释放应该封装在成员对象(构造和析构)中,该成员对象中也提供深复制。如同类string,程序员可以放心使用。 (3)继承结合虚函数可以实现运行时多态性,聚合做不到。 聚合类并不能直接访问被聚合类的保护成员,派生则可直接访问基类保护成员!
多态性: 多态性是面向对象程序设计最显著、最核心的技术之一,可以实现同名操作(函数)的多义性(不同功能),提高代码复用性。C++中有两种多态性 : 8.6 多态性与虚函数 通过函数和运算符的重载实现,函数同名,参数表不同。 编译时多态性 (1)派生体系中成员函数名和参数表都相同,使得编译时无法确定该调用哪个函数,必须在程序执行过程中动态确定; (2)通过类继承关系和虚函数来实现。 运行时多态性
8.6 多态性与虚函数 8.6.1 虚函数的定义 8.6.2 纯虚函数 8.6.3 继承与多态的应用— 单链表派生类(选读) 8.6.4 动态绑定(选读)