1 / 55

继承与类的派生

继承与类的派生. 多继承 多继承的概念 所谓 多继承 就是一个 新类 是从 多个基类 中 派生 而成 的。例如,在一个面向对象的图形用户界面中,为用 户界面提供的 窗口 、 滚动条 、 对话框 和各种 操作按钮 都是通过类对象来实现的。如果希望在 既有类 的基础 上定义一个 新类具有两个以上既有类的全部属性和操 作,就可以通过多继承的方法完成 。如,可以由 窗口 类 和 滚动条类 共同派生出一个 可滚动的窗口新类 。. 在有些情况下,可以使用 类的组合关系 ,即将一些 既有类对象定义为新类的成员来实现与多继承派生

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. 继承与类的派生

  2. 多继承 多继承的概念 所谓多继承就是一个新类是从多个基类中派生而成 的。例如,在一个面向对象的图形用户界面中,为用 户界面提供的窗口、滚动条、对话框和各种操作按钮 都是通过类对象来实现的。如果希望在既有类的基础 上定义一个新类具有两个以上既有类的全部属性和操 作,就可以通过多继承的方法完成。如,可以由窗口 类和滚动条类共同派生出一个可滚动的窗口新类。

  3. 在有些情况下,可以使用类的组合关系,即将一些在有些情况下,可以使用类的组合关系,即将一些 既有类对象定义为新类的成员来实现与多继承派生 的新类相同的功能。例如,同样是定义可滚动的窗 口类,可以以窗口类为基类单继承派生,并将滚动 条类对象作为新类的新增成员。在新类中窗口类的 属性和行为被继承,而滚动条的属性和行为并没有 被继承,对滚动条对象成员的使用是通过新类的内 部消息实现的。

  4. 多继承的定义 多重继承派生类定义的一般形式: class 派生类名:继承方式基类名1, … 继承方式基类名n { 派生类新增的数据成员和成员函数}; 注意,在每个基类名之前必须有继承方式,如果缺省 继承方式,则表示从该基类私有派生。例如: class c : public a,b{…}; // c 对 a 公有派生,对 b 私有派生 class c : a, public b{…}; // c 对 a 私有派生,对 b 公有派生 class c : public a, public b{… }; // c 对 a,b 均为公有派生

  5. 多重继承。 例 # include< iostream > #include< cstring > using namespace std; class Point{ // 二维直角坐标点类 protected: float x,y; public: Point(float x,float y) { this - >x=x; this - >y=y; } void Setxy (float x,float y) { this - >x=x; this - >y=y; } void Getxy (float &x,float &y) { x=this - >x; y=this - >y; } void ShowP ( ){ cout <<'('<<x<<','<<y<<')'; } };

  6. class Color{ // 颜色类 protected: char c[10]; public: Color(char*c) { strcpy (this - >c,c); } void Setc (char*c) { strcpy (this - >c,c); } char* Getc (char *c) { strcpy (c,this - >c); return c; } };

  7. class Circle: public Point,protected Color {// 彩色圆类 protected: • 派生类 Circle 的构造函数调用基类 Point float r; 和基类 Color 的构造函数 public: Circle(float x,float y,float r,char*c) :Point(x,y),Color(c) { this - >r=r; } void Setr (float r){ this - >r=r; } float Getr ( ){ return r; } float Area( ){ return r*r*3.14159f; } void Show( ) { cout <<" 圆心 :"; ShowP (); cout <<", 半径 :"<< r<<", 颜色 :"<< c<<", 面积 :"<< Area()<<' \ n'; } }; int main(void) { Circle c(1,2,3,"green"); c.Show(); 程序运行结果: return 0; } 圆心 :(1,2), 半径 :3, 颜色 : green, 面积 :28.2743

  8. 初始化基类成员和对象成员 • 派生类的构造函数完成: – 初始化派生类中的基类成员:调用基类的构造函数 – 初始化派生类中新增成员数据:调用派生类的构造函数 • 派生类的构造函数的格式: … 其中, ClassName 是派生类名; B1 、 、 Bn 是基类构造函数名; … a 是形参表, a1 、 、 an 是实参表。 … ClassName :: ClassName (a) : B1(a1), , Bn (an) … { } • 初始化成员列表: 冒号后列举 基类成员 的构 造函数和 对象成员 的构造函数,项与项之间 函数体初始 用逗号分隔。 化派生类中 • 初始化成员列表用基类名调用基类的构造函 的其他成员 数,可省略基类成员的缺省构造函数 ( 即无参 数据 或所有参数都带缺省值的构造函数 ) 。

  9. 例 派生类的构造函数和析构函数。 # include< iostream > using namespace std; class B1{ protected: int x; public: B1( int x){ this - >x=x; cout <<" 基类 B1 的构造函数 ! \ n"; } ~B1( ){ cout <<" 基类 B1 的析构函数 ! \ n"; } }; class B2{ protected: int y; public: B2( int y){ this - >y=y; cout <<" 基类 B2 的构造函数 ! \ n"; } ~B2( ){ cout <<" 基类 B2 的析构函数 ! \ n"; } };

  10. 基类构造函数的调用顺序: § 与继承基类的顺序有关 § 与初始化成员列表中的顺 class D: public B1,public B2 { 序无关 protected: int z; public: D( int x, int y, int z): B1(x),B2(y) { this - >z=z; cout <<" 派生类 D 的构造函数 ! \ n"; } ~D(){ cout <<" 派生类 D 的析构函数 ! \ n"; } }; int main(void){ D d(1,2,3) ; return 0; } 程序运行结果: • 说明派生类的对象: 先调用各基类的 基类 B1 的构造函数 ! 构造函数,后执行派生类的构造函数。 基类 B2 的构造函数 ! 若某个基类仍是派生类,则这种调用 派生类 D 的构造函数 ! 基类构造函数的过程递归进行。 派生类 D 的析构函数 ! • 撤消派生类的对象: 析构函数的调用 基类 B2 的析构函数 ! 顺序正好与构造函数的顺序相反。 基类 B1 的析构函数 !

  11. 派生类含对象成员:其构造函数的初始化成员列表既要列举基 类成员的构造函数,又要列举对象成员的构造函数。 • 例 派生类中包含对象成员。 # include< iostream > using namespace std; class B1{ protected: int x; public: B1( int x){ this - >x=x; cout <<" 基类 B1 的构造函数 ! \ n"; } ~B1( ){ cout <<" 基类 B1 的析构函数 ! \ n"; } }; class B2{ protected: int y; public: B2( int y){ this - >y=y; cout <<" 基类 B2 的构造函数 ! \ n"; } ~B2( ){ cout <<" 基类 B2 的析构函数 ! \ n"; } };

  12. 对象成员的构造函数的调用 • 基类成员的初始化必须 顺序与对象成员的说明顺序 使用基类名 有关,而与其在初始化成员 class D: public B1,public B2 { 列表中的顺序无关。 int z; • 对象成员的初始化必须使用 B1 b1,b2 ; 对象名 public: D( int x, int y, int z): B1(x),B2(y) , b1(2),b2(x+y) { this - >z=z; cout <<" 派生类 D 的构造函数 ! \ n"; } ~D( ){ cout <<" 派生类 D 的析构函数 ! \ n"; } • 程序运行结果: }; • 从结果看: 在创建类 基类 B1 的构造函数 ! int main(void) D 的对象 d 时,先调用 基类 B2 的构造函数 ! { D d(1,2,3) ; 基类 B1 的构造函数 ! 基类的构造函数,再 return 0; 基类 B1 的构造函数 ! 调用对象成员的构造 派生类 D 的构造函数 ! } 函数,最后执行派生 派生类 D 的析构函数 ! 类的构造函数。 基类 B1 的析构函数 ! 基类 B1 的析构函数 ! • 问题: 请写出产生上述输出结果的基类 基类 B2 的析构函数 ! 成员名或对象成员名。 基类 B1 的析构函数 !

  13. 歧义、优先规则和赋值兼容规则歧义 • 歧义: 在多重继承的派生类中,使用不同基类中的同名 成员所出现的现象。 • 例 # include< iostream > using namespace std; class A{ protected: int x ; public: void Show( ) { cout <<"x="<<x<<' \ n'; } A( int a=0){ x=a; } };

  14. VC6 x • 用 编译时,对成员数据 有如下错误和警告: class B{ error C2385: 'C::x' is ambiguous protected: warning C4385: could be the 'x' in base 'A' of class 'C' int x ; warning C4385: or the 'x' in base 'B' of class 'C' public: void Show( ) { cout <<"x="<<x<<' \ n'; } B( int a=0){ x=a; } }; class C:public A,public B{ protected: • VC6 Show( ) 用 编译时,对成员函数 int y; 有如下错误和警告: public: error C2385: 'C::Show' is ambiguous warning C4385: could be the 'Show' in int & AccessX ( ){ return x ; } base 'A' of class 'C' int & AccessY ( ){ return y; } warning C4385: or the 'Show' in base }; 'B' of class 'C' int main(void){ C c; c. Show( ) ; return 0; }

  15. 解决歧义的方法: Ø 使各基类中的成员名各不相同。 Ø 将基类的成员数据的访问权限说明为 private , 并 在相应的基类中提供成员函数访问这些成员数据, 但并不实用。原因是,在实际编程时,通常将基 类成员数据的访问权限定义为 protected , 以保障 类的封装性和便于派生类访问。 Ø 用类名限定来指明所访问的成员。格式为: 类名 :: 成员名

  16. 例 用类名限定来指明所访问的成员。 # include< iostream > using namespace std; class A{ protected: int x; public: void Show( ){ cout <<"x="<<x<<' \ n'; } A( int a=0){ x=a; } }; class B{ protected: int x; public: void Show( ){ cout <<"x="<<x<<' \ n'; } B( int a=0){ x=a; } };

  17. class C:public A,public B{ protected: int y; public: int & AccessAX ( ){ return A:: x; } int & AccessBX ( ){ return B:: x; } int & AccessY ( ){ return y; } }; • 用类名限定来指明所访问的成员,避免歧义 int main(void) { C c; c. AccessAX ( )=1; c. AccessBX ( )=2; c. AccessY ( )=3; c. A:: Show( ); // 调用类 A 中的成员函数 c. B:: Show( ); // 调用类 B 中的成员函数 cout <<"y="<<c. AccessY ( )<<' \ n'; return 0; }

  18. 用基类成员还是对象成员? • 多个基类成员: class B{ • 问题: 因派生类 D 中含有两个 直接 protected: 继承 来的成员 x , 在使用类 D 的对 int x; 象访问基类的成员 x 时会 产生无法 … 避免的歧义 。 }; class D: public B,public B { … }; • C++ 规定: 任一基类在派生类中只能 直接 继承一次,以 避免上述无法避免的歧义。

  19. 解决方法: Ø 组合法: 在 D 类中,组合类 B 的两个对象: class D{ B b1,b2 ;// 若需组合更多,则可用类 B 的数组 … }; Ø 继承+组合法: class D: public B { // 直接继承基类 B 一次 B b ; // 组合类 B 的对象 … }; • 基类成员和对象成员, 在功能上是相同的,但在使用 上是有区别的 。 • 通过类的继承和组合,可用已有的类来定义新类,为 软件重用提供了良好机制。

  20. 优先规则 • 优先规则: 指派生类新增成员优先于基类中的同名成 员,并不产生歧义。若要使用基类的同名成员,应在 其前面加类名限定。 • 例 优先规则。 # include< iostream > using namespace std; class A{ protected: int x ; public: A( int x){ this - >x=x; } void Show( ) { cout <<" 类 A 的 x="<<x<<' \ n'; } };

  21. class C:public A{ • 按支配规则,此处访问的是派 protected: 生类 C 中的新增成员 。 int x ; public: C( int x, int y):A(y){ this - >x=x; } int GetX ( ){ return x ; } int GetAX ( ){ return A::x ; } void Show( ) { cout <<" 类 C 的 x="<<x<<' \ n'; } }; • 加类名限定,访问的是基类 A 中 int main(void) 的成员 。 { C c(1,2); cout <<" 类 C 的 x="<<c. GetX ( )<<' \ n'; cout <<" 类 A 的 x="<<c. GetAX ( )<<' \ n'; c. Show( ) ; c. A::Show( ) ; return 0; }

  22. 赋值兼容规则 • 赋值兼容规则: 规定派生类的对象与其基类的对象之 间互相赋值的规则。设: class B{ … // }; class D: public B{ … // }; D d; B b,* pb ;

  23. 则有以下赋值兼容规则: Ø 派生类的对象可赋给基类的对象,但反之不然。 b=d;// 允许。将 d 中从类 B 继承的部分赋给 b d=b;// 不允许 Ø 派生类对象的地址可赋给基类型的指针变量。 pb =&d; Ø 派生类对象可初始化基类型的引用。 B& rb =d; • 注意: 在后两种情况下,使用基类的指针或引用时, 只能访问从相应基类中继承来的成员,而不允许访问 其他基类的成员或在派生类中增加的成员。

  24. 多继承派生类编程实例 例在屏幕上显示一个带有字符串的圆。 问题分析 定义一个圆类 circles和字符串类 gmessage分别用于 在屏幕的指定位置按设定半径绘制圆和在屏幕的指 定位置显示特定的字符串。为了使所显示的圆和字 符串的位置紧密相关,采用多继承机制,从 circles 和 gmessage派生出新类 mcircle用于完成本例的最 终需求。另外为了更好地体现面向对象的设计思 想,再定义一个 point类,用于确定所要显示的圆和 字符串的位置。因此 circles和 gmessage都应该从 point派生。

  25. point point #x:int #y:int #x:int #y:int circles mcircle gmessage #radius:int -font:int -field:int -msg:char* +show() +show() +show() ⑴类图描述

  26. 虚基类 • 例 一个公共的基类在派生类中产生两个拷贝。 #include< iostream > using namespace std; class A{ protected: int x; public: A( int a=0){ x=a; } }; class B: public A { public: B( int a=0):A(a){ } void PB( ){ cout <<"x="<<x<<' \ n'; } };

  27. class C: public A { public: C( int a=0):A(a){ } void PC( ){ cout <<"x="<<x<<' \ n'; } }; class D: public B,public C { 程序运行结果: public: x=1 D( int a, int b):B(a),C(b){ } x=2 void Print(void){ PB( ); PC( ); } }; • 尽管一个基类在派生类中只能 直接 继承一次, 但并未限制其在派生类中 间接 继承的次数。 int main(void) Ø 派生类 D 包含两份基类 A 成员 { D d(1,2); Ø 两份基类 A 成员的成员数据 x 的值 d.Print( ); return 0; }

  28. 派生类包含两份基类成员,产生歧义。 • 例 # include< iostream > using namespace std; class A{ protected: int x; public: A( int a=0){ x=a; } }; class B:public A{ public: B( int a=0):A(a){ } }; class C:public A{ public: C( int a=0):A(a){ } };

  29. class D:public B,public C{ public: D( int a, int b):B(a),C(b){ } void Print(void) { cout << x <<' \ n'; cout << x <<' \ n'; } • 歧义: 此时编译器无法确定成员 x 是继承于 }; 类 B , 还是继承于类 C 。 • 消除: 可用类名限定来指明成员 x 源自类 B 或 int main(void) 类 C 。 如可用 B::x 代替 E 行中的 x , 用 C::x 代 { D d(1,2); 替 F 行中的 x 。 d.Print( ); • 问题思考: 若希望类 D 只含一 份基类 A 成员 , return 0; 则用本例实现不仅多占内存,且可能产生歧 } 义,难以保证数据一致。 如何解决?

  30. 虚基类: 在定义派生类时, 在继承的公共基类的类名 前加关键字 virtual , 使得公共基类在派生类中只有一 份拷贝。 • 虚基类的格式: class ClassName : virtual access ClassName1 … { }; 或 class ClassName :access virtual ClassName1 … { };

  31. 定义虚基类,使派生类中只有基类的一个拷贝。 例 # include< iostream > using namespace std; class A{ protected: int x; public: A( int a=0){ x=a; } //L1 }; class B: virtual public A{ public: B( int a):A(a){ } void PB( ){ cout <<"x="<<x<<' \ n'; } };

  32. class C:public virtual A{ public: C( int a):A(a){ } void PC(){ cout <<"x="<<x<<' \ n'; } }; class D:public B,public C{ 程序运行结果: public: x=0 D( int a, int b):B(a),C(b){ } //L2 x=0 void Print(void){ PB(); PC(); } }; • 派生类 D 的对象 d 中只有基类 A 的一个拷贝,如 图所示。当改变成员 x 的值时,由基类 B 和 C 中 int main(void) 的成员函数输出的 x 的值是相同的。 • { D d(1,2); x 的初值为何 为 0 ? 。 原因: 系统约定,在执行 d.Print( ); 类 B 和类 C 的构造函数时都不调用虚基类 A 的构 return 0; 造函数,而是在类 D 的构造函数中直接调用虚 } 基类 A 的缺省的构造函数 。

  33. 进一步讨论: 若将 L1 行改为 A( int a){ x=a; } //L1 “ 再用 VC6 编译时,将指出 L2 行有错: error C2512: ‘ ’ A::A : no appropriate default constructor ” available , 即指出类 A 没有合适的缺省的构造函数可 用。此时可将 L2 行改为: D( int a, int b):B(a),C(a), A(1) //L2 即在类 D 的构造函数的初始化成员表中增加调用虚基 类 A 的构造函数,将类 D 中的 x 成员的初值置为 1 。 • 注意: 用虚基类进行多重派生时,若虚基类没有缺省 的构造函数,则在每一个派生类的构造函数的初始化 成员列表中都应有对虚基类构造函数的调用。如上面 的 A(1) 。

  34. 如果派生类继承了多个基类,基类中有虚基类和非虚基类,那 么在创建该派生类的对象时,首先调用虚基类的构造函数,然 后调用非虚基类的构造函数,最后调用派生类的构造函数。若 虚基类又有多个,则虚基类构造函数的调用顺序取决于它们继 承时的说明顺序。 • 例 12.14 虚基类与非虚基类构造函数的调用顺序。 # include< iostream > using namespace std; class A{ int x; public: A( int a=0){ x=a; cout <<"call A( int =0) \ n"; } };

  35. class B{ int y; public: B( int a=0){ y=a; cout <<"call B( int =0) \ n"; } }; class C: public B,virtual public A { int z; public: C( int a=0, int b=0):B(b+20),A(b) { z=a; cout <<"call C( int =0, int =0) \ n"; } }; 程序运行结果 : int main(void) call A( int =0) { C c(100,200); call B( int =0) return 0; call C( int =0, int =0) }

  36. 虚基类(抽象类) 小结: 1 为什麽要使用虚基类 在多继承派生中一种可能产生二义性的情况: ⑴ 派生类有多个直接基类是同一个间接基类的派 生类; ⑵ 在派生类中需要访问共同间接基类的成员。 例如: class base { // 共同的间接基类 protected: int a; public: base() { a = 5; } };

  37. class base1 : public base // 直接基类1 { public: base1() { cout << "base1 a = " << a << endl; } }; class base2 : public base // 直接基类2 { public: base2() { cout << "base2 a = " << a << endl; } };

  38. class derived : public base1, public base2 { public: derived() { cout << "derived a = " << a << endl; } // 二义性:base1::a 还是 base2::a? }; main() { derived obj; return 0; } base base base1 base2 derived

  39. 2虚基类的概念 显然,解决上述二义性问题的办法是使派生类对象 层次结构中只有一个间接基类base实体。C++允许 在派生类的定义中使用关键字 virtual将基类说明为 虚基类来实现此目的。用虚基类重新定义上例中的 直接基类: class base1 : virtual public base { public: base1() { cout << "base1 a = " << a << endl; } };

  40. class base2 : virtual public base { public: base2() { cout << "base2 a = " << a << endl; } }; class derived : public base1, public base2 { public: derived() { cout << "derived a = " << a << endl; } // 访问 base::a 具有唯一性 };

  41. 又例如,在定义图形用户界面时,可以考虑定义这 样三 个类: class port { … }; 图形显示区 class region { … }; 屏幕上的任意区域 class menu { … }; 菜单-选项的集合 base Base1 Base2 derived

  42. 从这些类中派生出两个新类:window类和 tools类: class window : public port, public region { … }; class tools : public port, public menu { … }; 这两个派生类还可以再派生出新类: class appwind : public window, public tools { … }; 派生appwind的目的是为了得到一个带工具条的窗 口, 但根据上述定义得到的结果(如下图所示)是 工具条和窗口分别拥有自己的显示区,这并不是所 希望的。

  43. 所希望得到的是这两个对象为一个整体,只包含一 个显示区,即工具条是包含在窗口之内的。要做到 这一点需要在窗口类 window和工具条类 tools的定 义中将显示区类 port定义为虚基类。因此 tools和 window的定义变为如下形式: class window : virtual public port, public region { … }; class tools : virtual public port, public menu { … }; Window abc +

  44. 此时窗口即变为如下图所示的形式: 使用虚基类的两点说明: ⑴ 关键字 virtual与继承方式 public,protected和 private 的先后顺序无关紧要,它只说明是"虚拟派生"。 例如下面两个虚拟派生的声明等价。 class derived : virtual public base { … }; class derived : public virtualbase { … }; Window abc +

  45. 任何一个类都可以在作为某些派生类的虚基类的同时,又作为另一些派生类的非虚基类,例如: class b { … }; class x : virtual public b { … }; class y : virtual public b { … }; class z : public b { … }; class aa : public x, public y, public z { … }; 类 aa的对象的层次结构如图所示:

  46. x y z aa b b

  47. 3 虚基类的初始化 多继承中,虚基类与非虚基类在构造函数的调用顺 序上不同。虚基类构造函数调用顺序有三个原则: ⑴ 若同一层次中同时包含虚基类和非虚基类,先调 用虚基类构造函数,再调用非虚基类的构造函 数,即“同层优先”。 ⑵ 若同一层次中包含多个虚基类,虚基类的构造函 数按照定义表达式中的顺序从左至右调用。 ⑶ 若虚基类是一个派生类,则仍然先调用该虚基类 的基类构造函数,再调用该虚基类的构造函数。

  48. 注意,一般情况下,虚基类只定义不带参数的或带注意,一般情况下,虚基类只定义不带参数的或带 缺省参数的构造函数。否则虚基类的构造函数必须 在最终派生类构造函数的初始化表中,显式列出。

  49. circles rectangles ellipses arcs mix 例定义一个多继承派生类 mix用来实现在显示器屏 幕上的以一个指定点为中心画弧、圆、椭圆和矩 形。mix的基类为弧类 arcs、圆类 circles、椭圆 类 ellipses和矩形类rectangles。这些基类又由一 个共同的基类 point派生而来,因此需要将point 定义为虚基类。各类之间的关系图如下: point

  50. 不难看出,多继承机制是一把双刃剑,它虽然为类 不难看出,多继承机制是一把双刃剑,它虽然为类 提供了更加强大和灵活的派生定义能力,为解决某些 复杂问题提供了一种有效的手段,但同时也因为多继 承本身具有复杂的类间包含关系,引起了类的不稳定 性和加大了处理共享和二义性的难度。虚基类机制正 是为了有效地解决多继承引起的问题而引入。因此, 在 C++的有些编程环境中对多继承的实现做了一定的 限制,例如 Visual C++的 MFC编程向导就不提供多继 承选择(但还是支持手工编程实现多继承功能的)。

More Related