第5章 继承和多态_第1页
第5章 继承和多态_第2页
第5章 继承和多态_第3页
第5章 继承和多态_第4页
第5章 继承和多态_第5页
已阅读5页,还剩50页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

第5章继承和多态本章主要内容:5.1公有继承和is-a关系

5.2派生类对象的构造、析构

5.3同名覆盖原则

5.4赋值兼容原则

5.5多态性和虚函数

5.6典型范例——各类物体面积求和

5.7其它继承方式

5.8多继承

5.9深度探索 计算机学院李卫明面向对象的核心思想是抽象、封装与信息隐蔽、继承和多态。前面学习了对现实世界或思维世界中的实体进行抽象,用类完成封装,完成类的接口和类的实现的分离,类的用户只需了解类的接口,无需关心类实现的细节,达到了信息隐蔽目的,同时,通过函数重载和运算符重载实现了静态多态性。本章学习面向对象程序设计中重要的继承机制和通过动态绑定实现动态多态性。计算机学院李卫明5.1 公有继承和is-a关系

5.1.1 继承和派生

人们对现实世界或思维世界中的实体进行分类,形成树形层次关系。如图5.1所示,细分类的个体和基础类个体之间具有特殊和一般的关系,细分类的个体也是一种基础类的个体,称为is-a关系,细分类个体除了具有基础类个体的属性和行为能力外,还具有自己特殊的属性和行为能力。图5.1分类关系举例在面向对象程序设计中,用公有继承和派生机制反映上述分类关系。用于描述基础类个体的类称为基类,也可称为父类,用于描述细分类个体的类称为派生类,也可称为子类。计算机学院李卫明

一个类可以直接派生出多个派生类,每个派生出的子类均继承了基类的公有属性和公有函数描述的功能,派生类本身也可作为基类继续派生,形成多级派生。

一个类直接派生出的子类以及子类直接或间接派生出的所有类统称为它的子孙类,一个类及它的子孙类形成一个派生关系类族,派生关系类族中从父类一直往上到最上层结点路径上的所有类通称为该类的祖先类,子孙类从祖先类直接或间接派生产生。子孙类继承祖先类的所有公有属性和公有函数成员描述的功能。计算机学院李卫明声明公有派生类的一般建议格式为:class派生类名:public基类名{public:派生类公有函数成员private:

派生类私有函数成员private:

派生类私有数据成员};与普通类声明一样,C++语言本身并未限制派生类成员的声明顺序。这里声明的是公有派生,绝大部分派生是公有派生,反映is-a(是)关系,关于特殊场合使用的其它派生,请参见本章第7节。计算机学院李卫明5.1.2 派生类定义C++中成员访问控制修饰符有三种:private、protected、public,其中,私有成员只能在本类成员函数和友元函数里访问,不可在派生类的成员函数里访问,公有成员可以在任何地方访问。protected访问控制专用于继承和派生,protected修饰的成员是保护成员,除了可以像私有成员在本类成员函数和友元函数里访问外,还可在派生类的成员函数里访问,但不可在类外访问。

无论公有成员、私有成员还是保护成员,基类数据成员都是派生类对象的组成部分,公有继承时,基类的公有和保护成员可访问性在派生类中保持不变,多级派生时,子孙类成员函数也可以直接访问祖先类的保护成员和公有成员。公有继承时,子孙类对象继承了祖先类的公有数据属性和公有函数功能,程序中任何位置可访问对象祖先类的公有成员。下节样例程序里我们可以看到,学生类CStudent和教师类CTeacher公有继承普通人类CPerson,学生和教师个体作为派生类对象具有基类公有函数所描述的功能,同时,派生类成员函数中可直接访问基类中的保护成员m_strName。计算机学院李卫明5.1.3 派生类访问控制CPerson、CStudent、CTeacherUML图示:

图5.2普通人类Cperson、学生类Cstudent和教师类Cteacher具体样例代码见Ex5.1计算机学院李卫明5.1.4 派生类样例整个派生类对象状态包含基类部分对象状态和派生类部分数据成员构成的状态,派生类对象实际可分为两部分:基类部分对象和派生类部分对象。基类部分数据成员,用于表示基类部分对象状态;派生类本身的所有数据成员,用于表示派生类部分对象状态。基类部分数据成员包括直接基类的所有数据成员,也包括间接基类的所有数据成员,不论它们的访问控制是公有、保护,还是私有的。

