530 likes | 627 Views
第九章 继承性. 继承( inheritance ) 机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能。 这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构。 体现了由简单到复杂的认识过程 。. 9.1 基类和派生类 9.2 单一继承 9.3 多重继承 小结与作业. 9.1 继承性 9.1.1 继承和派生的概念. 层次概念 是计算机的重要概念。通过 继承 ( inheritance )的机制可对类( class )分层,提供类型 / 子类型的关系。
E N D
第九章 继承性 继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能。这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构。体现了由简单到复杂的认识过程。 9.1 基类和派生类 9.2 单一继承 9.3 多重继承 小结与作业
9.1继承性9.1.1 继承和派生的概念 层次概念是计算机的重要概念。通过继承(inheritance)的机制可对类(class)分层,提供类型/子类型的关系。 C++通过类派生(class derivation)的机制来支持继承。被继承的类称为基类(base class)或超类(superclass),新产生的类为派生类(derived class)或子类(subclass)。 基类和派生类的集合称作类继承层次结构(hierarchy)。 如果基类和派生类共享相同的公有接口,则派生类被称作基类的子类型(subtype)。 派生反映了事物之间的联系,事物的共性与个性之间的关系。 派生与独立设计若干相关的类,前者工作量少,重复的部分可以从基类继承来,不需要单独编程。
9.1.2 单一继承和多重继承 单一继承: 若某派生类只有一个基类,则称该派生类为单一继承的派生类。如由“汽车”类派生出的“小汽车”类、“大卡车”类均是单一继承。 多重继承: 若某派生类有2个或更多个基类,则称该派生类为多重继承的派生类。“儿子”有“父亲”、“母亲”2个基类,他既继承了“父亲”的特性,又继承了“母亲”的特性,属于多重继承。 为了便于理解,我们用家族的名词来描述基类与派生类,一个基类得出有多个派生类,称为“一父多子”,属单一继承;一个派生类由多个基类派生,称为“一子多父”,属多重继承。 对于多层派生来说,一个派生类的基类为直接基类,而基类的基类为间接基类。
基类1 基类2 …… 基类n 基类 派生类1 派生类2 派生类1 派生类2 9.1.2 单一继承和多重继承 单一继承和多重继承示意图: 一个基类可以直接派生出多个派生类 派生类可以由多个基类共同派生出来,称多重继承。 (a)多重继承 (一子多父) (b)单一继承 (一父多子)
9.1.3 派生类的定义格式 由基类派生出派生类的定义的一般形式为: class 派生类名:访问限定符 基类名1,访问限定符 基类名2,…… { private: 成员表1;//派生类增加或替代的私有成员 public: 成员表2;//派生类增加或替代的公有成员 protected: 成员表3;//派生类增加或替代的保护成员 };//分号不可少 其中基类1,基类2,……是已声明的类。只有一个基类时,是单一继承,上述给出的形式是多重继承。 在派生类定义的类体中给出的成员称为派生类成员,它们可以是新增加成员,它们给派生类添加了不同于基类的新的属性和功能,当派生类成员名与基类成员名相同时,派生类成员将取代基类成员,成为更新成员。
9.1.3 派生类的定义格式 吸收基类的成员 不论是数据成员,还是函数成员,除构造函数与析构函数外全盘接收 编制派生类时可分四步 改造基类成员 声明一个和某基类成员同名的新成员,派生类中的新成员就屏蔽了基类同名成员称为同名覆盖(override) 发展新成员 派生类新成员必须与基类成员不同名,它的加入保证派生类在功能上有所发展。 重写构造函数与析构函数
9.1.3 派生类的定义格式 上面的步骤就是继承与派生编程的规范化步骤。 第二步中,新成员如是成员函数,参数表也必须一样,否则是重载。 第三步中,独有的新成员才是继承与派生的核心特征。 第四步是重写构造函数与析构函数,派生类不继承这两种函数。不管原来的函数是否可用,必须一律重写可免出错。 9.1.4 基类成员在派生类中的访问权限 访问控制,亦称为继承方式,是对基类成员进一步的限制。访问控制也是三种: 公有(public)方式,亦称公有继承 保护(protected)方式,亦称保护继承 私有(private)方式, 亦称私有继承。
基类权限 继承方式 public protected private public private protected public protected 不可直接访问 private private 不可直接访问 protected protected 不可直接访问 9.1.4 基类成员在派生类中的访问权限 访问限定符有两方面含义:派生类成员(新增成员)函数对基类(继承来的)成员的访问(调用和操作),和从派生类对象之外对派生类对象中的基类成员的访问。 公有派生是绝对主流。
【例9.1】分析程序,指出访问权限的错误。 class Base{public:int b1;protected:int b2;private:int b3;}; class D1:public Base{public:void fun( ){b1=11;b2=12;b3=13;}}; class D2:private Base{public:void fun( ){b1=21;b2=22;b3=23;}}; class D3:protected Base{public:void fun( ){b1=31;b2=32;b3=33;}}; class D11:public D1{public:void fun( ){b1=111;b2=112;b3=113;}}; class D22:public D2{public:void fun( ){b1=221;b2=222;b3=223;}}; class D33:public D3{public:void fun( ){b1=331;b2=332;b3=333;}}; void main( ) { D11 d1;d1.b1=1;d1.b2=2;d1.b3=3; D22 d2;d2.b1=4;d2.b2=5;d2.b3=6; D33 d3;d3.b1=7;d3.b2=8;d3.b3=9; } 上述程序中有16处错误!注意,类的公有成员函数可调用类中的私有成员,但不可调用基类中的私有成员。使用对象只能调用本类中的公有成员。
9.2 单一继承9.2.1 单一继承程序举例 【例9.2】公有继承例子 #include<iostream.h> class A //类A有哪些成员? {public:void fun1( );protected:int a1;private:int a2;}; class B:public A //类B有哪些成员? {public:void fun2( );protected:int b1;private:int b2;}; class C:public B //类C有哪些成员? {public:void fun3( );};......... void main( ) { C c;........... } 注意程序中对象c有哪些可访问成员? 可访问fun1()和fun2()
【例9.3】私有单一继承程序举例。 #include<iostream.h> class A{public:void fa(int i){cout<<i<<endl;} void ga( ){cout<<"A.\n";} }; class B:A{public:void hb( ){ cout<<"B.\n";} A::fa; }; //B继承A,用class说明,不指明权限的一定是私有 void main( ) { B b;b.fa(5);b.ga();b.hb( ); }//红色表示的,有错误。 私有继承将基类的公有、保护成员变为私有,成员函数可访问这些成员,但对象不可访问;间接私有继承(二次私有继承)后,原来的成员都不可访问。
输出结果: name:wang 【例9.4】保护单一继承程序举例。 #include<iostream.h> #include<string.h> class A {public:A(const char*name1){strcpy(name,name1);} protected:char name[80]; }; class B:protected A {public:B(const char*nm):A(nm){ } //派生类的构造函数 void Print( ){cout<<"name:"<<name<<endl;}}; void main( ){B b("wang");b.Print( ); } 保护继承将基类的公有、保护变为保护,成员函数可访问,但对象不可访问;间接保护继承(二次保护继承)后,仍是这样。
9.2.2 单一继承派生类的构造函数与析构函数 派生类的构造函数不能从基类中继承,需重新定义,形式为: 派生类名::构造函数名(参数表):成员初始化表 { ……//函数体中主要对派生类新增成员进行初始化 } 成员初始化列表包括:基类构造函数、子对象的构造函数、常成员初始化、其他初始化项等,执行顺序为:基类构造函数、子对象的构造函数、其他初始化项、派生类构造函数体。 在构造函数的声明中,冒号及冒号以后部分必须略去。 析构函数也不能继承,派生类的析构函数将调用基类的析构函数,执行顺序与构造函数的执行顺序相反:派生类析构函数、子对象的析构函数、基类析构函数。
9.2.2单一继承派生类的构造函数与析构函数 *在派生类构造函数中,只要基类不是使用缺省构造函数都要显式给出基类名和参数表。 如果基类没有定义构造函数,则派生类也可以不定义,全部采用系统给定的缺省构造函数。 如果基类定义了带有形参表的构造函数时,派生类就应当定义构造函数。
9.2.2 单一继承派生类的构造函数与析构函数 析构函数的功能是作善后工作的。 只要在函数体内把派生类新增一般成员处理好就可以了,而对新增的成员对象和基类的善后工作,系统会自己调用成员对象和基类的析构函数来完成。 析构函数各部分执行次序与构造函数相反,首先对派生类新增一般成员析构,然后对新增对象成员析构,最后对基类成员析构。
【例9.5】分析程序,掌握派生类构造函数及析构函数的用法。【例9.5】分析程序,掌握派生类构造函数及析构函数的用法。 #include<iostream.h> class A { public:A( ){a=0;cout<<"隐含构造函数。A\n";} A(int i){a=i;cout<<"构造函数。A\n";} ~A(){cout<<"析构函数。A\n";} void Print( ){cout<<a<<',';} int Geta( ){return a;} private:int a;}; class B:public A{ public:B(){b=0;cout<<"隐含构造函数。B\n";} B(int i,int j,int k); ~B(){cout<<"析构函数。B\n";} void Print(){A::Print();cout<<b<<','<<aa.Geta()<<endl;} private:int b; A aa;}; B::B(int i,int j,int k):A(i),aa(j),b(k){cout<<"构造函数。B\n";} void main() { B bb[2];bb[0]=B(8,3,9);bb[1]=B(17,-18,19); for(int i=0;i<2;i++)bb[i].Print();}
输出结果: 隐含构造函数。A 隐含构造函数。A 隐含构造函数。B 析构函数。B 析构函数。A 析构函数。A 释放 临时对象B(17,-18,19) 生成并初始化 对象bb[0] 隐含构造函数。A 隐含构造函数。A 隐含构造函数。B 生成并初始化 对象bb[1] 8,9,3 17,19,-18 执行for循环 析构函数。B 析构函数。A 析构函数。A 构造函数。A 构造函数。A 构造函数。B 释放 对象bb[1] 生成并初始化 临时对象B(8,3,9) 析构函数。B 析构函数。A 析构函数。A 析构函数。B 析构函数。A 析构函数。A 释放 对象bb[0] 释放 临时对象B(8,3,9) 构造函数。A 构造函数。A 构造函数。B 生成并初始化 临时对象B(17,-18,19)
【例9.6】运行程序,观察结果,掌握派生类构造函数的用法。【例9.6】运行程序,观察结果,掌握派生类构造函数的用法。 #include<iostream.h> class A { public:A( ){a=0;cout<<"隐含构造函数。A\n";} A(int i){a=i;cout<<"构造函数。A\n";} ~A(){cout<<"析构函数。A\n";} void Print( ){cout<<a<<',';} private:int a;}; class B:public A{ public:B(){b1=b2=0;cout<<"隐含构造函数。B\n";} B(int i){b1=0;b2=i;cout<<"单参数构造函数。B\n";} B(int i,int j,int k):A(i),b1(j),b2(k){cout<<"三参数构造函数。B\n";} ~B(){cout<<"析构函数。B\n";} void Print(){A::Print();cout<<b1<<','<<b2<<endl;} private:int b1,b2; }; void main() { B b1,b2(15),b3(11,12,13); b1.Print(); b2.Print(); b3.Print();}
输出结果: 隐含构造函数。A 隐含构造函数。B 生成并初始化 对象b1 析构函数。B 析构函数。A 释放对象b3 隐含构造函数。A 单参数构造函数。B 生成并初始化 对象b2 析构函数。B 析构函数。A 释放对象b2 构造函数。A 三参数构造函数。B 生成并初始化 对象b3 析构函数。B 析构函数。A 释放对象b1 0,0,0 0,0,15 11,12,13 分别执行 b1.Print(); b2.Print(); b3.Print();
【例9.7】运行程序,观察结果,掌握派生类构造函数的特点。【例9.7】运行程序,观察结果,掌握派生类构造函数的特点。 #include<iostream.h> class B { public:B(){b1=b2=0; cout<<"隐含构造函数。B\n";} B(int i,int j){b1=i;b2=j;cout<<"构造函数。B\n";} ~B(){cout<<"析构函数。B\n";} void Print( ){cout<<b1<<','<<b2<<endl;} private:int b1,b2;}; class D:public B{ public:D(){d=0;cout<<"隐含构造函数。D\n";} D(int,int,int,int,int); ~D(){cout<<"析构函数。D\n";} void Print(){B::Print();bb.Print();cout<<d<<endl;} private:int d; B bb; }; D::D(int i,int j,int k,int l,int m):B(i,j),bb(k,l),d(m) { cout<<"五元构造函数。D\n"; } void main() { D d1(11,12,13,14,15),d2; d1.Print(); d2.Print(); }
输出结果: 构造函数。B 构造函数。B 五元构造函数。D 析构函数。D 析构函数。B 析构函数。B 生成并初始化 对象d1 释放 对象d2 隐含构造函数。B 隐含构造函数。B 隐含构造函数。D 析构函数。D 析构函数。B 析构函数。B 生成并初始化 对象d2 释放 对象d1 11,12 13,14 15 执行 d1.Print(); 0,0 0,0 0 执行 d2.Print();
【例9.8】分析程序结果,掌握基类和派生类同名函数的用法。【例9.8】分析程序结果,掌握基类和派生类同名函数的用法。 #include<iostream.h> class M { public:void Set(int i,int j){m1=i; m2=j;} void Print(){cout<<m1<<','<<m2<<endl; } protected:int m1,m2; }; class N:public M { public:void Set( ){n=m1*m2; } void Print(){cout<<n<<endl; } protected:int n; }; void main() { M m; m.Set(2,3);m.Print( ); N n; n.M::Set(5,8); n.M::Print( );n.Set( );n.Print( ); } 注意,同名函数使用时的区别! 输出结果: 2,3 5,8 40
9.2.3 子类型和赋值兼容规则 1. 子类型和类型适应 B类具有A类的行为(使用成员函数对成员进行操作),则称B类是A 类子类型。 若B类是A 类的子类型,则称B类型适应于A类型,且B类的对象可以当作A类型的对象来使用。 “白马”和“马”均是抽象的概念(类或类型),一匹具体的马就是“马”这个类的一个对象。显然,“白马”是“马”的子类型,“白马”适应于“马”,一匹具体的“白马”可以当作一匹具体的“马”来使用。 一般来说,公有继承的派生类是其基类的子类型。 子类型不具有对称性,即若B类是A类的子类型,则A类不一定是B类的子类型。
9.2.3 子类型和赋值兼容规则 2. 公有继承和赋值兼容规则 赋值兼容是指在一定条件下,不同类的对象可以赋值。有以下几种情况: ①子类型对象可以赋值给其父类型对象,反之不成立。不过父类对象接受的仅仅是子类继承的父类中的成员,对于子类中新增加的或更新的成员是不能接受的,下述两种情况也一样。 ②父类型对象可以引用子类型对象,反之不成立。 ③父类型对象指针可以指向子类型对象,反之不成立。
【例9.9】赋值兼容的例子。 #include<iostream.h> class M { public:M( ){m=0;} M(int i){m=i;} void Print( ){cout<<m<<endl;} int Getm( ){return m;} private:int m; }; class N:public M { public:N( ){n=0;} N(int i,int j):M(i),n(j){ } void Print(){cout<<n<<','; M::Print( ); } private:int n; }; void fun(M&p){cout<<p.Getm( )-5<<','; p.Print( ); } void main() { M m(17),q; N n(13,18); n.Print( ); q=n; q.Print( ); M *pm=new M(16); N*pn=new N(15,19); pm=pn; pm->Print( ); fun(*pn); N n1(21,34); M &rm=n1;n1.Print( ); } 注意,不同对象(指针)之间的赋值!
【例9.10】分析程序结果,掌握赋值兼容和保护成员的使用。【例9.10】分析程序结果,掌握赋值兼容和保护成员的使用。 #include<iostream.h> class B { public:void Print(){cout<<b<<endl; } protected:void Setb(int i){b=i*i; } private:int b; }; class D1:public B { public:void Setd1(int i){d1=i;Setb(d1); } void Print(){cout<<d1<<endl; } private:int d1; }; class D2:public B { public:void Setd2(int i){d2=i;Setb(d2); } void Print(){cout<<d2<<endl; } private:int d2; }; void main() { B b; D1 d1; D2 d2; d1.Setd1(7); d1.Print( ); d2.Setd2(5); d2.Print( ); b=d1; b.Print( ); B &rb=d2; rb.Print( ); } 注意,主程序中决不能增加b.Setb(2);!对一个类来说,保护与私有是一样的!
9.3 多重继承 如有以下定义: class B1{...}; class B2{...}; class B3{...}; class D:public B1, public B2, public B3 {...}; 类D就是由类B1、类B2、类B3公有派生的派生类,或者说,类D多重继承类B1、B2、B3
9.3.1 多重继承派生类的构造函数和析构函数 多重继承派生类的构造函数、析构函数不能直接继承,构造函数的定义格式为: 派生类构造函数名(参数表):成员初始化列表 {派生类构造函数体} 成员初始化列表包括各个基类构造函数、子对象的类构造函数、其他初始化项。 构造函数执行时,首先按照派生类定义时的基类顺序依次执行所有的基类构造函数,其次执行子对象类的构造函数,最后执行派生类构造函数的函数体。 派生类的析构函数中包含有其所有基类的析构函数,析构函数的执行顺序与构造函数正好相反。 没有定义构造函数和析构函数时,系统提供默认的。
【例9.11】多重继承派生类定义的例子 #include<iostream.h> class B1 {public:void Setb1(int i){b1=i;}void Printb1(){cout<<"b1="<<b1<<endl;} private:int b1; }; class B2 {public:void Setb2(int i){b2=i;} void Printb2(){cout<<"b2="<<b2<<endl;} private:int b2; }; class D:public B1,B2 //因为用class定义, B2的继承权限隐含私有 {public:void Setd(int i,int j){d=i;Setb2(j);} void Printd(){Printb2();cout<<"d="<<d<<endl;} private:int d; }; void main() {D objd; objd.Setb1(16); objd.Printb1( ); objd.Setd(24,76); objd.Printd( );} 注意,类D有公有成员函数:Setb1、Printb1、Setd、Printd 有私有成员函数:Setb2、Printb2,私有数据成员:d 请问:通过类D的对象是否可以操作类B1、B2的私有数据成员?
【例9.12】多重继承派生类的构造函数及不同基类中含有同名函数的例子【例9.12】多重继承派生类的构造函数及不同基类中含有同名函数的例子 #include<iostream.h> class B1 {public:B1(int i){b1=i;} int Getd( ){return b1;} protected:int b1;}; class B2 {public:B2(char i){b2=i;} char Getd( ){return b2;} protected:char b2;}; class D:public B1,public B2 {public:D(int,char,double); double Getd( ){return d;} private:double d;}; D::D(int i,char j,double k):B1(i),B2(j){d=k;} void main( ) {B1 b1(18); B2 b2('G'); D d(15,'H',6.56); cout<<"b1="<<b1.Getd()<<"\nb2="<<b2.Getd()<<"\n\nD:b1="; cout<<d.B1::Getd()<<"\nDb2="<<d.B2::Getd()<<"\nD:d="<<d.Getd()<<endl; B1 *ptrb1=&d; B2 rb2=d; //兼容赋值 cout<<"\nb1="<<ptrb1->Getd()<<"\nb2="<<rb2.Getd()<<endl; } 注意,类D有4个公有成员函数:Getd(继承B1)、 Getd(继承B2) 、Getd (新增加)、D(构造函数) 有2个保护数据成员:b1、b2。有1 个私有数据成员:d
【例9.13】多重继承派生类构造函数、析构函数执行顺序验证【例9.13】多重继承派生类构造函数、析构函数执行顺序验证 #include<iostream.h> class B1 {public:B1(int i){b1=i;cout<<"构造B1.b1="<<b1<<endl;} void Print(){cout<<b1<<endl;} ~B1(){cout<<"析构B1.b1="<<b1<<endl;} private:int b1; }; class B2 {public:B2(int i){b2=i; cout<<"构造B2.b2="<<b2<<endl;} void Print(){cout<<b2<<endl;} ~B2(){cout<<"析构B2.b2="<<b2<<endl;} private:int b2; }; class B3 {public:B3(int i){b3=i; cout<<"构造B3.b3="<<b3<<endl;} void Print(){cout<<b3<<endl;} ~B3(){cout<<"析构B3.b3="<<b3<<endl;} private:int b3; };
【例9.13】多重继承派生类构造函数、析构函数执行顺序验证【例9.13】多重继承派生类构造函数、析构函数执行顺序验证 class B4 {public:B4(int i){b4=i; cout<<"构造B4.b4="<<b4<<endl;} void Print(){cout<<b4<<endl;} ~B4(){cout<<"析构B4.b4="<<b4<<endl;} private:int b4; }; class D:public B3,public B1,public B4 {public: D(int d1,int d2,int d3,int d4,int d5):B1(d1),B3(d3),B4(d4),b2(d5) {d=d2;} void Print() {B1::Print();cout<<d<<endl;B3::Print();B4::Print();b2.B2::Print();} private:int d; B2 b2;}; void main() { D d(11,12,13,14,15);d.Print(); } 注意:构造函数的执行顺序是按照派生类定义的顺序进行的。
9.3.2 多重继承的二义性 程序设计语言严防语法的二义性,即同一个语句或语法单元可能出现不同的含义,称为二义性。实际上,计算机在翻译时是不会出现二义性的,二义性实际是程序员在编程时理解上的二义性。多重继承中理解上容易产生二义性的有以下两种情况: 1.调用不同基类中的相同成员名 2.调用共同基类的成员 一个派生类有多个直接基类,而这多个基类又是由一个基类派生的,即派生类的多个父类拥有共同的父类(相对于派生类来说,应称为祖父类)。如类A有父类B、C、D,而B、C是类E的子类,则类E是类A的共同基类。 解决情况1.的方法是在成员名前增加类名及作用域运算符,如类A的基类B、C中均有成员f在A中可被调用,调用B中f时,应写为B::f,调用C中f时,写成C::f。 解决情况2.的方法有两种措施,其一与上述方法相同,即成员限定法,在A中调用E中的成员h时,不写E::h,而写成B::h或C::h,因为A中使用的h是通过B或C间接继承来的。 其二是采用虚基类(后面介绍)。
【例9.14】解决不同基类中同名成员二义性的例子【例9.14】解决不同基类中同名成员二义性的例子 #include<iostream.h> class A {public:void f( ){cout<<"A::f( ).\n";}}; class B {public:void f( ){cout<<"B::f( ).\n";} void g( ){cout<<"B::g( ).\n";}}; class C:public A,public B {public:void g( ){cout<<"C::g( ).\n";} void h1( ){A::f( );cout<<"C::h1( ).\n";} void h2( ){B::f( );cout<<"C::h2( ).\n";} }; void main( ) { C c; c.A::f( );c.B::f( );c.B::g( );c.g( ); c.h1( ); c.h2( ); } 注意,类C有公有成员函数:f(从A继承)、f(从B继承)、g(从B继承)、g(自身新建)、h1 (自身新建)、 h2 (自身新建)等6个,同名成员必须区别,继承来的要注明成员的定义范围,如果不指明范围,一定是派生类中新定义的,否则编译将出错。
【例9.15】解决共同基类产生二义性的例子 #include<iostream.h> class A{public:A(int i){a=i;cout<<"构造 A\n";} void Print(){cout<<a<<',';} ~A(){cout<<"析构 A\n";} private:int a;}; class B1:public A{public:B1(int i,int j):A(i){b1=j;cout<<"构造 B1\n";} void Print(){A::Print();cout<<b1<<',';} ~B1(){cout<<"析构 B1\n";} private:int b1; }; class B2:public A {public:B2(int i,int j):A(i){b2=j;cout<<"构造 B2\n";} void Print(){A::Print();cout<<b2<<',';} ~B2(){cout<<"析构 B2\n";} private:int b2; }; class C:public B1,public B2 {public:C(int i,int j,int k,int l,int m):B1(i,j),B2(k,l){c=m;cout<<"构造 C\n";} void Print(){B1::Print();B2::Print();cout<<c<<endl;} ~C(){cout<<"析构 C\n";} private:int c; }; void main( ){ C c(16,19,23,25,38);c.Print();} 注意,类C的公有成员函数Print有5个,从A中间接继承来2个,一个由B1继承来,一个由B2继承来。 若增加语句c.A::Print( );则出现歧义错误。另外,从运行结果可以看出,类A的构造函数被调用2次。
【例9.16】成员限定法选择不同基类中的同名函数,解决二义性的例子【例9.16】成员限定法选择不同基类中的同名函数,解决二义性的例子 #include<iostream.h> class A { public:char *Return( ){return "A.\n"; } }; class B:public A { public:char *Return( ){return "B.\n"; } }; class C:public B { public:char *Return( ){return "C.\n"; } }; class D:public C { public:char *Return( ){return "D.\n"; } }; void main() { D d; cout<<d.A::Return( ); cout<<d.B::Return( ); cout<<d.C::Return( ); cout<<d.Return( ); } 注意,Return函数的具体表示方法。
9.3.3 虚基类 将公共基类说明成虚基类,可以避免派生类构造时,公共基类的构造函数被多次调用的情况,而且在初始化派生类对象时内存中只保存一份公共基类的成员。 说明虚基类的方法是: class 派生类名 virtual 继承方式 虚基类名 使用虚基类的派生类的构造函数中成员初始化列表中应专门列出虚基类的构造函数: 派生类构造函数名(参数表):若干基类构造函数,子对象构造函数,虚基类构造函数{派生类构造函数体} 只在创建派生类对象时调用一次虚基类构造函数,而在基类构造函数中不再调用虚基类构造函数,用以保证虚基类成员只初始化一次。
【例9.17】在【例9.15】中使用虚基类解决共同基类二义性【例9.17】在【例9.15】中使用虚基类解决共同基类二义性 #include<iostream.h> class A {public:A(int i){a=i;cout<<"构造 A\n";} void Print( ){cout<<a<<',';} ~A( ){cout<<"析构 A\n";} private:int a;}; class B1:virtual public A {public:B1(int i,int j):A(i){b1=j;cout<<"构造 B1\n";} void Print( ){A::Print( );cout<<b1<<',';} ~B1( ){cout<<"析构 B1\n";} private:int b1; }; class B2:virtual public A {public:B2(int i,int j):A(i){b2=j;cout<<"构造 B2\n";} void Print( ){A::Print( );cout<<b2<<',';} ~B2( ){cout<<"析构 B2\n";} private:int b2; };
【例9.17】在【例9.15】中使用虚基类解决共同基类二义性【例9.17】在【例9.15】中使用虚基类解决共同基类二义性 class C:public B1,public B2 {public:C(int i,int j,int k,int l,int m):B1(i,j),B2(k,l),A(i) { c=m;cout<<"构造 C\n"; } void Print( ) {B1::Print( );B2::Print( );cout<<c<<endl;} ~C( ){cout<<"析构 C\n";} private:int c; }; void main( ) { C c(16,19,23,25,38);c.Print( );c.A::Print( ); } 注意,与例15比较类C的公有成员函数Print只有4个,从A中间接继承来的只有1个,即由B1和B2继承来的是一个。 若增加语句c.A::Print( );则不会出现歧义错误。另外,从运行结果可以看出,类A的构造函数被调用1次。
【例9.18】运行程序,分析结果,注意虚基类的作用【例9.18】运行程序,分析结果,注意虚基类的作用 #include<iostream.h> class A{public:A(const char*s){cout<<s<<endl;} }; class B:virtual public A {public:B(const char*s1,const char*s2):A(s1) {cout<<s2<<endl;} }; class C:virtual public A {public:C(const char*s1,const char*s2):A(s1) {cout<<s2<<endl;} }; class D:public B,public C {public: D(const char*s1,const char*s2, const char*s3,const char*s4): B(s1,s2),C(s1,s3),A(s1){ cout<<s4<<endl;} }; void main( ) { D *ptr=new D("class A","class B","class C","class D"); delete ptr; } 注意,构造函数的执行顺序为: 虚基类A、基类B、基类C、派生类D
9.3.4 多重继承应用举例 • 【例9.19】计算4类人员月收入的程序 • 面向对象设计过程: • 一、设计类(对象) • 1.顶层类(对象):职工(编号,姓名,薪金,数据初始化方法) • 2.、二层类(对象):技术员(小时工资)、总经理(固定工资)、销售员(销售额提成) • 3.、三层类(对象):销售经理(固定工资+销售额提成) • 每个类(对象)均有数据初始化方法、计算薪金方法、显示数据方法等(公有成员函数) • 二、研究类(对象)的层次关系 • 显然,二层类(对象)可以从“职工”类(对象)继承许多信息,“销售经理”又可以从二层类(对象)中的“总经理”和“销售员”中继承,这样一来,相对于“销售经理”来说,“职工”是公共基类,可以设计成虚基类。
【例9.19】源程序(定义“职工”类) #include<iostream.h> class Employee {public: Employee( ) {cout<<"编号:";cin>>no; cout<<"姓名:";cin>>name; salary=0;} protected: int no; char name[20]; double salary; };
【例9.19】源程序(定义“技术员”类) class Technician:public Employee {public: Technician( ){hourlyrate=150;} void pay( ) {cout<<"\n技术员:"<<name<<"本月工作时数:"; cin>>workhours; salary=hourlyrate*workhours;} void display( ) {cout<<"技术员:"<<name<<" 编号:"<<no; cout<<" 月薪:"<<salary<<endl; } private: double hourlyrate; int workhours; };
【例9.19】源程序(定义“销售员”类) class Salesman:virtual public Employee {public: Salesman ( ){commrate=0.07;} void pay( ) {cout<<"\n销售员:"<<name<<"月销售量:"; cin>>sales; salary=sales*commrate;} void display( ) { cout<<"销售员:"<<name<<"编号:"; cout<<no<<"月薪:"<<salary<<endl; } protected: double commrate,sales; };
【例9.19】源程序(定义“总经理”类) class Manager:virtual public Employee {public: Manager( ){ monthlypay=8500; } void pay( ){ salary=monthlypay; } void display( ) {cout<<"经理:"<<name<<" 编号:"<<no; cout<<" 月薪:"<<salary<<endl; } protected: double monthlypay; };
【例9.19】源程序(定义“销售经理”类) class Salesmanager:public Manager,public Salesman {public: Salesmanager( ) { monthlypay=4000; commrate=0.005; } void pay( ) { cout<<"\n销售经理:"<<name<<"部门月销售量:"; cin>>sales; salary=monthlypay+sales*commrate;} void display( ) {cout<<"销售经理:"<<name<<" 编号:"<<no; cout<<" 月薪:"<<salary<<endl; } };
【例9.19】源程序(定义主函数) void main( ) { cout<<"创建经理信息:\n"; Manager m1; cout<<"创建技术员信息:\n"; Technician t1; cout<<"创建销售员信息:\n";Salesman s1; cout<<"创建销售经理信息:\n"; Salesmanager sm1; m1.pay( ); m1.display( ); t1.pay( ); t1.display( ); s1.pay( ); s1.display( ); sm1.pay( ); sm1.display( ); } 观察本程序运行结果,体会面向对象编程的思想
【例9.20】计算日期和时间的程序 面向对象设计过程: • 一、设计类(对象) • 1.“日期”类(年、月、日等数据,设置日期、输出日期等方法) • 2.“时间”类(时、分、秒等数据,设置时间、输出时间等方法) • 3.“时间日期”类,有时间和日期的所有数据,并增加日期和时间的联合输出方法 • 二、研究类(对象)的层次关系 • 显然,“时间日期”类,可由“日期”、“时间”两个类多重继承得来。
【例9.20】源程序(定义“日期”类) #include<iostream.h> #include<stdio.h> typedef char string80[80]; class Date {public: Date( ){ } Date(int y,int m,int d){ SetDate(y,m,d); } void SetDate(int y,int m,int d) { Year=y;Month=m;Day=d; } void GetStringDate(string80 &Date)//此处Date不是类名,是字符串名 { sprintf(Date,"%d,%d,%d",Year,Month,Day); //上述函数将年月日的具体数据作为字符串输出到字符数组Date中 } protected: int Year,Month,Day; };
【例9.20】源程序(定义“时间”类) class Time {public: Time( ){ } Time(int h,int m,int s){ SetTime(h,m,s); } void SetTime(int h,int m,int s) { Hours=h;Minutes=m;Seconds=s; } void GetStringTime(string80 &Time) { sprintf(Time,"%d,%d,%d",Hours,Minutes,Seconds); } protected: int Hours,Minutes,Seconds; };