第十二讲 继承 与 派生
继承/派生 —— 类的继承/派生 什么是类的继承/派生 怎么定义派生类 如何继承父类的成员 派生类的构造函数和析构函数 类型兼容规则:派生类对象/父类对象 多重继承时重复继承问题 — 虚父类
什么是继承/派生 什么是继承/派生: —— 在已有的类的基础上定义新的类 类的派生 为什么继承/派生: —— 在已有的类的基础上定义新的类 类的派生 为什么继承/派生: —— 继承可以使得派生类具有父类的各种属性和功能, 而不需要再次编写相同的代码。 派生类在继承父类的同时,还可以通过重新定义某些属性或改写某些方法,来更新父类的原有属性和功能,或增加新的属性和功能。
什么是继承/派生(续) 在已有类的基础上产生新类的过程就是类的派生 原有类称为父类或基类,新类称为派生类或子类 类的继承:派生类继承了父类的特性(数据和函数) 派生类可以加入新的特性 派生类也可以作为父类,派生新的子类 继承层次结构 火车 大卡车 例: 交通工具 汽车 小轿车 飞机 面包车 继承和派生提高了代码的可重用性,有利于软件开发。
怎么定义派生类 一个派生类可以有多个父类(多重继承) 单继承:一个派生类只有一个父类 一个父类可以派生出多个子类 类族 class 派生类名: 继承方式 父类名1, 继承方式 父类名2, ... { 派生类成员声明; }; 一个派生类可以有多个父类(多重继承) 单继承:一个派生类只有一个父类 一个父类可以派生出多个子类 类族 继承是可传递的:从父类继承的特性可以传递给新的子类 继承方式:规定了如何访问从父类继承的成员 继承方式有三种:public、protected、private 派生类成员:从父类继承的成员+新增加的成员
类的派生过程 派生过程:吸收父类成员,改造父类成员,添加新成员。 吸收父类成员 派生类包含父类中除构造和析构函数外的所有非静态成员 - 父类成员的访问控制(通过继承方式实现) - 对父类成员的覆盖或隐藏(如同名隐藏,即新成员与父类成员同名(若是函数,则形参也要一样),则只能访问新成员) 添加新成员 根据实际需要,添加新的数据成员或函数成员 构造函数、析构函数、静态成员不能被继承!
派生类成员的访问控制 访问控制:能否访问/怎样访问从父类继承得来的成员。 这里主要强调派生类中新增成员和派生类外部函数访问派生类中从父类继承的成员 继承方式不同,访问控制不同 公有继承(public) - 父类的公有和保护成员的访问属性保持不变 - 父类的私有成员不可直接访问 从父类继承的成员函数对父类成员的访问不受影响!
例:公有继承 class Point { public: void initPoint(float x=0, float y=0); void move(float offx, float offy); float getx() const {return x;} float gety() const {return y;} private: float x, y; } class Rectangle: public Point { public: void initRect(float x,float y,float w,float h) { initPoint(x,y); this->w=w; this->h=h; } float geth() const {return h;} float getw() const {return w;} private: float h, w; // 新增私有成员 }
派生类成员的访问控制(续) 私有继承(private) - 父类的公有和保护成员都成为派生类的私有成员 - 父类的私有成员不可直接访问 私有继承后,父类成员(特别是公有函数)无法在以后的派生类中直接发挥作用,相当于终止了父类功能的继续派生。因此,私有继承较少使用。 保护继承(protected) - 父类的公有和保护成员都成为派生类的保护成员 - 父类的私有成员不可直接访问 与私有继承的区别: 父类成员(特别是公有函数)可以在以后的派生中作为保护成员继承下去。
访问控制小结 父类成员函数访问父类成员:正常访问 派生类成员函数访问派生类新增成员:正常访问 父类成员函数访问派生类新增成员:不能访问 派生类成员函数访问父类成员:继承方式+成员本身访问属性 非成员函数访问派生类所有成员:只能访问公有成员 派生类成员按访问属性可划分为下面四类: - 不可访问成员:父类的私有成员 - 私有成员:父类继承的部分成员 + 新增的私有成员 - 保护成员:父类继承的部分成员 + 新增的保护成员 - 公有成员:父类继承的部分成员 + 新增的公有成员 如果没有指定继承方式,则缺省为 private
派生类的构造函数 派生类对象的初始化: - 派生类的构造函数只负责新增数据成员的初始化 - 从父类继承的成员需通过调用父类的构造函数进行初始化 派生类对象的数据成员:父类成员 + 新增成员 初始化:父类成员初始化 + 新增成员初始化 数据成员:基本类型数据成员 + 其它类的对象 由于父类成员需要调用父类构造函数,因此在派生类构造函数的参数中,有一些参数是传递给父类的构造函数的 派生类名(总参数列表): 父类1(参数),...,父类n(参数), 成员对象1(参数), ..., 成员对象m(参数) { 新增数据成员的初始化(不包括继承的父类成员); }
派生类的构造函数(续) 派生类构造函数执行的一般次序: - 调用父类的构造函数,按被继承时声明的顺序执行 - 对派生类新增成员对象初始化,按它们在类中声明的顺序 - 执行派生类的构造函数体的内容 若父类使用缺省(即不带形参)构造函数,则可以省略 若成员对象使用缺省(即不带形参)构造函数来初始化,也可以省略 构造函数的总参数列表中的参数需要带数据类型(形参),其他不需要
派生类构造函数:例一 class B1 // 类B1,构造函数有参数 { public: B1(int i) { cout<<"constructing B1 "<<i<<endl; } }; class B2 // 类B2,构造函数有参数 B2(int j) { cout<<"constructing B2 "<<j<<endl; } class B3 // 类B3,构造函数无参数 B3() { cout<<"constructing B3 *"<<endl; }
派生类构造函数:例一(续) 屏幕输出结果: constructing B2 2 constructing B1 1 ex12_Inheritance01.cpp class C: public B2, public B1, public B3 // 派生新类C,注意父类名的顺序 { public: //派生类的公有成员 C(int a, int b, int c, int d) : B1(a), memberB2(d), memberB1(c), B2(b) { x=a; } // 注意父类名的个数与顺序 // 注意成员对象名的个数与顺序 private: // 派生类的私有对象成员 B1 memberB1; B2 memberB2; B3 memberB3; int x; }; int main() { C obj(1,2,3,4); } 屏幕输出结果: constructing B2 2 constructing B1 1 constructing B3 * constructing B1 3 constructing B2 4
派生类构造函数:例二 class Person // 父类 { public: Person(string & str, int age ) { name=str; this->age = age; } void show() { cout << "Name: " << name << endl; cout << "Age: " << age << endl; } private: string name; // 姓名 int age; // 年龄 };
派生类构造函数:例二(续) class Student : public Person // 派生类 { public: Student(string & str, int age, int stuid) : Person(str, age) // 父类数据成员初始化 { this->stuid = stuid; } void showStu() this->show(); // 不能直接访问 name 和 age cout << "Stuid: " << stuid << endl; } private: int stuid; // 学号 };
派生类构造函数:例二(续) 思考:如果 Student 中的成员函数 showStu 也取名为 show, 该如何处理? ex12_Inheritance02.cpp int main() { string str="Xi Jiajia"; Student stu1(str, 18, 20150108); stu1.showStu(); return 0; } 思考:如果 Student 中的成员函数 showStu 也取名为 show, 该如何处理?
派生类成员的标识与访问 派生类的成员:父类的成员 + 新增成员 派生类成员的标识问题:如何处理成员同名问题? 作用域分辨符 :: —— 限定要访问的成员所在的类 类名::成员名 // 数据成员 类名::成员名(参数) // 函数成员
屏蔽规则 如果存在两个或多个具有包含关系的作用域,外层作用域声明的标识符在内层作用域可见,但如果在内层作用域声明了同名标识符,则外层标识符在内层不可见。 父类是外层,派生类是内层 若在派生类中声明了与父类同名的新函数,即使函数参数表不同, 从父类继承的同名函数的所有重载形式都会被屏蔽 如何访问被屏蔽的成员:类名+作用域分辨符 若派生类有多个父类,且这些父类中有同名标识符,则必须使用 作用域分辨符来指定使用哪个父类的标识符! 通过作用域分辨符就明确标识了派生类中从父类继承的成员,从而解决了成员同名问题。
派生类的复制构造函数 派生类复制构造函数的作用机制: 调用父类的复制构造函数完成父类部分的复制,然后再复制派生类部分 class C: public B { C(const C &v) : B(v); ... ... }; C::C(const C &v) : B(v) // 派生类复制构造函数 } 在定义派生类的复制构造函数时,需要为父类的复制构造函数传递参数
派生类析构函数 派生类的析构函数只负责新增非对象成员的清理工作 父类成员的清理工作由父类的析构函数负责 新增对象成员的清理工作由对象所在类的析构函数负责 析构函数的执行顺序(与构造函数相反): - 执行派生类析构函数 - 执行派生类对象成员的析构函数 - 执行父类的析构函数
类型兼容规则/多态 在需要父类对象出现的地方,可以使用派生类(以公有方式继承)的对象来替代。 类型兼容规则中的替代包括以下情况: 通俗解释:公有派生类具备了父类的所有功能,凡是父类能解决的问题,公有派生类都可以解决。 类型兼容规则中的替代包括以下情况: - 派生类的对象可以隐式转化为父类对象 - 派生类的对象可以初始化父类的引用 - 派生类的指针可以隐式转化为父类的指针 用派生类对象替代父类对象后,只能使用从父类继承的成员,即派生类只能发挥父类的作用。
虚父类 为什么虚父类: 在多重继承时,如果派生类的部分或全部父类是从另一个共同父类派生而来,则在最终的派生类中会保留该间接共同父类数据成员的多份同名成员。这时不仅会存在标识符同名问题,还会占用额外的存储空间,同时也增加了访问这些成员时的困难,且容易出错。事实上,在很多情况下,我们只需要一个这样的成员副本(特别是函数成员) 虚父类: 当某个类的部分或全部父类是从另一个共同父类派生而来时,可以将这个共同父类设置成虚父类,这时从不同路径继承来的同名数据成员在内存中只存放一个副本,同一个函数名也只有一个映射。
虚父类的声明 class 派生类名: virtual 继承方式 父类名 { ... ... }; class A { ... }; class B : virtual public A { ... }; class C : virtual public A { ... }; class D : public B, public C { ... };
虚父类及其派生类的构造函数 在直接或间接继承虚父类的所有派生类中,都必须在构造函数的初始化列表中列出对虚父类的初始化。 class A { public: A(int x); ... }; class B : virtual public A B(int x) : A(x); class C : public B C(int x) : A(x), B(x);
虚父类举例 class Person // 公共父类 Person { public: Person(string str, int a) { name=str; age=a;} protected: // 保护成员 string name; int age; }; class Teacher: virtual public Person // 声明 Person 为虚父类 { public: Teacher(string str, int a, string tit):Person(str,a) { title=tit; } protected: string title; // 职称 };
虚父类举例(续) class Student: virtual public Person // 声明 Person 为虚父类 { Student(string str, int a, float sco) // 构造函数 : Person(str,a), score(sco){ } // 初始化表 protected: float score; // 成绩 };
虚父类举例(续) class Graduate: public Teacher, public Student { public: ex12_Inheritance03.cpp class Graduate: public Teacher, public Student { public: Graduate(string str, int a, string tit, float sco, float w) : Person(str,a), Teacher(str,a,tit), Student(str,a,sco), wage(w){} // 初始化表 void show() cout << "name:" << name << endl; cout << "age:" << age << endl; cout << "score:" << score << endl; cout << "title:" << title << endl; cout << "wage:" << wage << endl; } private: float wage; // 工资 };
虚父类几点注记 虚父类并不是在声明父类时声明的,而是在声明派生类时,指定继承方 式时声明的。 NOTE 虚父类并不是在声明父类时声明的,而是在声明派生类时,指定继承方 式时声明的。 一个父类可以在生成某个派生类时作为虚父类,而在生成另一个派生类 时不作为虚父类。 为了保证虚父类成员在派生类中只继承一次,应该在该父类的所有直接 派生类中声明其为虚父类。否则仍然可能会出现对该父类的多重继承。
上机作业 1) 设计一个名为 Point 的类,表示平面坐标下的一个点,这个类包括: 两个 double 型数据成员:x, y,分别表示横坐标和纵坐标 一个不带形参的构造函数,用于创建原点:(0,0) 一个带形参的构造函数:Point(double x, double y) 成员函数 getx(),返回横坐标 成员函数 gety(),返回纵坐标 成员函数 dist(const Point& p),返回当前点与给定点的距离 实现这个类,并在主函数中测试这个类:创建点 A(0,0) 和 B(4,5.6),并输出它们之间的距离。程序取名为 hw12_01.cpp 2) 在 Point 类的基础上定义派生类 Point3D,表示三维空间的一个点,包括: 一个 double 型数据成员:z,表示 z-坐标 一个不带形参的构造函数,用于创建原点:(0,0,0) 一个带形参的构造函数:Point3D(double x, double y, double z) 成员函数 getz(),返回 z-坐标 成员函数 dist(const Point3D& p),返回当前点与给定点的距离 实现这个类,并在主函数中测试这个类:创建点 A(0,0,0) 和 B(4,5.6,7.8),并输出它们之间的距离。程序取名为 hw12_02.cpp
上机作业 3) 设计一个名为 Clock 的类,这个类包括: 三个保护型的 int 数据成员:hour, minute, second 一个带形参的构造函数:Clock(int hour, int minute, int second) 成员函数 show(),输出当前时间,如 09:55:08 设计一个名为 Weekday 的类,这个类包括: 一个保护型的 int 数据成员:date,代表星期,0 为周日,1~6 分别代表周一至周六 一个不带形参的构造函数,用当前日期设置 date 的值,如今天是周二,则 date=2 一个带形参的构造函数:Weekday(int date) 设计一个名为 Mytime 的派生类,以公有方式继承 Clock 和 Weekday,这个类包括: 一个不带形参的构造函数,用当前日期和当前时间设置数据成员的值 一个带形参的构造函数:Mytime(int date, int hour, int minute, int second) 成员函数 show(),按下面的额要求输出时间:“周二下午15点48分06秒”的输出为“工作日,15:48:06”,若是“周六下午15点48分06秒”,则输出为“周末,15:48:06” 实现上面三个类,并在主函数中测试:创建时间 T1,用当前日期和当前时间初始化数据成员,并在屏幕上输出,然后将其修改为 周四的9点55分8秒,并在屏幕上输出。 程序取名为 hw12_03.cpp