派生类构造函数一般形式如下:派生类名(形参表):基类构造函数名(基类构造函数参数表),数据成员1(数据成员参数表1),数据成员2(数据成员参数表2),…{……//函数体}对象建立时数据成员初始化由构造函数完成,对象撤销时扫尾处理由析构函数完成。基类构造函数只能完成基类部分对象的初始化,基类的析构函数只能完成基类部分对象的扫尾处理,从这个意义来说,基类的构造函数和析构函数不可继承。计算机学院李卫明5.2 派生类对象的构造、析构派生类构造函数会先调用基类构造函数完成基类部分数据成员的初始化,再完成派生类部分数据成员的初始化,析构时,次序正好相反,派生类析构函数会先执行派生类析构函数体,完成派生类部分对象的扫尾处理,再隐式调用基类析构函数完成基类部分对象的扫尾处理。

样例Ex5.1中基类、派生类均未定义构造函数和析构函数,基类、派生类构造函数和析构函数由编译器合成。编译器合成的CPerson调用string类构造函数完成名字成员m_strName初始化为空字符串,年龄成员m_iAge未初始化,派生类CStudent构造函数先调用基类无参构造函数完成基类部分对象的初始化,再完成数据成员子对象选修课表向量m_strVec初始化为空向量,派生类CTeacher构造函数先调用基类无参构造函数完成基类部分对象的初始化,再完成数据成员子对象讲授课表向量m_strVec初始化为空向量。

计算机学院李卫明如果需要在建立CStudent类对象时直接指定学生的名字和年龄,在建立CTeacher类对象时直接指定教师的名字和年龄,需要为基类CPerson类增加下述公有构造函数:

CPerson(stringstrName,intiAge):m_strName(strName),m_iAge(iAge){}

CStudent类和CTeacher类也需要增加下述公有构造函数:

CStudent(stringstrName,intiAge):CPerson(strName,iAge){}CTeacher(stringstrName,intiAge):CPerson(strName,iAge){}

计算机学院李卫明上述派生类新增构造函数在初始化列表里显式调用基类的构造函数。如果派生类构造函数初始化列表里没有显式调用基类的构造函数,编译器会隐式调用基类的缺省无参构造函数,当基类有其它构造函数而无缺省无参构造函数时,编译器会报错。

计算机学院李卫明一般情况下,派生类继承了基类公有的数据成员和公有函数成员。如果派生类新增的数据成员或函数成员与基类中成员同名,则基类中所有同名成员名字被覆盖,派生类成员函数中或外部不可通过派生类对象名直接访问基类同名成员,这一原则称为同名覆盖原则。如我们为上节样例中CPerson类增加显示人员信息的成员函数Show后:voidShow()const{cout<<m_strName<<","<<m_iAge<<"yearsold"<<endl;}再为CStudent类和CTeacher类增加显示学生信息和教师信息的成员函数Show后,基类CPerson中同名成员函数已被覆盖。CStudent类、CTeacher类的对象或它们的派生类对象调用的Show函数都是覆盖后的派生类成员函数Show。

下面2个语句分别调用CStudent类、CTeacher类的成员函数Show:studZhang.Show();//studZhang是一个CStudent类对象teacherLiu.Show();//teacherLiu是一个CTeacher类对象计算机学院李卫明5.3 同名覆盖原则如果需要调用基类中被覆盖的同名成员函数,调用时可在成员函数名前添加基类名::。如下述CStudent类和CTeacher类成员函数Show实现时,用CPerson::Show()调用基类Show成员函数:voidCStudent::Show()const{CPerson::Show();cout<<"isastudent"<<endl;}voidCTeacher::Show()const{CPerson::Show();cout<<"isateacher"<<endl;}同名覆盖后,CStudent类和CTeacher类对象,可用下述方式调用基类Show成员函数:studZhang.CPerson::Show();//studZhang是一个CStudent类对象teacherLiu.CPerson::Show();//teacherLiu是一个CTeacher类对象计算机学院李卫明公有继承下,派生类或子孙类对象也是一种特殊的基类对象,具有基类对象的属性和公有函数描述的功能,因此,在需要基类对象的场合,派生类对象或子孙类对象也都可以满足需要,这称为赋值兼容原则。具体如下:首先,考虑对象指针类型。指针类型变量用于存放对象的地址,指向对象。指针类型变量可以指向本类对象,也允许基类指针变量指向派生类对象或子孙类对象,但不可以指向祖先类对象;基类指针变量作为形参时,实参可以是指向本类对象的指针、指向派生类对象或子孙类对象的指针,不可以是指向祖先类对象的指针;函数返回对象指针类型时,函数可以实际返回本类对象的指针、派生类对象或子孙类对象的指针,不可以返回祖先类对象的指针。赋值时,可以给本类对象指针类型的变量赋值本类对象指针、派生类对象指针或子孙类对象指针,不可以将祖先类对象的指针赋值给基类类型的指针变量。计算机学院李卫明5.4 赋值兼容原则函数OlderOne返回候选者中选出年龄较大者的指针:CPerson*OlderOne(CPserson*first,CPserson*second);可以调用函数OlderOne从两位学生中选出年龄大的一位,也可以调用函数OlderOne从学生和教师中选出年龄大的一位:CPerson*pOlderOne;pOlderOne=OlderOne(&studZhang,&studWang);CPerson*pOlderPerson=OlderOne(&zhang,&teacherLiu);但不可以将基类指针变量赋值给派生类指针变量,如:CStudent*pOlderStudent=pOlderOne;//错误函数BestStudent可以从两位学生中选出选课多的人,返回学生指针:CStudent*BestStudent(CStudent*first,CStudent*second);可以调用函数BestStudent从两位学生中选出选课多的一位:CStudent*pBestStudent=BestStudent(&studZhang,&studWang);但不可以调用函数BestStudent从学生和教师中选出选课多的一位,也不可以从普通人和学生中选出选课多的一位:BestStudent(&studZhang,&teacherLiu);//错误BestStudent(&studZhang,&someOne);//错误计算机学院李卫明我们再来考虑引用。引用具有指针类似道理。引用是对象的别名,引用对象与被引用对象是同一个对象。引用变量可以引用本类对象,C++允许基类引用变量引用派生类对象或子孙类对象,但不可以引用祖先类对象;基类引用变量作为形参时,实参可以是本类对象、派生类对象或子孙类对象,实参不可以是祖先类对象;函数返回对象引用时,函数可以实际返回本类对象、派生类对象或子孙类对象,但不可以返回祖先类对象。定义引用变量初始化时,可以初始化为本类对象、派生类对象或子孙类对象,不可以初始化为祖先类对象。计算机学院李卫明函数OlderOne2返回候选者中年龄最大者的引用,原型如下:constCPerson&OlderOne2(constCPserson&first,constCPserson&second);可以调用函数OlderOne2从两位学生中选出年龄大的一位,也可以从学生和教师中选出年龄大的一位:CPerson&olderOne=OlderOne2(studZhang,studWang);CPerson&olderPerson=OlderOne2(studZhang,teacherLiu);不可以定义派生类引用变量时将其初始化为基类对象:CStudent&olderStudent=olderOne;//错误函数BestStudent返回选课多学生的引用:constCStudent&BestStudent2(constCStudent&first,constCStudent&second);可以调用函数BestStudent从两位学生中选出选课多的一位:constCStudent&bestStudent=BestStudent2(studZhang,studWang);不可以调用函数BestStudent2从学生和教师中选出选课多的一位,也不可以从普通人和学生中选出选课多的一位:BestStudent2(studZhang,teacherLiu);//错误BestStudent2(studZhang,someOne);//错误,someOne不是学生对象计算机学院李卫明考虑类类型普通变量和函数参数传值、函数返回普通对象类型情况。一个类类型变量,可以存放一个同类对象,不能存放其它类类型对象。C++可以将对象赋值给同类对象变量,同类对象赋值时实际执行的是对象的复制赋值或转移赋值。C++不可以将对象赋值给不相关类对象变量或派生类对象变量。C++允许将对象赋值给基类对象变量,不会编译报错、甚至不会警告,但实际复制的只是基类部分子对象,不是派生类对象的全部,这个现象称为切片现象,一般场合下存在切片现象的赋值是不合适的。参数采用传值方式时,基类变量作为形参,实参可以是本类对象、不可以是祖先类对象,C++允许派生类对象或子孙类对象作实参,但复制给形参的只是基类部分对象,也存在切片现象,并不合适;

函数返回对象时,函数可以实际返回本类对象,不可以返回祖先类对象,C++允许返回派生类对象或子孙类对象,但返回的只是基类部分对象,存在切片现象,也并不合适。此外,大对象在函数间传递时,如果参数采用传值方式,存在对象的复制,效率会较低,一般应该尽量避免大对象的传值方式传递或传值方式返回,同时,要保证不可返回临时对象的引用或指针,因为临时对象在函数返回后已析构。计算机学院李卫明面向对象程序设计中,作用在对象上的函数或运算符的调用可以看作向对象发出某个消息,对象接收消息后做出某些动作。不同类的对象接收同一个消息后可以调用不同的函数体,做出不同的动作,这个现象称为多态性。抽象、封装、继承和多态性是面向对象程序设计的主要特征。C++面向对象程序设计中多态性可根据决定调用函数时机分为静态多态性和动态动态性。如果在编译期间确定实际调用的函数,也就是采用静态绑定(binding),这样的多态性称为静态多态性;如果编译期间不能确定实际调用的函数,需要程序执行到具体函数调用语句时才能确定实际调用的函数,也就是需要采用动态绑定(binding),这样的多态性称为动态多态性。前面介绍的普通函数重载和成员函数重载都是在编译阶段确定实际调用的函数,因此,函数重载是一种静态多态性。下一章介绍的函数模板和类模板也是在编译阶段确定实际调用的函数和实际使用的类及相应成员函数,这样的多态性也是一种静态多态性。动态多态性是一种面向对象程序设计中非常重要的多态性。例如:同样调用run方法,飞鸟调用时动作是飞,马调用时动作是奔跑。计算机学院李卫明5.5 多态性和虚函数5.5.1 多态性面向对象程序设计中动态多态性的实现受到继承性的支持,通过在派生类中重定义基类虚函数来实现多态性,利用类继承的层次关系,把具有通用功能的协议存放在类层次中尽可能高的地方,而将实现这一功能的不同方法置于较低层次,在这些低层次上生成的对象就能给通用消息以不同的响应。同一类族的对象可以统一管理,利用多态性用户可发送一个通用的信息给同一类族的对象,而将所有的实现细节都留给接收消息的对象自行决定,不同类型的对象即可调用不同的函数响应同一消息,简化了程序。计算机学院李卫明样例Ex5.2在Ex51基础上,综合了前面各节情况,各类增加了构造函数、增加了成员函数Show,增加了三个普通函数ShowByPointer、ShowByReference、ShowByValue,参数分别以对象指针、对象引用、对象传值方式传递,根据赋值兼容原则,调用这三个函数时形参实际是派生类对象指针(语句S112~S114)、派生类对象引用(语句S117~S119)、派生类对象的切片(语句S122~S124)。从运行结果我们看到,无论何种情况,样例程序调用的都是基类的Show函数,我们需要的动态多态性并未发生。由于C++采用的是静态绑定,这些情况下,普通函数ShowByPointer、ShowByReference、ShowByValue三个函数中参数类型分别为基类对象指针、基类对象引用、基类对象,绑定的都是基类的Show成员函数。

计算机学院李卫明5.5.2 虚函数C++动态多态性如何实现呢?如果需要动态多态性,我们需要在基类成员函数声明或定义前加入关键字virtual,声明这个成员函数是虚函数,也就是上述样例语句S13函数定义前加关键字virtual,声明Show成员函数是虚函数,派生类成员函数Show前是否加关键字virtual没有实际影响,C++对虚函数采用动态绑定。修改后运行结果中,通过对象指针和引用调用虚函数都体现出动态多态性,通过传值调用时,形参得到的实际是派生类对象的切片,已完全是基类对象,因此无动态多态性。C++规定构造函数不可以是虚函数,析构函数可以是虚函数。类成员函数中直接调用虚函数或通过this调用虚函数都是当前实际对象类型自己定义或继承的虚函数,具有动态多态性。构造函数里调用的虚函数时,由于对象派生类部分尚未构造完成,调用的是构造函数所在类或继承的虚函数,不是派生类中定义的虚函数。计算机学院李卫明

C++规定构造函数不可以是虚函数,析构函数可以是虚函数。那么,如何精确复制一个基类指针所指的派生类对象呢?例如,我们有哺乳动物类CMammal,从哺乳动物类CMammal派生出狗类CDog和马类CHorse,指针向量zoo定义如下:vector<CMammal*>zoo;程序运行过程中,指针向量zoo已存放若干动物对象指针,每个指针分别指向狗类CDog对象或马类CHorse对象,如何复制出原指针向量zoo里管理的动物,让动物加倍呢?我们可以采用如下克隆技术,就是在哺乳动物类CMammal里增加纯虚函数(在下节介绍)克隆Clone,在狗类CDog和马类具体实现Clone函数:计算机学院李卫明5.5.3 派生类对象的克隆classCMammal{public:virtualCMammal*Clone()const=0;...//其它部分省略};classCDog:publicCMammal{public:CMammal*Clone()const{returnnewCDog(*this);//调用拷贝构造动态生成一只狗}...//其它部分省略};计算机学院李卫明classCHorse:publicCMammal{public:CMammal*Clone()const{returnnewCHorse(*this);//调用拷贝构造动态生成一匹马}...//其它部分省略};下列代码执行后将zoo中原有动物逐个克隆,zoo中动物个数多了一倍。

size_tn=zoo.size();//原动物数量

for(size_ti=0;i<n;++i){zoo.push_back(zoo[i]->Clone());//克隆原动物,放入zoo}计算机学院李卫明考虑类族统一接口的需要,有时在基类中将某一成员函数声明为虚函数,只是在基类中预留了一个接口函数名,具体功能在基类中并无实现,而是在派生类中根据需要去实现,正如上节动物园例子中所述。这时应当将基类的这个成员函数声明为纯虚函数,声明纯虚函数的一般形式是:virtual返回值类型函数名(形参表)=0;纯虚函数没有函数定义,不能被调用,包含纯虚函数的类称为抽象类或抽象基类。抽象类无法建立对象,只作为一种用作继承时的基类。如果抽象类派生出的新类还有未定义的纯虚函数,新派生类仍然是抽象类。只有所派生出的新类中已定义了基类的所有纯虚函数,这时所有虚函数就可以被调用,这样的派生类就不再是抽象类,可以用来建立具体的对象。虽然不能建立抽象类的对象,但可以定义抽象类的指针变量,用指针变量指向派生类对象,然后通过该指针调用虚函数,实现多态性的操作。下节讲述使用了抽象类的典型案例。计算机学院李卫明5.5.4 纯虚函数和抽象类程序设计中经常需要求各类形状物体的面积。样例Ex5.3定义了抽象形状基类CShape,具有求面积的纯虚函数GetArea,由CShape类派生出3个类:圆类CCircle、长方形类CRectangle和三角形类CTriangle。程序中定义了求面积和函数doubleTotalArea(constvector<CShape*>&),该函数用虚函数计算向量里各基类指针所指物体的面积并求和。样例程序动态生成各类对象,将对象指针保存在指针向量容器里,需要时可以调用求面积函数求容器里指针指向各类对象的正确面积,实现了动态多态性,达到了对象创建和对象使用的分离,即使将来需要增加新类型,如椭圆形,TotalArea函数也无需修改。程序执行结束时,指针向量容器vecShapes正常析构,容器对象析构时先执行容器里每个元素对象的析构,再释放本容器直接动态分配的内存空间。本例中容器里元素类型为普通指针,析构时并不会主动删除所指对象,需要显式遍历删除所指对象,由于基类析构函数是虚函数,所以可以动态调用各对象相应类的析构函数,完成资源的释放。如果配合第7章智能指针,本例中各对象的删除可以自动完成。具体代码参见样例。计算机学院李卫明5.6 典型范例——各类物体面积求和前面讨论都是公有继承,反映派生类对象也是一种基类对象,继承了基类对象的特征。除了公有继承,C++允许私有继承和保护继承,语法上只需将公有继承关键字改为private或protected即可,形式如下:class派生类名:private基类名{...};class派生类名:protected基类名{...};私有继承后,基类公有成员和保护成员在派生类里成为私有成员,也就是派生类对象不再具有基类对象所具有的功能,派生类对象不再是一种基类对象,不符合赋值兼容原则。保护继承后,基类公有成员和保护成员在派生类里成为保护成员,派生类对象不再具有基类对象所具有的功能,派生类对象也不再是一种基类对象,也不符合赋值兼容原则。计算机学院李卫明5.7 其它继承方式程序设计中,绝大部分继承是公有继承,私有继承和保护继承很少使用。C++规定,没有声明继承方式时,默认继承方式是私有继承。

Ex5.4是私有继承的样例,它通过私有继承整形双链表类list<int>实现了栈类,利用双链表类功能很方便地实现了栈类功能。注意,栈类对象不再是一个双链表对象,不具有双链表对象具有的功能。

具体代码参见样例EX5.4。计算机学院李卫明在第2章中,我们知道类的数据成员可以是其它类类型的对象,具有其它类类型,这个类类型和数据成员的类类型之间形成一种组合关系,数据成员对象是类类型主对象的一部分,也可以称为子对象,主对象和子对象具有整体和局部的关系,是一种有(has-a)关系,主对象功能可以通过子对象功能实现。本章讨论的公有继承关系,反映一种是(is-a)关系,派生类对象也是一种基类对象,具有基类对象的功能,派生类对象也具有基类对象部分。所以,反映是(is-a)关系时采用公有继承,反映有(has-a)关系时采用组合。私有继承、保护继承和组合间建议优先采用组合。计算机学院李卫明5.7.2 继承与组合如果一个派生类有两个或更多个基类,这种行为称为多继承。C++允许多继承,具有多继承的派生类的一般声明格式如下:class派生类名:继承方式1基类名1,继承方式2基类名2……{……//派生类新增的数据成员和成员函数};例如已声明了类A和类B,可按如下方式声明多继承的派生类C:classC:publicA,privateB{……//类C新增的数据成员和成员函数}派生类对象除具有派生类里描述的公有属性和公有函数描述的功能外,还继承了公有继承的各基类的公有属性和公有函数功能。例如,鸟类具有飞功能,马类具有奔跑的功能,飞马类公有继承了鸟类和马类,飞马类对象既具有飞的功能,又具有奔跑的功能。

计算机学院李卫明5.8 多继承继承时,派生类对象包含各基类部分对象和派生类数据成员子对象。建立派生类对象时,各基类部分对象和数据成员子对象的初始化通过派生类构造函数完成。多继承派生类的构造函数在参数初始化表中可包含多个基类构造函数,一般定义格式如下:派生类名(形参表):基类名1(基类构造函数参数表1),基类名2(基类构造函数参数表2),…,数据成员1(数据成员参数表),数据成员2(数据成员参数表2),…{……//函数体}构造函数中各基类构造顺序应该与声明派生类时继承顺序一致,派生类构造函数的执行顺序为:先调用各基类的构造函数,再执行各数据成员的构造或初始化,最后执行派生类构造函数的函数体。派生类对象撤销时会执行派生类析构函数,完成数据成员子对象和基类部分对象的扫尾处理。析构函数的执行顺序与构造函数的执行顺序正好相反。多继承在实际应用中并不普遍。计算机学院李卫明多继承在带来便利性的同时,也可能会带来一些问题。其中一个问题是二义性问题,比如,两个类都具有同名的公有成员函数,派生类对象从两个类都继承了这功能,调用时就会有二义性。解决二义性的办法是通过派生类对象调用时在函数名前加类名::指定调用某个类的成员函数,如样例Ex5.5所示,或在派生类中定义同名成员函数覆盖基类同名成员函数,在派生类同名成员函数里通过类名::指明调用哪个基类成员函数。

具体参见样例代码Ex5.5。计算机学院李卫明5.8.2 二义性问题解决办法多继承可能带来的另一个问题是信息冗余和不一致问题。比如,两个类从同一个类派生,而这两个类又共同派生出了一个新类,形成了一个菱形的继承关系。B类和C类都从A类派生,而D类通过多继承由B与C共同派生。如果类A有成员函数Show(),通过D的对象去访问A类的成员函数Show(),这时由于类B和类C都有继承来自于类A的Show()的副本,编译器无法确定使用哪个副本,将报错。同时,D的对象中存在2份A类对象部分,即A类数据成员有2份,造成信息冗余和信息不一致问题。计算机学院李卫明5.8.3 *虚继承虚继承是解决多义性问题的一种简便而有效的方法。虚继承由关键字virtual标识,一般语法格式如下:class派生类名:virtual继承方式基类名虚继承时的基类称为虚基类。虚基类不是在基类定义时声明,而是在声明派生类时,在继承方式前加关键字vitual声明。虚基类的最终子孙类对象只建立一份虚基类部分对象,虚基类部分对象构造方法在最终建立对象的子孙类构造函数中指明,忽略中间类构造函数里虚基类构造部分,中间类部分对象构造时将不再构造虚基类部分对象。这样将不会导致信息冗余和不一致问题,也不会出现多义性问题。如果在虚基类中定义有带参数的构造函数,并且参数没有默认值,而且没有定义无参构造函数,则在虚基类的直接派生类或间接派生类的构造函数的初始化表中都要对虚基类进行初始化,均应加上:虚基类构造函数名(参数表)计算机学院李卫明样例Ex5.6中描述了普通人类CPerson、研究生类CGraduate、教师类CTeacher、在职研究生CGraduateTeacher类,各类之间的继承关系如图5.3所示。CPerson具有姓名、年龄数据成员和分别设置、获取姓名、年龄的公有函数成员,CGraduate类具有学习某课程能力和显示已学课程列表功能,CTeacher类具有教授某课程和显示所有教授课程能力,CGraduate类和CTeacher类继承了CPerson类公有成员。CGraduateTeacher类共有继承CGraduate类、CTeacher类,CGraduateTeacher类对象研究生助教可以认为既是研究生,也是教师,具有研究生和教师的双重功能。具体代码参见Ex5.6。图5.3研究生助教类CGraduateTeacher菱形继承关系图计算机学院李卫明前面讲述了利用继承和多态性进行C++程序设计。本节深度探索派生类对象的内存分布、虚函数机制实现原理和特殊场合使用的运行时类型识别和动态类型转换。C++标准并未规定这些机制如何实现,探索这些机制的实现原理,有利于加深对C++程序执行机制的理解,设计出结构良好、高效的程序。计算机学院李卫明5.9 深度探索无论是公有继承、保护继承还是私有继承,派生类对象内部都包含了基类部分对象的数据成员和派生类新增数据成员,基类部分对象的数据成员本身也可能由祖先类部分对象的数据成员和基类本身数据成员组成。根据赋值兼容原则,基类指针可以指向派生类对象。下面探讨派生类对象的内存分布。计算机学院李卫明5.9.1 派生类对象的内存分布//Ex5.7A完整代码见样例classCBase{//成员省略};classCDerived:publicCbase{//函数省略};intmain(){CDerivedd;CBase*pb;CDerived*pd;pd=&d;pb=pd;cout<<reinterpret_cast<int>(pd)<<endl;//输出指针值

cout<<reinterpret_cast<int>(pb)<<endl;}计算机学院李卫明5.9.1.1 单继承派生类对象内存分布如图5.4所示,样例Ex5.7A派生类对象d内存包含基类部分对象的数据成员和派生类部分新增的数据成员,派生类指针变量pd指向派生类对象d,根据赋值兼容原则,可以将派生类对象指针pd赋值给基类对象指针pb,单继承时,2个指针变量值相同。图5.4单继承CDerived对象内存分布图计算机学院李卫明//Ex5.7B完整代码见样例classCBase1{//成员省略};classCBase2{//成员省略};classCDerived:publicCBase1,publicCBase2{//成员省略};intmain(){CDerivedd;CBase1*pb1;CBase2*pb2;CDerived*pd;pd=&d;pb1=pd;pb2=pd;cout<<reinterpret_cast<int>(pd)<<endl;//输出指针值

cout<<reinterpret_cast<int>(pb1)<<endl;cout<<reinterpret_cast<int>(pb2)<<endl;}计算机学院李卫明5.9.1.2 *多继承派生类对象内存分布如图5.5所示,派生类对象d内存包含2个基类部分对象的数据成员和派生类部分新增的数据成员,派生类指针变量pd指向派生类对象d,根据赋值兼容原则,可以将派生类对象指针pd赋值给基类对象指针pb1和基类对象指针pb2,多继承时,pb1、pd指针变量值相同,pb2指针变量值不同,比pb1、pd指针变量值大CBase1部分对象内存大小:4字节。这里,我们看到指针变量赋值不仅仅是数值的复制。注意,当派生类对象指针变量pd值为nullptr时,基类对象指针pb1和基类对象指针pb2值应该也是nullptr。图5.5多继承CDerived对象内存分布图计算机学院李卫明//Ex5.7C完整代码见样例classCCommonBase{//成员省略};classCBase1:virtualpublicCCommonBase{//成员省略};classCBase2:virtualpublicCCommonBase{//成员省略};classCDerived:publicCBase1,publicCBase2{//成员省略};intmain(){CDerivedd;CCommonBase*pcb;CBase1*pb1;CBase2*pb2;CDerived*pd;pd=&d;pb1=pd;pb2=pd;pcb=pd;cout<<reinterpret_cast<int>(pd)<<endl;//输出指针值

cout<<reinterpret_cast<int>(pcb)<<endl;cout<<reinterpret_cast<int>(pb1)<<endl;cout<<reinterpret_cast<int>(pb2)<<endl;}

计算机学院李卫明5.9.1.3 *虚继承派生类对象内存分布如图5.6所示,派生类对象d内存包含1份CCommonBase部分对象、2个基类CBase1、CBase2部分对象和派生类部分新增的数据成员。pd指向派生类对象d,根据赋值兼容原则,可以将pd赋值给pb1、pb2、pcb,pb1、pb2也需要能够访问CCommonBase部分对象,CBase1、CBase2基类部分对象包含公共基类部分对象的偏移或地址,pb1、pd指针变量值相同,pb2指针变量值不同,比pb1、pd指针变量值大CBase1部分对象内存大小:数据成员和偏移量共8字节,pcb指针变量值,比pb1、pd指针变量值大CBase1部分对象内存大小8字节、CBase2部分对象内存大小8字节和CDerived类新增数据成员大小4字节:共20字节。这里展示了指针变量赋值不仅仅是数值复制的更复杂案例。注意,当pd值为nullptr时,pb1、pb2、pcb值应该也是nullptr。图5.6虚继承CDerived对象内存分布图计算机学院李卫明C++中动态多态性通过虚函数体现,编译器一般是通过为每个具有虚函数的类建立虚函数表实现虚函数的。计算机学院李卫明5.9.2 虚函数实现原理虚函数体现了多态性.classCDog:classCAnimal{…};CAnimal*p=newCDog;p->Move();虚函数工作原理.V-表(VirtualTable)中包含虚函数地址.基类数据成员派生类数据成员对象内存分布图V-表如图5.7所示,基类CBase和派生类CDerived都具有虚函数。CBase类虚函数表包含2个指针项,指向CBase类f1、f2函数,CDerived类虚函数表包含3个指针项,指向CDerived类改写后的f1函数、继承CBase类的f2函数、CDerived类新增的f3函数,每个具有含虚函数类的对象都含有一个指针vptr,指向各自类的虚函数表,调用虚函数动态绑定时,根据对象里保存的vptr指针,查找所指虚函数表中对应函数项,执行所指函数即可实现动态多态性。虚函数表不含指向普通函数的指针。图5.7虚函数实现原理计算机学院李卫明完整代码见样例Ex5.8。

样例中基类CBase具有4个成员函数:普通成员函数g1、g2和虚成员函数f1、f2,派生类CDerived公有继承基类CBase,也有4个成员函数:普通成员函数g1、g3和虚成员函数f1、f3,其中g1、f1覆盖基类同名成员函数,g3、f3是派生类新增成员函数,样例中通过实际指向派生类对象的基类指针调用g1、g2和f1、f2时,g1、g2是普通成员函数,根据调用时指针类型静态绑定执行基类函数,f1、f2是虚成员函数,根据调用时指针所指实际对象动态绑定CDerived类的相应函数,CDerived类f1虚成员函数已覆盖基类f1虚成员函数,执行CDerived类f1虚成员函数,CDerived类继承了基类f2函数,执行CBase类f2函数。

计算机学院李卫明根据赋值兼容原则,派生类对象指针可以赋值给基类对象指针,内部所需转换是在编译时进行的,是静态转换。这样,同一类族的对象可以统一管理,再配合动态多态性统一使用。反过来,有时需要发挥其中某些派生类对象的派生类特有功能,这时,一般不可以使用编译时静态转换static_cast将基类对象指针转换成派生类对象指针,因为基类对象指针实际可能并非指向指定的派生类对象,这样的static_cast编译时静态转换不安全。计算机学院李卫明5.9.3 *运行时类型识别RTTI和动态类型转换dynamic_cast下面讨论建立在上节样例基础上。CBase*pb,baseObj;CDerived*pd,derivedObj;//CDerived公有继承baseObjpb=&baseObj;pd=static_cast<CDerived*>(pb);//不安全if(pd!=nullptr)pd->g3();pb=&derivedObjj;pd=static_cast<CDerived*>(pb);//可行if(pd!=nullptr)pd->g3();计算机学院李卫明C++提供了运行时类型识别RTTI(RunTimeTypeIdentification)和动态类型转换dynamic_cast机制用于解决这一特定需求。C++将含虚函数的类称为多态性类,不含虚函数的类称为非多态性类。每个多态性类具有像虚函数表一样的类型信息,编译器一般在多态性类的虚函数表中保存类型信息对象,每个多态性类的对象有指针指向虚函数表,因而可获得的对象的类型信息。C++程序可以通过通过下述2种语句获得类型信息,获得的类型信息为type_info类型对象的常引用,type_info类型对象可以进行相等比较和通过成员函数返回类型信息文字描述,使用时需包含头文件<typeinfo>:consttype_info&tiObj=typeid(表达式);consttype_info&tiObj=typeid(类型);当表达式结果为非多态性类时,编译器编译时就可以获取类型信息,一般也无此必要;当表达式结果为多态性类对象引用或指针时,上述语句返回相应多态性类的虚函数表中类型信息对象的常引用,为多态性类提供了运行时类型识别RTTI支持。计算机学院李卫明C++通过运行时类型识别RTTI支持,进一步支持运行时动态类型转换dynamic_cast:程序运行过程中,遇到dynamic_cast基类指针转换时,查询基类指针所指对象的实际类型信息,基类指针如果实际指向派生类对象,就可以通过dynamic_cast安全转换为派生类指针,基类指针如果实际指向的不是派生类对象,dynamic_cast转换返回空指针。下述语句段可以正常获得预期结果:CBase*pb,baseObj;CDerived*pd,derivedObj;//CDerived公有继承baseObjpb=&baseObj;pd=dynamic_cast<CDerived*>(pb);//安全,实际返回空指针if(pd!=nullptr)pd->g3();pb=&derivedObjj;pd=dynamic_cast<CDerived*>(pb);//可行,实际返回派生类对象指针if(pd!=nullptr)pd->g3();计算机学院李卫明动态类型转换dynamic_cast同样适用于基类对象引用。程序运行过程中,遇到dynamic_cast基类引用转换时,查询基类引用对象的实际类型信息,基类引用的实际是派生类对象,就可以通过dynamic_cast安全转换为派生类对象引用,

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论