550 likes | 629 Views
继承与类的派生. 多继承 多继承的概念 所谓 多继承 就是一个 新类 是从 多个基类 中 派生 而成 的。例如,在一个面向对象的图形用户界面中,为用 户界面提供的 窗口 、 滚动条 、 对话框 和各种 操作按钮 都是通过类对象来实现的。如果希望在 既有类 的基础 上定义一个 新类具有两个以上既有类的全部属性和操 作,就可以通过多继承的方法完成 。如,可以由 窗口 类 和 滚动条类 共同派生出一个 可滚动的窗口新类 。. 在有些情况下,可以使用 类的组合关系 ,即将一些 既有类对象定义为新类的成员来实现与多继承派生
E N D
多继承 多继承的概念 所谓多继承就是一个新类是从多个基类中派生而成 的。例如,在一个面向对象的图形用户界面中,为用 户界面提供的窗口、滚动条、对话框和各种操作按钮 都是通过类对象来实现的。如果希望在既有类的基础 上定义一个新类具有两个以上既有类的全部属性和操 作,就可以通过多继承的方法完成。如,可以由窗口 类和滚动条类共同派生出一个可滚动的窗口新类。
在有些情况下,可以使用类的组合关系,即将一些在有些情况下,可以使用类的组合关系,即将一些 既有类对象定义为新类的成员来实现与多继承派生 的新类相同的功能。例如,同样是定义可滚动的窗 口类,可以以窗口类为基类单继承派生,并将滚动 条类对象作为新类的新增成员。在新类中窗口类的 属性和行为被继承,而滚动条的属性和行为并没有 被继承,对滚动条对象成员的使用是通过新类的内 部消息实现的。
多继承的定义 多重继承派生类定义的一般形式: 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 均为公有派生
• 多重继承。 例 # 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<<')'; } };
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; } };
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
初始化基类成员和对象成员 • 派生类的构造函数完成: – 初始化派生类中的基类成员:调用基类的构造函数 – 初始化派生类中新增成员数据:调用派生类的构造函数 • 派生类的构造函数的格式: … 其中, ClassName 是派生类名; B1 、 、 Bn 是基类构造函数名; … a 是形参表, a1 、 、 an 是实参表。 … ClassName :: ClassName (a) : B1(a1), , Bn (an) … { } • 初始化成员列表: 冒号后列举 基类成员 的构 造函数和 对象成员 的构造函数,项与项之间 函数体初始 用逗号分隔。 化派生类中 • 初始化成员列表用基类名调用基类的构造函 的其他成员 数,可省略基类成员的缺省构造函数 ( 即无参 数据 或所有参数都带缺省值的构造函数 ) 。
• 例 派生类的构造函数和析构函数。 # 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"; } };
• 基类构造函数的调用顺序: § 与继承基类的顺序有关 § 与初始化成员列表中的顺 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 的析构函数 !
• 派生类含对象成员:其构造函数的初始化成员列表既要列举基 类成员的构造函数,又要列举对象成员的构造函数。 • 例 派生类中包含对象成员。 # 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"; } };
• 对象成员的构造函数的调用 • 基类成员的初始化必须 顺序与对象成员的说明顺序 使用基类名 有关,而与其在初始化成员 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 的析构函数 !
歧义、优先规则和赋值兼容规则歧义 • 歧义: 在多重继承的派生类中,使用不同基类中的同名 成员所出现的现象。 • 例 # include< iostream > using namespace std; class A{ protected: int x ; public: void Show( ) { cout <<"x="<<x<<' \ n'; } A( int a=0){ x=a; } };
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; }
• 解决歧义的方法: Ø 使各基类中的成员名各不相同。 Ø 将基类的成员数据的访问权限说明为 private , 并 在相应的基类中提供成员函数访问这些成员数据, 但并不实用。原因是,在实际编程时,通常将基 类成员数据的访问权限定义为 protected , 以保障 类的封装性和便于派生类访问。 Ø 用类名限定来指明所访问的成员。格式为: 类名 :: 成员名
• 例 用类名限定来指明所访问的成员。 # 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; } };
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; }
用基类成员还是对象成员? • 多个基类成员: class B{ • 问题: 因派生类 D 中含有两个 直接 protected: 继承 来的成员 x , 在使用类 D 的对 int x; 象访问基类的成员 x 时会 产生无法 … 避免的歧义 。 }; class D: public B,public B { … }; • C++ 规定: 任一基类在派生类中只能 直接 继承一次,以 避免上述无法避免的歧义。
• 解决方法: Ø 组合法: 在 D 类中,组合类 B 的两个对象: class D{ B b1,b2 ;// 若需组合更多,则可用类 B 的数组 … }; Ø 继承+组合法: class D: public B { // 直接继承基类 B 一次 B b ; // 组合类 B 的对象 … }; • 基类成员和对象成员, 在功能上是相同的,但在使用 上是有区别的 。 • 通过类的继承和组合,可用已有的类来定义新类,为 软件重用提供了良好机制。
优先规则 • 优先规则: 指派生类新增成员优先于基类中的同名成 员,并不产生歧义。若要使用基类的同名成员,应在 其前面加类名限定。 • 例 优先规则。 # include< iostream > using namespace std; class A{ protected: int x ; public: A( int x){ this - >x=x; } void Show( ) { cout <<" 类 A 的 x="<<x<<' \ n'; } };
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; }
赋值兼容规则 • 赋值兼容规则: 规定派生类的对象与其基类的对象之 间互相赋值的规则。设: class B{ … // }; class D: public B{ … // }; D d; B b,* pb ;
则有以下赋值兼容规则: Ø 派生类的对象可赋给基类的对象,但反之不然。 b=d;// 允许。将 d 中从类 B 继承的部分赋给 b d=b;// 不允许 Ø 派生类对象的地址可赋给基类型的指针变量。 pb =&d; Ø 派生类对象可初始化基类型的引用。 B& rb =d; • 注意: 在后两种情况下,使用基类的指针或引用时, 只能访问从相应基类中继承来的成员,而不允许访问 其他基类的成员或在派生类中增加的成员。
多继承派生类编程实例 例在屏幕上显示一个带有字符串的圆。 问题分析 定义一个圆类 circles和字符串类 gmessage分别用于 在屏幕的指定位置按设定半径绘制圆和在屏幕的指 定位置显示特定的字符串。为了使所显示的圆和字 符串的位置紧密相关,采用多继承机制,从 circles 和 gmessage派生出新类 mcircle用于完成本例的最 终需求。另外为了更好地体现面向对象的设计思 想,再定义一个 point类,用于确定所要显示的圆和 字符串的位置。因此 circles和 gmessage都应该从 point派生。
point point #x:int #y:int #x:int #y:int circles mcircle gmessage #radius:int -font:int -field:int -msg:char* +show() +show() +show() ⑴类图描述
虚基类 • 例 一个公共的基类在派生类中产生两个拷贝。 #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'; } };
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; }
派生类包含两份基类成员,产生歧义。 • 例 # 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){ } };
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; 则用本例实现不仅多占内存,且可能产生歧 } 义,难以保证数据一致。 如何解决?
• 虚基类: 在定义派生类时, 在继承的公共基类的类名 前加关键字 virtual , 使得公共基类在派生类中只有一 份拷贝。 • 虚基类的格式: class ClassName : virtual access ClassName1 … { }; 或 class ClassName :access virtual ClassName1 … { };
• 定义虚基类,使派生类中只有基类的一个拷贝。 例 # 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'; } };
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 的缺省的构造函数 。
• 进一步讨论: 若将 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) 。
• 如果派生类继承了多个基类,基类中有虚基类和非虚基类,那 么在创建该派生类的对象时,首先调用虚基类的构造函数,然 后调用非虚基类的构造函数,最后调用派生类的构造函数。若 虚基类又有多个,则虚基类构造函数的调用顺序取决于它们继 承时的说明顺序。 • 例 12.14 虚基类与非虚基类构造函数的调用顺序。 # include< iostream > using namespace std; class A{ int x; public: A( int a=0){ x=a; cout <<"call A( int =0) \ n"; } };
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) }
虚基类(抽象类) 小结: 1 为什麽要使用虚基类 在多继承派生中一种可能产生二义性的情况: ⑴ 派生类有多个直接基类是同一个间接基类的派 生类; ⑵ 在派生类中需要访问共同间接基类的成员。 例如: class base { // 共同的间接基类 protected: int a; public: base() { a = 5; } };
class base1 : public base // 直接基类1 { public: base1() { cout << "base1 a = " << a << endl; } }; class base2 : public base // 直接基类2 { public: base2() { cout << "base2 a = " << a << endl; } };
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
2虚基类的概念 显然,解决上述二义性问题的办法是使派生类对象 层次结构中只有一个间接基类base实体。C++允许 在派生类的定义中使用关键字 virtual将基类说明为 虚基类来实现此目的。用虚基类重新定义上例中的 直接基类: class base1 : virtual public base { public: base1() { cout << "base1 a = " << a << endl; } };
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 具有唯一性 };
又例如,在定义图形用户界面时,可以考虑定义这 样三 个类: class port { … }; 图形显示区 class region { … }; 屏幕上的任意区域 class menu { … }; 菜单-选项的集合 base Base1 Base2 derived
从这些类中派生出两个新类:window类和 tools类: class window : public port, public region { … }; class tools : public port, public menu { … }; 这两个派生类还可以再派生出新类: class appwind : public window, public tools { … }; 派生appwind的目的是为了得到一个带工具条的窗 口, 但根据上述定义得到的结果(如下图所示)是 工具条和窗口分别拥有自己的显示区,这并不是所 希望的。
所希望得到的是这两个对象为一个整体,只包含一 个显示区,即工具条是包含在窗口之内的。要做到 这一点需要在窗口类 window和工具条类 tools的定 义中将显示区类 port定义为虚基类。因此 tools和 window的定义变为如下形式: class window : virtual public port, public region { … }; class tools : virtual public port, public menu { … }; Window abc +
此时窗口即变为如下图所示的形式: 使用虚基类的两点说明: ⑴ 关键字 virtual与继承方式 public,protected和 private 的先后顺序无关紧要,它只说明是"虚拟派生"。 例如下面两个虚拟派生的声明等价。 class derived : virtual public base { … }; class derived : public virtualbase { … }; Window abc +
⑵ 任何一个类都可以在作为某些派生类的虚基类的同时,又作为另一些派生类的非虚基类,例如: 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的对象的层次结构如图所示:
x y z aa b b
3 虚基类的初始化 多继承中,虚基类与非虚基类在构造函数的调用顺 序上不同。虚基类构造函数调用顺序有三个原则: ⑴ 若同一层次中同时包含虚基类和非虚基类,先调 用虚基类构造函数,再调用非虚基类的构造函 数,即“同层优先”。 ⑵ 若同一层次中包含多个虚基类,虚基类的构造函 数按照定义表达式中的顺序从左至右调用。 ⑶ 若虚基类是一个派生类,则仍然先调用该虚基类 的基类构造函数,再调用该虚基类的构造函数。
注意,一般情况下,虚基类只定义不带参数的或带注意,一般情况下,虚基类只定义不带参数的或带 缺省参数的构造函数。否则虚基类的构造函数必须 在最终派生类构造函数的初始化表中,显式列出。
circles rectangles ellipses arcs mix 例定义一个多继承派生类 mix用来实现在显示器屏 幕上的以一个指定点为中心画弧、圆、椭圆和矩 形。mix的基类为弧类 arcs、圆类 circles、椭圆 类 ellipses和矩形类rectangles。这些基类又由一 个共同的基类 point派生而来,因此需要将point 定义为虚基类。各类之间的关系图如下: point
不难看出,多继承机制是一把双刃剑,它虽然为类 不难看出,多继承机制是一把双刃剑,它虽然为类 提供了更加强大和灵活的派生定义能力,为解决某些 复杂问题提供了一种有效的手段,但同时也因为多继 承本身具有复杂的类间包含关系,引起了类的不稳定 性和加大了处理共享和二义性的难度。虚基类机制正 是为了有效地解决多继承引起的问题而引入。因此, 在 C++的有些编程环境中对多继承的实现做了一定的 限制,例如 Visual C++的 MFC编程向导就不提供多继 承选择(但还是支持手工编程实现多继承功能的)。