




版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
1、高质量C+编程 C+的继承,继承的种类,继承分类: 1:从父类个数区分:单继承、多继承 2:从继承方式区分:公有继承、保护继承、私有继承 继承语法: class 派生类名: 继承方式 基类名 新增成员声明; ; 继承的三项工作内容:吸收、改造、新增 。,继承方式列表,三种继承方式,三种继承方式 私有继承 private (化公为私) 受保护继承 protected (折中) 公有继承 public (原封不动) 继承方式从两个方面影响了访问权限: 派生类的成员对基类成员的访问权限; 派生类对象对基类成员的访问权限。,保护级别降低,继承方式与访问权限比较,尽管继承方式与访问权限都在使用三个相同的
2、关键字:public、protected、private。 但语义上有本质区别。 继承方式关键字描述了类间关系,即从大的方面控制类的所有成员,表现了类的继承特性。 访问权限关键字仅是类控制其成员的被访问性的,表现了类的封装特性。,继承方式,先看继承方式: 公有继承:基类的公有成员和保护成员在派生类中保持原有访问属性,基类的私有成员仍为私有的。“原封不动” 私有继承:基类的公有成员和保护成员在派生类中成为私有成员,基类的私有成员仍为私有的。“化公为私” 受保护继承:基类的公有成员和保护成员在派生类中成为受保护成员,基类的私有成员仍为私有的。“二者折中” 先讨论最简单的公有单继承,这种方式最简单,
3、但应用最广。,访问权限,再看访问权限: public权限表示类的这个管辖域下的成员,在类内、类外皆可以无限制地被访问; private权限表示类的这个管辖域下的成员,仅供本类的成员任意访问,对一切本类以外是不开放的。 protected权限表示类的这个管辖域下的成员基本同于私有,但比私有稍放松,即将“仅供本类的成员任意访问”扩大为“派生类的成员也可以访问”。,派生类对象的内存布局,子类对象中包含了基类子对象和子类新增部分。 而且基类子对象在子类对象的前面。 这个规则对其他继承方式或多个基类的情况下始终成立。,父类部分(父类子对象),子类新增部分,子类对象,成员访问 同上 同上 对象访问,成员访
4、问(即类内成员间的相互访问),pri 表示私有成员 pro 表示保护成员 pub 表示公有成员 B 表示基类成员 N 表示子类新增成员,pri pro pub,继承方式模型和成员访问图例,公有继承-public继承方式,从两个方面看: 成员访问: 基类的三种访问域成员的访问属性在派生类中保持不变。 即,派生类中的成员函数可以直接访问基类中的public和protected成员,但不能直接访问基类的private成员。 对象访问: 通过派生类的对象只能访问基类的public成员。 (我们称类内访问叫成员访问;称类外访问叫对象访问。),各成员的访问性图示,Bpri Npri Bpro Npro B
5、pub Npub,pri pro pub,类用户概念,如果没有继承,类只有两种用户: 类的自身用户(设计类的程序员)供类内自用 类的外部用户(使用类的程序员)供类外程序使用 将类划分为 private 和 public 访问级别反映了用户种类的这一差别: 类成员用户既可以访问public成员,也可以访问private、protected 成员; 使用类的用户(即对象用户)只能访问public成员(这些成员被称为“类接口”)。,C+的接口分为三个层次: 1、函数的形参表是函数接口; 2、类的公有成员是类的接口; 3、对于一个多态类族,基类是多态类族的接口。 另外,设计模式还提供更高层次的接口,这
6、里不作讨论。,类的第三种用户,有了继承,就有了类的第三种用户:从类派生定义新类的程序员。(比较保护成员和私有成员的封装性) 表示可以访问。,protected访问权限,protected有另一个重要性质: 派生类只能通过派生类对象(参数传入的)访问其基类的protected成员,派生类对其基类对象(参数传入)的protected成员没有特殊访问权限。 这个表述有点晦涩,下面通过一个实例来解析这条性质。,protected 的例子,class Base public: int pub_data; protected: int pro_data; private: int pri_data; ;,
7、class Derived:public Base public: void memfcn(Derived ,子类的成员函数只能通过上面两种方式访问基类的protected成员,第三种访问是错误的! 也就是说想通过对象访问私有和保护成员,只能在本类作用域内或者友元中访问。,子类中不可以访问父类私有成员!,也不可以通过对象访问私有成员!,理解此题的思路: 与memfcn函数打交道的共有几个对象?各对象状况?子类对象中的pro_data属于哪个类?三条语句是怎样访问 pro_data的?画出函数调用的示意图。,前例的讨论,第一句: cout pro_data; 是在 memfcn()函数中访问th
8、is所指对象的父类部分的pro_data ,属于类内访问。,首先弄清与memfcn( )打交道的几个对象:,this,第二句: cout d. pro_data; 是在 memfcn()函数中访问参数d所传对象的父类部分的pro_data ,也属于类内访问。,第三句: cout b. pro_data; 是在 memfcn()函数中访问参数b所传的父类对象的pro_data ,而memfcn()函数对于b 对象属于类外访问。所以error,memfcn( ),结论:想通过对象访问私有或保护成员,只能在本类作用域或使用友元中访问.,就是,继承下的构造函数,基类的构造函数不被继承,派生类中需要重新
9、定义自己的构造函数。 定义构造函数时,只需要对本类中新增成员进行初始化即可。对于继承来的基类成员的初始化工作由基类构造函数完成,基类构造函数是自动被调用的,但对有参的构造函数要用初始化列表给出实参。 原则:先初始化基类成员,再初始化派生类的成员。 派生类的构造函数要用参数总表备齐实参,以便于传递给基类的构造函数所用。,这意味着什么?,这意味着责任!,澄清一个认识: 对基类构造函数的调用是由派生类构造函数发起的,这初始化列表就是函数调用的发起者,这恰表现出派生类构造函数对基类构造函数的“连锁调用”。,派生类构造函数语法,派生类名:派生类名 (基类所需的形参, 本类成员所需的形参) : 基类名(参
10、数表), 成员1(), 成员2(), . 函数体 构造函数初始化列表:为派生类中的基类部分和该派生类的数据成员提供初始值。它并不能指定初始化的执行次序。 固有次序是首先初始化基类,然后根据声明次序依次初始化派生类的各成员。,基类构造函数显式调用可以不显式写出,但不写不等于不调用调用基类无参的构造函数!,派生类构造函数调用图示,pri pro pub,pri pro pub,pri pro pub,基类,四个函数,新增成员,派生类,继承成员,是用派生类构造函数中的初始化列表体现的!,连锁调用的发起点!,构造函数连锁调用,class Father public: Father() cout “Fa
11、ther()”endl; Father( int m ,float n) cout m n endl; Father( char *pName) cout pName endl; ; class Child:public Father public: Child(): Father( 80,1.80) Child( int m):Father(“ Jon”) coutm1.75endl; Child( int m ,float n): Father(“ Jon”) Child( char *pName )coutpNameendl; ; void main() Child C1; Child
12、C2( 50,70); Child C3(“Li”); ,我们可以运用初始化列表来指定连锁调用走向,用以达到设计目标。,体现了构造函数的连锁调用。,派生类构造函数说明,派生类的构造函数的书写和基类是否有无参构造函数密切相关!如果派生类没有显式给出构造函数,则派生类会调用基类的无参构造函数,如果找不到,则编译不通过。 派生类构造函数的初始化列表只能初始化派生类的成员,不能直接初始化继承来的基类成员不可插手基类成员的初始化,而是通过基类构造函数来初始化的。(见后例) 派生类构造函数通过初始化列表来间接初始化基类的成员。,派生类与基类构造函数的关系,当基类中未声明任何构造函数或声明了无参构造函数时,
13、派生类构造函数可以不向基类构造函数传递参数。 若基类中未声明构造函数,派生类中也可以不声明,全启用缺省形式构造函数。 当基类声明了带参构造函数时,派生类必须提供构造函数,通过初始化列表将参数传递给基类构造函数。 派生类构造函数只能初始化自己的直接基类对象,在孙类中初始化祖父类对象是错误的。,派生类构造函数-例:考察下面代码,class Base public: Base():i(0) private: int i; / 是私有的 ; class Derived: public Base public: Derived():i(0),j(0) i=0; private: int j; ;,cla
14、ss Base public: Base():i(0) public: int i ; / 是公有的 ; class Derived: public Base public: Derived():i(0),j(0) i=5; private: int j; ;,两种写法都是错误的!因为基类的i是私有。,i(0)写法错误。i=5写法正确,但语义不合理。,派生类构造函数-注意点,class Base public: Base():i(0) private: int i; ; class Derived: public Base public: Derived():Base(),j(0) priva
15、te: int j; ;,class Grandchildren:public Derived public: Grandchildren():Base(),Derived(),k(0) protected: private: int k; ;,可以显式写出,也可以省略,派生类构造函数只能初始化自己的直接基类,在 Grandchildren类的构造函数初始化列表中初始化 Base是错误的。,错误:初始化了间接基类。,多继承且有内嵌对象时的构造函数,派生类名:派生类名 (基类1形参,基类2形参,.基类n形参,本类形参) : 基类1(参数), 基类2(参数), .基类n(参数), 对象数据成员的初
16、始化 本类成员赋值语句; 这种情况下的初始化列表包括俩部分: 多个基类子对象的初始化; 派生类新增的组合对象的初始化。,初始化列表,多继承且有内嵌对象时的构造函数例,#include using namespace std; class B1 /基类B1 public: B1(int i) coutconstructing B1 iendl; /有参构造函数 ; class B2 /基类B2 public: B2(int j) coutconstructing B2 jendl; /有参构造函数 ; class B3 /基类B3 public: B3()coutconstructing B3
17、*endl; /无参构造函数 ;,class C: public B2, public B1, public B3 public: C(int a, int b, int c, int d): B1(a),memberB2(d),memberB1(c),B2(b) ,x(d) cout“constructing C ”endl; private:/派生类的对象成员 B1 memberB1; B2 memberB2; B3 memberB3; int x; ; void main() C obj(1,2,3,4); ,运行结果: constructing B2 2 constructing B1
18、 1 constructing B3 * constructing B1 3 constructing B2 4 constructing B3 * constructing C,26,初始化列表的书写次序无法指定继承及成员的初始化次序!,B2(b) , B1(a), memberB1(c), memberB2(d), x(d),应该写成:,基类1,新增成员是对象,派生类,多继承且有内嵌对象的图示,对象,基类2,基类3,继承下的构造函数调用次序,1 首先调用基类构造函数。调用顺序按照它们被继承时在继承列表中指明的顺序(从左向右)。 2 然后调用成员对象的构造函数。调用顺序按照它们在本类中的声明
19、顺序。 3 最后,执行派生类构造函数体中的语句。 不管是多继承还是单继承、不管是公有还是私有还是保护、不管类中是否含有对象成员,以上规则是铁定的。,派生类拷贝构造函数,#include using namespace std; class B / 父类 public: B(); B( int i); B(const B /父类其它成员函数实现 略,派生类拷贝构造函数,class C: public B / 子类 public: C(); C( int i, int j); C(const C /子类其他成员函数的实现 略,拷贝构造函数使用初始化列表,void main() C obj1(5,6
20、); C obj2(obj1); obj2.Print(); ,派生类拷贝构造函数的性质,1)如果派生类没有定义了自己的拷贝构造函数,则编译器将自动调用基类的缺省拷贝构造函数初始化对象的基类部分。 2)如果派生类定义了自己的拷贝构造函数,该拷贝构造函数一般应显式使用基类拷贝构造函数,以初始化对象的基类部分。,class Base /* . */ ; class Derived: public Base public: Derived( const Derived,如果没有显式写出,这时编译器不会报错,而是去调用基类无参的构造函数。看下例:,派生类拷贝构造函数代码,class Base publ
21、ic: Base( int a=2,int b=2) :i(a),j(b) Base( const Base,class Derived: public Base public: Derived( int a=1,int b=1,int c=1) :Base(a,b), k(c) Derived( const Derived,显示: 2 3 2 2 3,void main() Derived d(1,2,3); d.ShowData(); Derived dd(d); dd.ShowData(); 运行结果如何?,此处没显式写出拷贝构造函数,结果分析,根据“如果派生类定义了自己的拷贝构造函数,
22、但没有显式调用基类拷贝构造函数,编译器不会报错,而是去调用基类无参的构造函数”法则, 对于语句: Derived( const Derived / 为简化,省略参数 person(); . private: string name, address; / 组合 ;,class student: public person public: student();/ 为简化,省略参数 student(); . private: string schoolname, schooladdress; / 组合; / 一个返回对象的独立函数,形参也是个对象 student returnstudent(stu
23、dent s) return s; 当有student s1; 后调用函数: student ss = returnstudent(s1 );,此句会发生什么?,有继承有组合,每个子类对象共含有六个对象:,说来难以置信:有18次函数调用(12次拷贝构造和6次析构!)这还是经过优化后的结果,否则是30次! Why?,优化前: 6次拷贝构造发生在s1传给s, 6次拷贝构造来完成s给中间对象,中间对象给ss有6次拷贝构造, 6次析构中间对象, 6次析构s。 优化后: s1传给s 6次拷贝构造, s给ss6次拷贝构造, 6次析构s。,struct和class比较,默认访问级别: struct 和 cl
24、ass 关键字定义的类具有不同的默认访问级别。struct 定义的类若不写出访问控制权限,默认是公有的; class 定义的类若不写出访问控制权限,默认是私有的;,class Base /* . */ ; struct D1 : Base /* . */ ; class D2 : Base /* . */ ;,此处没显式写出继承方式,默认为公有继承。,此处没显式写出继承方式,默认为私有继承。,在类继承时,继承方式关键字也可以不写。 默认继承方式: 对于 struct 类默认的继承方式是 public继承。而对于 class 类默认的继承方式是 private继承。,struct和class比较
25、,一种常见的误解认为用 struct关键字定义的类与用 class 定义的类有更大的区别。实际区别只有两方面:默认的成员保护级别和默认的继承方式,此外没有其他。 尽管私有继承在使用 class关键字时是默认情况,但这在实践中很少使用。因为私有继承是如此罕见,通常显式指定 private 继承是比依赖于默认更好的办法。显式指定可清楚指出想要私有继承而不是一时疏忽。,继承中的静态成员,回顾static成员 static 成员是类属性 static 数据成员必须在类外初始化 能否定义 static 常成员函数? 如果基类定义 static 成员,则整个继承层次中只有一个这样的成员。即:无论从基类派生
26、出多少个派生类,每个 static 成员只有一个实例。 在访问权限允许的条件下,子孙类们都可访问基类的静态成员。,继承中的静态成员的访问方式,假定静态成员是可访问的(即公有的),则既可以通过基类名访问,也可以通过派生类名访问 static 成员。,class Base public: static void f() /* */ ; class Derived: public Base ;,Base:f(); /OK Derived:f(); /OK,静态成员访问方式示例,struct Base static void statmem() / 默认为 public权限 cout Basestat
27、ic member called!endl; ; struct Derived : Base / 默认为公有继承 void fun(const Derived /可否这么访问? ,这四种调用皆可!,静态成员访问仍受访问权限制约,class Base public: static size_t object_count(); private: static size_t obj_count; ; size_t Base:obj_count = 0; class Derived : public Base /* . . . */ ;,void main() Base b; Derived d; B
28、ase:object_count(); Base:obj_count; b.object_count(); b.obj_count; Derived:object_count(); Derived:obj_count; d.object_count(); d.obj_count; ,这个静态成员是私有的,以上红色标出的4句访问错误,继承知识的深入IS-A关系,公有继承是IS-A关系。 所谓“IS-A关系”是指当Base能派上用场的地方,Derived一定可以胜任。就是说,Base具有的行为,Derived也都拥有! 私有和保护继承则不属于 IS-A 关系范畴。 请你指出下面的关系是否能构造出i
29、s-a关系 例如: 企鹅和鸟; 矩形和正方形。 世界上并不存在一个“适用于所有软件”的完美设计。所谓最佳设计取决于系统希望做什么事。,但是IS-A 关系并不简单!,类型兼容规则引入,class Person . ; class Student: public Person . ; 从日常经验中我们知道,每个学生是人,但并非每个人是学生。这正是上面的层次结构所声明的。 这表达了一种逻辑:任何对 于“人” 成立的事实 , 如都有生日都有姓名,对于 “学生”也成立;但任何对 于“学生” 成立的事实,如在某一学校上学,对于 人则不成立。 “人”的概念比“学生”的概念更广泛更抽象; “学生”是“人”这个
30、大概念下的小概念,是一种具体的、特定的人群。,类型兼容规则概念,类型兼容规则:在需要基类对象的任何地方,公有派生类的对象都可以使用胜任。表现在三个方面: 派生类的对象可以初始化或赋值给基类对象; 派生类的对象可以初始化基类的引用; 基类的指针可以指向派生类对象。 此规则又称“类型包容法则”、“向上兼容性”、“向上映射” 大家会体会到,类型兼容规则只体现了类接口的性质,不包括封装在类内部的细节。通过公有继承,派生类具备了基类的所有功能(public方法),凡是基类能解决的问题,公有派生类都可以胜任。,类型兼容规则图示,若将基类对象形象化为:,则派生类的对象可表示为:,在需要一杯水时,一壶水足以满
31、足要求:,这就是“类型兼容”! 派生类对象(一壶水)中完全含有了基类对象(一杯水),因此完全可以满足要求基类对象初始化或赋值要求,而且绰绰有余杯子装不下的水会洒掉。,拷贝构造函数应用了类型兼容规则,class A public: A() A( const A,利用了类型兼容规则,类型兼容规则的后果,class Base public: int m_a , m_b ; ; class Derived : public Base public: int m_c ; ; void main( void ) Derived objD1; Base objB1= objD1; /发生转换 Derived
32、 *pD1 = /发生转换 ,这3句是类型兼容规则的3种表现,但后果不同。,m _a,m _b,m _c,Derived objD1,m _b,m _a,Base objB1,对象初始化。 真切割!,m _a,m _b,m _c,Derived objD1,Derived * pD1,Base * pB1,指针初始化。 假切割!,Base class B0 public: void display() coutdisplay(); / “对象指针-公有成员名 ,void main() B0 b0;/B0类对象 B1 b1;/B1类对象 D1 d1;/D1类对象 B0 *p;/B0类型的指针 p
33、= ,运行结果: B0:display() B0:display() B0:display(),切割的结果!,类型兼容规则下的指针,class B public: . private: int b; ; class D :public B public: . private: int d; ; D objd; B * pb = ,这两个指针尽管指在同一个位置,那仅仅是首址相同而已,所涵盖的区域并不相同。 是指针的类型决定了识别域。,类型兼容规则的“切割”,“切割”的真实含义是指对像的内存是否变化、对象的行为是否受到影响、是否可以恢复成原来类型? “真切割”则是物理改变,不可恢复了。 “假切割”
34、则是逻辑变化,可以恢复原状。 类型兼容前提下的三种初始化或赋值操作中,只有子类对象初始化或赋值给父类对象时发生“真切割”。而使用指针或引用,只是逻辑上发生了“切割”,实际上并没有缺失任何东西。于是才会有 “可以通过强制类型转换,将父类的指针或引用转变成子类指针或引用”的转换操作。,类型兼容规则千万不要用在数组上,子类对象确实是父类对象的一种。但子类对象的数组却不是父类对象数组的一种。如有: class B class D :public B / / ; ; D Array5; B * p = Array; 此时可以用Arrayi 访问每个元素, 可是能用 pi访问吗? Why? 画出内存存储图
35、示就明白了!,内存存储图示,子类对象,基类对象,class B,class D,“切割”在数组上的体现!,效果: 按基类类型看待子类类型的数组,每个元素会被切割地七零八落。 弊端: 1. 访问时会找错对象; 2. 析构时会删错对象,还会残留有内存垃圾; 如何正确使用?,1. 用指针数组,使对象不放在数组中; B * array = ; 2. 彻底不使用数组,改用强类型的vector. vector vbp ;,类型兼容规则也不要用在二级指针上,用父类的指针指向子类对象确实是一种常用的手法: Circle * cp = new Circle ; / Circle 是公有继承的子类 Shape *
36、 sp = cp ; / Shape是父类 但指向父类指针的指针却不是指向子类指针的指针的一种: Circle * ccp = / error! Why? 看下面图示:,它的自身类型是Circle *,它的目标类型是Shape *,它们相符吗?,继承下作用域的关系,C+语言新定义了一个作用域类作用域! 每个类都持有自己的作用域,在该作用域中定义了自己的成员的名字。 在继承情况下,语法上看,基类与派生类尽管在声明时是并列的,可在语义上看是嵌套的:大概念包含了小概念。,基类作用域覆盖了派生类的作用域,派生类的外层是基类。,继承下作用域的嵌套关系,自然地,在子类的地盘上,子类的标识符会屏蔽掉外层父类
37、的同名标识符。 若想在子类域中启用父类的同名标识符,用 : :“捞出来”! 匹配次序:如果在子类作用域中找不到某标识符,就在外层的父类作用域中查找该名字。 名字匹配发生在编译时。,正是这种类作用域的层次嵌套,使我们能够直接访问父类的成员,就好象这些成员是子类自己的成员一样。,同名隐藏规则:,C+语言对于各个类成员的命名、对于继承时派生类新增成员命名是没有任何限制的,于是便可能引发同名冲突。 继承时基类之间、基类与派生类之间发生成员同名时,将出现对成员访问的不确定性同名二义性。 同名隐藏规则: 当派生类与基类有同名成员时,派生类中的成员将屏蔽基类中的同名成员。 若未特别指明,通过派生类对象使用的
38、都是派生类中的成员,基类中同名成员被屏蔽; 如要通过派生类对象访问基类中被屏蔽的同名成员,应使用基类名限定符(:)。,同名隐藏实例,#include using namespace std; class B1 public: int nV; void fun() coutMember of B1endl; ; class B2 public: int nV; void fun() coutMember of B2endl; ; class D1: public B1, public B2 / 多继承 public: int nV; void fun()coutMember of D1endl;
39、 /同名成员函数 ;,与另一基类数据成员同名。,与两个基类数据成员都同名。,void main() D1 d1; d1.nV=1; d1.fun(); d1.B1:nV=2; d1.B1:fun(); d1.B2:nV=3; d1.B2:fun(); ,用“对象名. 成员名” 访问的是子类成员。,“屏蔽”的机制揭秘:编译器会对基类同名的成员名之前,额外增加其类名,以示区分。 于是用户访问时也要前缀类名。,同名二义性-举例,class A public: void f(); ; class B public: void f(); void g(); ;,class C: public A, pu
40、blic B public: void g(); void h(); ; 如果定义了:C c1; 则 c1.f(); 有二义性(c1对象体内有同名的两个父类的 void f() ) 而 c1.g(); 无二义性(尽管c1体内有两个同名的g(),可分属不同的作用域,它们同名隐藏),同名二义性-举例,解决方法一:用类名来限定 c1.A:f() 或 c1.B:f() 解决方法二:同名隐藏,再造接口 在派生类C 中再声明一个同名成员函数f(),屏蔽掉两个基类的同名成员函数,再在该函数体内根据需要调用 A:f() 或 B:f()。,加“作用域分辨符标识”, 可访问基类被屏蔽的成员,标识符查找次序,在子类
41、对象中查找某个标识符的步骤: 1.首先查找本函数作用域; 2.查找函数外围作用域(class Derived作用域); 3.再往外扩大到 class Base作用域; 4.再往外扩大到 Base所在的namespace作用域(如果有的话); 5.最后,查找全局作用域。,这恰是编译器实现“同名隐藏规则”的内幕。,作用域屏蔽关系,class Base public: void mf1(); void mf1(int); void mf2(); void mf3(); void mf3(double); private: int x; ; class Derived: public Base pub
42、lic: void mf1(); /屏蔽了Base:mf1()和Base:mf1(int) void mf3(); /屏蔽了Base:mf3()和Base:mf3(double) void mf4(); / 新增成员 ;,不要违背IS-A关系,如果你正在使用public继承而又屏蔽了那些重载函数,就是违反base和derived 之间的is-a关系。前例中子类的void mf1()会屏蔽父类的重载函数 void mf1()和 void mf1(int)。于是子类对象就不能随心所欲地表现出is-a关系。 因为父类的 void mf1()和 void mf1(int)不能使用。 而我们知道:is-
43、a是public继承的基石! 为了不屏蔽重载函数,可以使用using声明。但是要注意不可造成“重复定义”! 见下例:,使用using声明,class Base public: void mf1() ; void mf1(int); void mf2(); void mf3(); void mf3(double); private: int x; ; class Derived: public Base public: void mf4(); void mf1(int x,int y);/ 因为它的存在使得基类的mf1() 不能调用 ;,using Base:mf1; / 把两个Base: mf
44、1引入到本作用域内 / 使得基类的两个mf1函数可以被调用,/此时不可再定义void mf1(); 因为它将与Base:mf1()冲突,转交函数,上述代码或许会在某些不支持using声明的老式编译器上出错,我们可以采用“转交函数”的手段来弥补,将继承而得的名称汇入Derived class作用域内。,class Base public: void mf1() ; void mf1(int i); ; class Derived : public Base public: void mf1(); void mf1(int i) /转交函数 Base:mf1(i); ;,直接调用基类相应函数,以不
45、动声色地完成调用。,又一种二义性,当派生类(孙类)从多个基类派生(父类),而这些基类又从同一个基类(祖父类)派生,则在孙类中访问此共同基类(祖父类)中的成员时,将产生另一种访问不确定性 路径二义性。,子派生类1,基类,孙派生类,子派生类2,路径二义性示意代码,class B / 祖父类 public: void fun(); int b; ; class B1 : public B /一个父类 private: int b1; ; class B2 : public B /另一父类 private: int b2; ;,class D : public B1,public B2 public:
46、 int fund(); private: int d; ; void main() D dobj ; ,基类和各子类拥有不同名的成员,没有了同名二义性问题。,路径二义的含义: D dobj; dobj.b dobj.B:b,到底是哪个b?,固然可以用类名:加以区别,但 dobj.B1:b和 dobj.B2:b却表示的是两个不同的成员,这仍未解决重复存储的问题。,D对象存储示意图,解决方法:虚基类,概念: 以virtual修饰声明共同的直接基类。虚基类又称虚拟继承。 语法:class B1: virtual public B 作用: 用来解决多继承时可能发生的对同一基类继承多次和多层而产生的访
47、问二义性问题。 为最远的派生类提供唯一的一份基类成员,而不产生多个重复的副本。 注意: 在第一级继承时就要将共同基类设计为虚基类。,虚基类举例,class B public: int b; / 为演示方便,将数据成员暂设为公有的 class B1 : virtual public B private: int b1; class B2 : virtual public B private: int b2; class C: public B1, public B2 private: float d; 在子类对象中最远基类成分是唯一的。于是下面的访问是正确的: C cobj; cobj.b;,在
48、第一级继承时将基类设计为虚基类。,在孙类继承时不再加virtual 。,虚拟继承示意图,虚拟继承前的孙类对象,子类1对象,子派生类1,基类,孙派生类,子派生类2,子类2对象,虚拟继承后的孙类对象,孙类对象,基类对象,基类对象,虚拟继承的实现机制,虚拟继承时,编译器会在派生类的对象中自动添加一个vbptr指针,这个vbptr指向一个由类维护的、本类的全体对象共享的vbtable(偏移量表),它是个数组,(请类比虚函数的“虚函数表”vftable)。 这个表的第一项通常为0,设计者不公开其含义,或许是某种掩码或标志,目的为了某些设计的需要。 其规律是:当类不带虚函数时,它是0 x00000000;
49、当类带虚函数时,它是0 xFFFFFFFC。 这个表的第二项为:在派生类中所含虚基类对象部分的首地址与该虚基类的vbptr之间的偏移量,是个整数。,#include using namespace std; class B0 /定义基类B0 public: int nV; void fun()coutMember of B0endl; ; class B1: virtual public B0 /B0为虚基类,派生B1类 public: int nV1; ; class B2: virtual public B0 /B0为虚基类,派生B2类 public: int nV2; ;,class D
50、: public B1, public B2 /派生类D定义 public: int nVd; void fund()coutMember of Dendl; ;,虚拟继承机制的图示,B0只有一个数据成员,是整形,于是其对象为: B1也只有一个数据成员,其对象本应为: 可是设计为 虚拟继承后,对象呈现为: 同理,另一个子类对象 应该是: 孙类对象该如何排列其成员?,nV,原本在此位置的基类成员nV被调整到对象的最后边,取而代之的是一个指针。,nV,vbptr,nV1,nV,nVd,于是前面的4个类构成的类族,由虚拟继承消除了因继承的“多层共祖” 形成的二义。 于是,孙类对象就可以放心地使用最远
51、基类的成员了。 void main()/程序主函数 D d1; /声明D类对象d1 d1.nV=2; /使用最远基类成员 d1.fun(); ,struct A int a1; void af( ); ; struct B : virtual public A int b1; void bf( ); ; struct C : virtual public A int c1; int c2; void cf( ); ; struct D:public B,public C int d1; void df( ); ;,b1,vbptr,B,a1,0,8,c1,vbptr,C,c2,0,12,a1,
52、b1,vbptr(B),D,vbptr(C),0,24,c1,c2,d1,a1,0,16,它们是各类自己的表,注:以上数字是十进制数,对象布局,vbtable,8,24,16,12,前一页图中会看到,编译器自动为对象插入了“内部调整指针”,这不是“指向成员的指针”,而是指向“调整量数组”的。因为基类B0会含有多个成员,这些成员都要被调整走,不可能用一个指针指向多个成员,因此需要一个“调整量数组” 。 这个“内部调整指针” vbptr简称其为“指针”,它占4个字节(一个字长),当然它仍然遵循对齐原则。 这个“内部调整指针”不是只出现在孙类对象中,在两个子类的对象中就已存在了(若产生对象的话才能测到)。,可以看出,编译器外加的“内部调整指针”和“调整量数组”都是用于对象数据成员的,与函数成员无关。函数在继承时是对象“共享的”而不是“含有的”,不占用对象空间,也就不用调整。,调整指针有几个?内存布局啥样?,#include class B0 char e; ; class B1 : virtual public B0 char b1; ; class B2 : virtual public B0 char b2; ; class D:public B1,public B2 char d; ; void main() B0 b; B1 b1;
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 责令改正法律适用研究
- SLM成形HfO2@TiCp-GH3536复合材料组织性能研究
- 基于VR-AR的编程课程教学设计与应用研究-以中职C语言为例
- 糖尿病酮症病人的个案护理
- 妇女两癌健康知识
- 幼儿健康蔬菜知识启蒙
- 颌面部骨折护理课件
- 某企业客户关系管理分析
- 2025护理质量控制计划
- 傅玄教育思想体系解析
- 九年级全一册英语单词默写表(人教版)
- DB50T 990-2020 地质灾害治理工程施工质量验收规范
- 《铁路电力线路运行与检修》课件 第五章 电力线路运行与维护
- 2024年交通基础设施行业信用回顾与2025年展望
- 10kV油浸式变压器技术规范书-通 用部分
- 专题1 重要词汇复习及专练-2022-2023学年七年级英语上学期期末考点大串讲(人教版)(试题版)
- 【物 理】2024-2025学年八年级上册物理寒假作业人教版
- 上海市2025年中考模拟初三英语试卷试题及答案
- 医学教材 医药市场营销学(陈玉文主编-人卫社)0医药产品价格与价格策略
- 2024全球美甲用品市场分析报告
- DB51-T 3060-2023 四川省政务信息化后评价指南
评论
0/150
提交评论