版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
6.1继承的概念
6.2抽象和分类
6.3继承的实现
6.4继承中的成员重定义
6.5继承下的类域
6.6组合的概念
6.7派生类和组合类对象的构造
6.8模板类的继承关系
6.9基类和派生类的指针
本章要点
练习
对象就是一件事物,类就是针对一些事物共性的概括。不同的类经常具有相似的特征。为了充分利用对象之间的关系,就引进了继承的概念。继承(inheritance)是允许通过重用现有类来构建新类的特性。
我们来看一下实际生活中的继承,如图6-1所示。假如已经定义了“人”,在需要说明什么是“教师”时,只需在“人”的基础上增加“教书”这一行为即可,即教书的人,而不必从头说明什么是人。若想进一步说明“男教师”,只需在“教师”的特征中增加“男性”这一特征即可。6.1继 承 的 概 念图6-1继承关系图
C++中的继承是在现有的类的基础上建立一个新类。原类称为“基类”、“父类”或“超类”,新建类称为“派生类”或“子类”。子类继承父类的所有行为和特征。
在图6-1中的人是学生的基类,学生是大学生的基类。或者说,学生是人的派生类,大学生是学生的派生类。继承关系中的大学生有两个基类,学生是它的直接基类,人是它的间接基类。在对事物的描述中,正确地使用继承与组合能带来层次分明、结构清晰的效果,这就要求我们对具体问题进行具体分析和研究,进而进行抽象和分类。
抽象是要找出事物的共同特性和方法,是具体事物描述的一个概括。抽象的目的就是能实现层层继承。分类就是要找出事物实体的各自特性和方法。它使概念逐渐细化,即具体化。分类的目的就是能实现对类的正确描述。下面通过图6-2中对交通工具的描述,来表现抽象和分类的关系。其中,从上向下是分类的过程,从下往上是抽象的过程。6.2抽 象 和 分 类图6-2抽象和分类的关系示意从分类的角度看,交通工具类可分为轮船、飞机和车,车又分为汽车和人力车。其中汽车的行为是可运输,特征是燃油和有车轮。汽车又可分为卡车和小汽车等。卡车的特征是载货,小汽车的特征是载人。
从抽象的角度看,在对小轿车类的描述中,展现出所有小轿车都有一个共性:体积小,可载人。对小轿车进行抽象可得到:小轿车是小汽车中的一种,它可以继承小汽车,它自身的特征是比小汽车更舒适、速度更快等。面向对象的程序设计中,对象被抽象成类,类又是层层分解的,这些类与派生类的关系可以被规格化描述。描述类后再描述其派生类,就可以只描述其增加部分。所有派生类层次上的编程,都只需在已有类的基础上进行。派生类继承基类定义的所有成员并添加自己的独特成员从而形成一个新类,这样使得各类之间既有共性又有个性,使各类看似相似却又是各个独立的类。
进行抽象和分类的方法主要从行为和特性两方面考虑,从中找出个性与共性,以及它们之间的关系。这部分工作的好坏直接关系到程序的优劣,因而要求在实践中不断总结和积累经验。6.3.1建立继承的方法
在继承关系中,派生类继承基类定义的所有成员并添加自己的独有成员。通过继承可以实现类库的重用性,有效地利用现有的类,既使程序结构合理、层次清晰,对对象的描述符合实际,又可缩短程序的开发时间。
建立继承的方法是:
class派生类名:(冒号)引用权限类派生表6.3继 承 的 实 现其中,引用权限有public、protected和private。若不指定引用权限,则为默认的方式,即private。类派生表就是指定的基类。指定的基类必须首先定义,方可被指定。
如对于上面提到的小轿车继承小汽车,可以用下面的方法实现。先描述基类(小汽车类):
classCar //小汽车类
{protected:
intsize;
public:
Car();
voidTake(); //乘坐
};然后描述派生类,并建立继承关系。在描述派生类中只要添加新增部分:
classMinbus:publicCar //小轿车,公有继承小汽车类
{protected:
intspeed;
public:
Minbus();
voidShow(); //作秀
};上面完成的两个步骤,就实现了图6-3所示的继承关系。在声明派生类Minbus类时指定一个Car类为它的基类,于是小轿车继承了小汽车,或者说小轿车类是小汽车的派生类。有了这个继承关系,小轿车就具有了小汽车的所有行为和特征。通过上面的描述,当我们要说什么是小轿车时,不用先说什么是小汽车,而只需说小轿车就是小汽车加上漂亮和快速。通过继承,我们可以实现一个类对另一个类的特征和方法的利用,C++中称之为可重用性。因此我们在进行抽象与分类时要考虑到信息的重用和扩展。重用现有的代码可节约时间和资金,并提高可读性。扩展就是增加了新的内容。
继承可以使已存在的类不需修改地适应新应用。从一定意义上讲,继承是C++面向对象编程的一个基石,是C++的精华部分。如果不存在继承,各种对象就是相互独立的,就不能通过类来充分而简略地描述各种对象。图6-3继承示意继承可以使已存在的类不需修改地适应新应用。从一定意义上讲,继承是C++面向对象编程的一个基石,是C++的精华部分。如果不存在继承,各种对象就是相互独立的,就不能通过类来充分而简略地描述各种对象。
继承关系必须合理,符合实际生活要求,即符合客观实际,否则将闹出笑话且无法完成程序设计。良好的继承关系,必须经过细致分析,使其层次分明、结构合理。要达到该要求,就要进行抽象和分类。在面向对象应用程序开发中,抽象和分类是十分重要的环节,也是衡量程序优劣的重要标准。6.3.2成员之间的关系
在继承中基类对象与派生类对象之间存在着既有联系,又相互独立的关系。独立性表现为,基类对象与派生类对象是两个不同的对象。联系性表现为,基类的所有非静态数据成员都被拷贝到派生类对象中生成一个副本。也就是说,派生类对象实际上是由子对象,即由基类对象数据成员的拷贝和派生类中新增的非静态数据成员这两部分组成的。基类的成员函数不在派生类中作成员函数的拷贝,基类的成员函数与派生类共享,反之不成立。
【例6-1】该例说明继承关系的实现以及成员的访问方法。
#include<iostream>
#include<string>
usingnamespacestd;
classStudent
{private:
floatcredit; //学分
protected:
floathigh;intweight; //身高、体重public:
stringname; //姓名
Student(string="noname");
voidSet(float,int,float);//设置学分、身高、体重
voidDisplay();
voidCourse(float); //总分
};classGraduateStudent:publicStudent
{protected:
floatsum;
//总学分
public:
GraduateStudent(float=5.0);
voidAdvance(float,float); //累积学分
voidShow(); //得到学分
};Student::Student(strings)
{name=s;high=0;weight=0;credit=0;}
voidStudent::Set(floath,intw,floatc)
{high=h;weight=w;credit=c;}
voidStudent::Display()
{cout<<"name\t"<<"high\t"<<"weight\t"<<"credit\t"<<endl;
cout<<name<<'\t'<<high<<'\t'<<weight<<'\t'<<credit<<endl;
}
voidStudent::Course(floatx){credit+=x;}GraduateStudent::GraduateStudent(floatave){sum=ave;}
voidGraduateStudent::Advance(floatgrade,floatresult)
{sum+=grade*result;}
voidGraduateStudent::Show()
{cout<<"name\t"<<"high\t"<<"weight\t"<<"sum\t"<<endl;
cout<<name<<'\t'<<high<<'\t'<<weight<<'\t'<<sum<<endl;
}voidmain()
{GraduateStudentgs; //定义派生类对象gs
gs.Set(1.82f,65,4.3f); //设置credit、high和weight
gs.Display(); //调用基类的成员函数
gs.Advance(3,0.8f); //调用派生类的成员函数
gs.Show(); //调用派生类的成员函数
Studentstud("Jones");
}运行结果:
name high weight credit
noname 1.82 65 4.3 //学生的姓名、身高和学分
name high weight sum
noname 1.82 65 7.4 //研究生的姓名、身高和
学分
在本例中我们定义了学生类对象stud和研究生类对象gs。gs是学生类的公有派生类对象,它继承了基类学生类。派生类对象gs实际上是由两个子对象,即基类中的被继承部分和派生类中新增的部分组成的。继承关系中各成员之间的关系如图6-4所示。图6-4成员函数与数据成员的关系图6-4描述了继承关系中对象stud和对象gs在内存中的分配情况。它共有四个部分:基类对象stud;派生类对象gs;基类的成员函数;派生类中新增的成员函数。
派生类对象gs由基类对象数据成员和派生类对象中新增的非静态数据组成,基类的成员函数为基类和派生类共享,这也就是说派生类中同时也拥有了基类的成员函数。而派生类中的成员函数仅派生类独享,基类不能拥有它。
stud对象与gs对象是两个独立的对象。gs继承了stud,因此在gs中拥有了一份基类对象数据成员的拷贝。stud中的name等和gs中的name等分别属于不同的对象。
综上所述,基类的非静态数据成员在派生类对象中都有一份拷贝,生成副本,即派生类中包含基类对象数据成员。基类的成员函数不在派生类中生成副本,它们是共享的。也就是说派生类的公有接口,由基类的公有接口加派生类的公有接口组成。6.3.3访问成员的方法
在继承关系中访问成员,可以通过基类成员函数和派生类成员函数来实现,两者访问的成员有所不同。
1.基类成员函数
在继承关系中,基类的成员函数为派生类共享,它能访问基类的数据成员和被拷贝到派生类中的数据成员。为了能区分它们各自访问的数据成员,就要有对象名作引导。在例6-1中,用研究生类对象gs引导调用了基类成员,实现了对派生类数据成员的操作。例6-1中的gs.Set(4.3f,1.82f,65)只能访问图6-4所示的派生类对象gs中的数据成员,若要访问基类对象中的数据成员,就要定义基类对象stud,然后由stud引导调用基类成员函数。也可以将例6-1主函数改写如下:
voidmain()
{GraduateStudentgs; //定义派生类对象gs
gs.Set(4.3f,1.82f,65);
//调用基类的成员函数,设置派生类对象
gs.Display();
//调用基类的成员函数,显示派生类对象
gs.Advance(3,0.8f); //调用派生类的成员函数,累积学分
gs.Show(); //调用派生类的成员函数,显示派生类对象
Studentstud(“Jones”); //定义基类对象stud
stud.Set(4.5f,1.78f,63);
//调用基类的成员函数,设置基类对象
stud.Display(); //调用基类的成员函数,显示基类对象
}运行结果:
name
high
weight credit
noname1.82
65 4.3
//研究生的姓名、身高和学分
name high weight sum
noname 1.82 65 7.4
name high weight credit
Jones
1.78 63 4.5
//学生的姓名、身高和学分从运行结果可见,同样的Set和Display函数访问的数据成员是不同的,它通过对象名来区分。
研究生类中得到了其基类(学生类)中数据成员的拷贝。通过派生类对象gs引导,既可调用基类(学生类的成员函数访问被拷贝为派生类中的基类数据成员),也可调用派生类(研究生类的成员函数访问新增数据成员),即研究生既有学生的行为也有研究生的行为。
2.派生类成员函数
派生类成员函数可以访问被继承到了派生类中的原基类的公有和保护成员,但是不可以访问被继承到了派生类中的基类的私有成员。例如,派生类的成员函数Show()不能访问在图6-4中派生类的credit数据成员,只有基类的成员函数才可访问。派生类成员函数可以访问派生类的数据成员(包括被继承和新增的数据成员)。
3.单继承中的访问控制
在继承关系中的访问控制如图6-5所示。该图表示出了对于基类而言的一般用户、派生类的成员和友元以及自身的成员和友元的访问权限;同时也指出,若要想使得派生类的成员函数也能访问到基类的私有成员,可以将派生类声明为基类的友类。图6-5基类成员在派生类中的访问控制6.4.1概念与方法
在继承中,需要新增的行为可以通过在派生类中添加成员函数来实现。在派生类中若新增的函数与基类中的成员函数同名,这种现象就称为成员函数的重定义。成员函数的重定义是指在指定语义上重新修改继承成员函数的实现。在派生类中重定义成员函数之后,基类中的同名函数就被屏蔽了。
在例6-1中,基类Student中有一个Display函数,现在要在派生类GraduateStudent中添加一个和基类相同的函数Display,见例6-2。6.4继承中的成员重定义
【例6-2】演示不改变基类代码,在派生类中通过重定义为该函数赋予新的功能。
voidGraduateStudent::Display()
{cout<<“achievementis:”<<sum<<endl;}
那么在主函数不变的情况下,运行结果为:
achievementis:5 //初始化值
name
high
weight sum
noname 1.82
45 7.4
//研究生的姓名、身高和学分从运行结果来看,gs.Display();调用的是派生类对象研究生的成员函数Display。我们并没有改变基类代码,而是在派生类中通过重定义使该函数实现新的功能,也就改变了运行结果。它也给了我们一个启发,假定我们不再需要基类的Display,可在派生类中将它重
定义为空函数。这样,用派生类名引导的gs.Display()就访问不到基类的Display了,这相当于把它屏蔽掉了,即在派生类中一旦赋予了与基类同名的函数新的功能,该基类函数就被屏蔽。那么当我们不希望基类中的某函数被用户访问时,能否不赋予某功能而将其屏蔽呢?
【例6-3】演示通过在私有区重定义空函数来实现屏蔽。若要在派生关系中实现屏蔽,只将要屏蔽的基类成员在派生类的私有区重定义便可。
#include<iostream>
usingnamespacestd;
classBaseone
{protected:
intone;
public:
voidDisplay()
{cout<<"Displayone"<<endl;}
};classBasetwo:publicBaseone //公有继承
{private:
voidDisplay(){} //在私有区重定义,屏蔽
public:
voidShow()
{cout<<"Displaytwo"<<endl;}
};
voidmain()
{Basetwobtwo;btwo.Show(); //输出Displaytwo
//btwo.Display(); //非法
}此处,用btwo.Display()访问就会出错,也不能再去调用基类的Display,仿佛不存在Display一样。假如去掉派生类中的重定义,就能调用基类的Display。
继承中可以通过成员的重定义来实现访问控制或赋予函数新的功能。有了重定义,我们就可以达到两个目的:第一,可以不改变基类,而在派生类中通过重定义为该函数赋予新的功能;第二,对于不需要继承的功能,可以通过重定义成员函数予以屏蔽。6.4.2成员重定义后的访问
在派生类对象中,虽然有基类的私有成员的拷贝,但派生类成员函数不能访问基类的私有成员,只有基类成员函数才可访问。在此我们介绍如何在派生类成员函数内部和外部,访问保护或公有的基类数据成员。
派生类中的被继承保护或公有成员有两种情况:一种是未作任何处理的;另一种是被重定义而屏蔽,即不可见的。前者用对象名引导访问;后者要用基类名来区分:基类名::成员名。
【例6-4】通过下面的例子介绍如何访问被屏蔽的基类中的保护或公有成员。
在本例中,基类Student类中有公有数据成员credit,在派生类中又对它作了重定义,因而被屏蔽。基类中的公有成员函数Display在派生类中也作了重定义,也被屏蔽。这里介绍两种方法,即在派生类的成员函数内和成员函数外的访问方法。
派生类的Display中提供了在成员函数中访问被屏蔽的基类的公有数据成员credit的方法。在main函数中提供了在非成员函数中访问被屏蔽的基类的公有数据成员credit和成员函数Display的方法。#include<iostream>
#include<string>
usingnamespacestd;
classStudent
{private:
inthigh; //身高
floatweight;
//体重public:
intcredit; //学分,公有
stringname; //姓名,公有
Student(){name="noname";weight=0;high=0;credit=0;}
voidDisplay(){cout<<"high"<<high<<"weight"<<weight<<endl;}
};classGraduateStudent:publicStudent
{protected:
floatsum; //总学分
public:
intcredit; //学分,数据成员重定义
GraduateStudent(){sum=0;credit=0;}
voidAdvance(intgrade,floatresult)
//累积学分
{sum+=grade*result;}
voidDisplay() //成员函数重定义 {cout<<"sumis:"<<sum<<endl;
cout<<"Studentcreditis"<<Student::credit<<endl; //在成员函数中的访问法
cout<<"GraduateStudentcreditis"<<credit<<endl;
}
};
voidmain()
{GraduateStudenta;a.credit=10; //GraduateStudent::credit
a.Student::credit=5; //Student::credit
=“Jonas”; //Student::name
a.Advance(3,0.8f);
cout<<“GraduateStudent:”<<<<endl;
a.Display(); //调用研究生的Display
cout<<“Student:”<<a.Student::name<<endl;
//它未被屏蔽,用结果不变
a.Student::Display(); //调用学生的Display,使被屏蔽者重现
}运行结果:
GraduateStudent:Jonas
sumis:2.4
Studentcreditis5
GraduateStudentcreditis10
Student:Jonas
high0weight0
被继承部分highweightcredit被屏蔽name新增部分sumcredit重定义运行结果:
GraduateStudent:Jonas
sumis:2.4
Studentcreditis5
GraduateStudentcreditis10
Student:Jonas
high0weight0
本例中成员的关系如图6-6所示。在派生类中重定义了credit和Display,因此就将原继承到的credit和Display屏蔽,使之不可见。用a.credit和a.Display()都是访问重定义的成员,若要访问被屏蔽的成员就要用类名来引导,如a.Student::credit和a.Student::Display()。图6-6成员的关系若在派生类成员函数内访问数据成员,就不需要用对象名来引导。例如在派生类成员函数Display内访问重定义的成员credit,可以直接用credit或用GraduateStudent::credit,访问被屏蔽的成员可用Student::credit。在继承下,派生类域被嵌套在直接基类域中,如图6-7所示。在这种嵌套情况下,若一个名字在派生类域中没有找到,则编译器会在基类域中查找该名字的定义。6.5继承下的类域图6-7类域关系6.5.1重定义与重载的区别
需要说明的是,成员的重定义和重载是两个不同的概念。其区别主要表现在以下几个方面:
(1)方法不同。重载是定义同名不同参数函数,其中包括参数个数、类型或顺序不同;重定义是在派生类中重新定义一个同名函数。
(2)产生的条件不同。重载可以在普通函数之间发生,重定义必须在继承关系中有效。重载的名字只能在同一类域中并且可见才能生效,而重定义只能在派生类中有效。一般来说,同一作用域中可实现重载,嵌套的作用域中可实现重定义。重载,只能是函数的重载。重定义不仅可以作用于函数,而且可以作用于数据成员。
(3)作用方式不同。重载是在同一作用域中相同的可见函数名之间发生,而重定义是在嵌套的作用域中的某一名字将与其同名成员屏蔽而不可见。
(4)执行过程不同,函数匹配关系不同。重载只在当前域中寻找函数匹配,若不存在该函数,则出错。重定义时首先在当前类域中选择同名的函数匹配,若不存在该函数,则在外层域,如基类域中寻找函数匹配。若都不存在该函数,则出错。
【例6-5】演示函数重定义与重载的区别。
#include<iostream>
usingnamespacestd;
classStudent
{private:
floatno; //学号
public:
Student(floatx=0){no=x;}
voidDisplay()
{cout<<"no="<<no<<endl;}
};classGraduateStudent:publicStudent
{protected:
floatscore; //平均学分,数据成员重定义
public:
GraduateStudent(floatave=0.0)
{score=ave;}
voidDisplay(intx)
{cout<<"score="<<score<<endl;}};
voidmain()
{GraduateStudenta; //定义派生类对象a
a.Display(); //出错,不能匹配基类的Display
a.Display(66); //调用派生类的Display
}观察本例的代码,研究生类继承学生类,学生类中有成员Display函数,研究生类中也有Display函数,这就是重定义。即只要研究生类中有同名的Display函数,不论它有几个或没有参数,都是重定义,不是重载。若在研究生类中同时有Display(intx)和Display(),则这两个函数是重载。
假如去掉派生类中的Display(),那么派生类中的Display(intx)与基类中的Display()也是不能重载的。假如此时对于派生类的对象a发生a.Display();的调用,就会发生少给出一个参数的错误。这也就是我们为什么要称其为重定义(overriding),而不是重载(overloading)的原因。6.5.2用using消除屏蔽
派生类域被嵌套在直接基类域中,因此基类的Display是被重定义而不可见的,通过使用using可以将已被屏蔽的名字可见。可在例6-4中添加usingStudent::Display;,使其可见,从而实现重载。…
public:
usingStudent::Display;
…
voidmain()
{GraduateStudenta; //定义派生类对象a
a.Display(); //ok,匹配基类的Display
a.Display(66); //调用派生类的Display
}一个类以另一个类的对象、引用或指针作为其数据成员,我们称之为组合(composition)类。通过组合可将其他类的特征融入本类的特征中。
在组合关系中常见的方式有两种:按值组合和按引用组合。按值组合就是类的实际对象被声明为一个成员。如我们前面的例题中常出现的strings就是按值组合的一个事例。按引用组合就是通过对象的引用或指针间接操作一个对象。6.6组 合 的 概 念若有人类、教师类,又有教具类。教师上课时可以使用教具,也就是说教师有教具。我们可以将教具类作为教师类的数据成员。教师是人,教师类可继承人类。例如:
classTeachTool
{protected:
intsize;
floatweight;
public:
voidhelp();
};classTeacher:publicman //教师类继承了人类
{protected:
charname[40];
TeachTooltool; //教师类组合了教具类
public:
voidTeaching();
};这样我们将教具类的特征融入了教师类中,那么教师也有了教具的特征,可以表现为教具的方式,如通过形体语言来提高教学效果。
在继承关系中我们要求,各继承关系要符合客观实际,同样在组合中也要求组合关系必须符合客观实际。为了能使关系的描述也贴近实际,在继承与组合中,用“是”与“有”来描述。在这个组合中我们可以用“有”的关系来描述,我们称教师“有”教具。要注意,是教师类组合了教具类,而不是教具类组合了教师类,不能说教具“有”教师。因此,类以另一个类对象作为数据成员,称为组合。组合关系就是“有”的关系。6.6.1描述关系
建立了继承与组合的概念之后,我们自然而然想到,在什么情况下用继承,什么情况下用组合。若能用“有”和“是”的关系来描述事物,问题就解决了。如现在有三个类:教师类、教具类和人类。教师属于人的范畴,则可用教师类继承人类。描述方法就是,教师“是”人。教师使用教具,则可将教具类组合到教师类。描述方法就是,教师“有”教具。反之说教师类继承教具类,或者说教师类组合人类,都是错误的。教师不是教具,教师中有人,难到还有非人教师吗?若将人类组合教师类,就是说人都有教师特征,也不行。通过上面的分析,我们说,只有当继承与组合的关系描述符合客观实际时,才是正确的。也就是说,用“有”和“是”的关系来描述是合理的,这样的继承与组合的关系才有意义。
当然在某些情况下,就要由程序员根据着重点来区分。例如有教师类,现要建立一个专家类。专家可以继承教师,描述为专家是教师;也可以不用继承的方法,而用组合,把教师类作为专家类的数据成员,专家也就有了教师的特性,也就可以说专家中“有”教师。6.6.2数据成员关系
一个类可以是另一个类的派生类,也可以将某一类的对象作为自己的数据成员,使自己也具有某类的特征。在继承和组合过程中,基类的成员函数是共享的,在派生类中将得到基类和组合类的数据成员的拷贝。由于在继承与组合关系中既有共享的成员函数,又有数据成员的拷贝,因此我们在掌握它们的调用和访问方法的同时,也要避免一些特征的重复,如姓名的重复等。下面说明继承和组合关系同时存在时各成员的访问方法。
【例6-6】本例说明继承和组合中数据成员的拷贝及访问方法。
#include<iostream>
#include<string>
usingnamespacestd;
classAdvisor //导师
{protected:
stringname;public:
Advisor(strings="Sandy")
{name=s;}
voidADisplay()
{cout<<"Advisorname"<<name<<endl;} //输出姓名
};
classStudent{protected:
stringname;
//姓名
public:
Student(strings=“John”)
{name=s;}
voidSetname(strings=“noname”)
{name=s;}
voidDisplay()
{cout<<“Studentname”<<name<<endl;}
//输出姓名
};classGraduateStudent:publicStudent
//研究生类继承学生类,组合了导师类
{protected:
Advisoradvisor;
stringname;public:
GraduateStudent(strings="noname")
{name=s;}
voidGsDisplay()
{cout<<"GraduateStudentname"<<name<<endl;
cout<<"Studentname"<<Student::name<<endl;
advisor.ADisplay(); //显示导师姓名
}
};voidmain()
{Studentst(“Lolee”);
//创建Student对象st
GraduateStudentgs("Jonas");//创建GraduateStudent对象gs
Advisoras("Java"); //创建Advisor对象as
gs.Setname("Huanghua"); //设置Student姓名
st.Display(); //显示学生姓名Lolee
gs.GsDisplay(); //显示研究生姓名Jonas和导师姓名Sandy
gs.Display(); //显示研究生中的学生姓名Huanghua
}运行结果:
StudentnameLolee
GraduateStudentnameJonas
StudentnameHuanghua
AdvisornameSandy
StudentnameHuanghua 本例中,研究生继承了学生,组合了导师。研究生既有学生行为,也有导师特征。研究生“是”学生,研究生中“有”导师,也就是具有可辅导特征。这是符合常理的。由于导师对象是研究生的保护成员,因此对导师的访问只能用研究生的成员函数来实施。
例6-6中各对象的数据成员在内存中的空间分配如图6-8所示。在内存中共开辟有三个空间,它们分属于st、as和gs三个对象。用研究生对象作引导的各个显示函数都作用在gs空间的数据成员上。在研究生中有学生、导师和自己三个姓名,这样的继承不是一个好方法,因为一个研究生同时有三个名字不合常理。所以说在使用继承与组合时要考虑到数据成员的冗余。图6-8各对象存储关系6.7.1概念与方法
在继承与组合关系中,类的构造次序是:先构造基类,再构造组合类,然后构造派生类。类构造离不开构造函数,那么就必须能使各个构造函数工作,我们称之为激活构造函数。同时,我们也要考虑如何进行参数传递。6.7派生类和组合类对象的构造对于基类的构造可以用默认构造函数(称为隐式),也可以指定参数(称为显式)。当需要指定基类构造函数的参数时,在派生类构造函数后加“:基类名(参数表)”。
对于组合类的构造可以用默认构造函数,也可以指定参数。当需要指定组合类构造函数的参数时,在派生类构造函数后加“:对象名(参数表)”。6.7.2构造与激活
在构造派生类时,必须激活基类构造函数。激活方式可以是直接给出基类参数,由派生类的构造函数传出参数给基类构造函数,也可让基类以默认参数构造。
在以前介绍的继承关系中,未讨论如何激活基类构造函数,而都是以默认参数的方法激活基类构造函数的。
1.派生类对象的构造
激活基类构造函数的方法是:在派生类的构造函数中增加被传递的参数,然后在派生类构造函数形参列表的圆括号后加冒号以及基类名和形参列表。【例6-7】通过派生类构造函数传递参数。
#include<iostream>
#include<string>
usingnamespacestd;
classStud
{private:
stringname;
charsex;
protected:
intnum;public:
Stud(intn=982,strings="Wanggang",charc='M')
{name=s;num=n;sex=c;}
voidDisplay()
{cout<<"num:"<<num<<"\t"<<"name:"<<name<<"\t"<<"sex:"<<sex<<endl;}
};
classStudent:publicStud //派生类,派生于Stud{private:
intage;
stringaddr;
public:
Student(intn,stringnam,charc,intag,stringad)
:Stud(n,nam,c)
//在构造基类时用n、nam、c初始化Stud
{age=ag;addr=ad;}
voidShow() {cout<<“num:”<<num<<“\t”; cout<<“age:”<<age<<“\t”<<“address:”<<addr<<endl;
Display(); //调用基类Display
}
};
voidmain()
{Studa;
a.Display();
Studentb(994,“Lolee”,‘W’,26,“shanghai”);
//先构造基类Stud,再构造派生类
b.Show();
b.Display();
}运行结果:
num:982name:Wanggangsex:M
num:994age:26adress:shanghai
num:994name:Loleesex:W
num:994name:Loleesex:W
2.派生类中组合新特征对象的构造
一个类可以将另一个类的对象作为自己的数据成员,使自己拥有新特征,派生类也是如此。在构造派生类组合类对象时同样要求激活组合类构造函数。对于类中具有常量的数据成员,必须初始化。
组合类的激活方式与激活基类的方式相同,若既有继承又有组合时要用逗号分隔。若在派生类中还有需要初始化的成员,可以再用逗号分隔,然后加上成员名和参数。在C++中,将构造函数后的“:”和用“,”分开的成员名及其初始值的列表称为成员初始化表。成员初始化表只能在构造函数定义中被指定,而不是在声明中指定。该表被放在参数表和构造函数体之间。
【例6-8】说明如何构造派生类中组合的新成员的对象。在派生类中组合了导师Advisor类,也组合了const的成员ID。对于常量、变量成员,必须在构造时完成初始化。#include<iostream>
#include<string>
usingnamespacestd;
classAdvisor //导师类
{protected:
charsex;public:
Advisor(charadv=‘m’)
{sex=adv;cout<<“ConstructAdvisor\n”;}
voidDisplay()
{cout<<“Advisorsexis”<<sex<<endl;}
};
classStudent
//学生类
{protected:
stringname;public:
Student(strings=“noname”)
{name=s;cout<<“ConstructStudent\n”;}
voidDisplay()
{cout<<“Studentnameis”<<name<<endl;}
};
classGraduateStudent:publicStudent
//研究生类继承了学生类,组合了导师类
{protected:
Advisoradvisor; //组合了导师类
constintID;
//const的成员
floatsalary;public:
GraduateStudent(strings,charadv,floatsal)
:Student(s),advisor(adv),ID(320)
//多个成员的初始化值之间用逗号分隔
{salary=sal;cout<<"ConstructGraduateStudent\n";}
voidDisplay()
{cout<<"ID="<<ID<<'\t'<<"GraduateStudentnameis"<<name
<<'\t'<<"Salaryis:"<<salary<<endl;
advisor.Display(); //在成员函数中访问保护成员advisor
}
};voidmain()
{Advisoras;as.Display();Studentds("Lolee");
ds.Display();cout<<"\n\n";
GraduateStudentgs("Lihua",'w',3500);
gs.Display();
}运行结果:
ConstructAdvisor
Advisorsexism
ConstructStudent
StudentnameisLolee
ConstructStudent
ConstructAdvisor
ConstructGraduateStudent
ID:320GraduateStudentnameisLihuaSalaryis:3500
Advisorsexisw在派生类研究生类GraduateStudent的构造函数后用:Student(s),意为用s作为参数去初始化在派生类中得到的一份学生类Student数据成员的拷贝。而advisor(adv)意为以adv的值来初始化组合在研究生类中的导师类advisor的对象。
【例6-9】演示在派生类中如何初始化基类中的const数据成员。#include<iostream>
#include<string>
usingnamespacestd;
classStudent
//学生类
{protected:
stringname; //姓名
constintid; //const的成员public:
Student(strings=“noname”,intx=738)
//用x初始化基类中的const数据成员
:id(x)
{name=s;cout<<"ConstructStudent\n";}
voidDisplay()
{cout<<id<<'\t'<<name<<'\t'<<endl;}
};classGraduateStudent:publicStudent
{protected:
constintID; //const的成员
floatsalary;
stringname; //姓名public:
GraduateStudent(stringss,floatsal,inty,stringsg)
:Student(ss,y),ID(320),salary(sal) //salary的初始化
{name=sg;cout<<"ConstructGraduateStudent\n";}
voidDisplay()
{cout<<"ID="<<ID<<'\t'<<name<<'\t'<<salary<<endl;
cout<<"id="<<id<<'\t'<<Student::name<<'\t'<<salary<<endl;
}
};voidmain()
{Studentds("Lolee");
ds.Display();
cout<<endl;
GraduateStudentgs("Lihua",3500,388,"Jonas");
gs.Display();
}运行结果:
ConstructStudent
738 Lolee
ConstructStudent
ConstructGraduateStudent
ID=320 Jonas 3500
Id=388 Lihua 3500
观察本例可以发现,在研究生对象的特征中既有学生的姓名,也有研究生的姓名,即一个学生有两个名字,作为学生是Lihua,作为研究生是Jonas,所以这方法不好。可以把研究生的姓名去掉,这个请读者自己完成。模板是一种高效、安全和实用的重用代码方式。它实现了类型参数化,在创建对象或函数时所传递的类型参数决定了其性质。模板类的派生类与普通类一样,有公有、保护和私有三种,继承成员的访问控制规则也一样。
所谓模板,就是一个框架,一个还没有决定成员中参数类型的框架。那么在模板类的继承关系中,不仅要给出派生类中的类型参数,而且要给出基类中的类型参数。
【例6-10】演示在派生类中给出类型参数的方法。6.8模板类的继承关系#include<iostream>
#include<string>
usingnamespacestd;
template<classType>
classBase //定义了类模板,类名是Base
{public:
voidShow(Typeb)
{cout<<b<<"\n";}
};template<classType1,classType2>
classDerived
:publicBase<Type2>
//指定Derived是Base<Type2>的公有派生类
{public:
voidSHow(Type1d1,Type2d2)
{cout<<d1<<""<<d2<<endl;}
};voidmain()
{Derived<char*,float>objA;
objA.SHow("Piis:",3.14159f); //调用成员函数
Derived<char*,char>objB;
objB.SHow("Areyousure",'?'); //调用成员函数
objB.Show(89); //给出ASCII码值
//obj.SHow("Youareright"); //出错,参数太少
Derived<string,char>objC;
objC.SHow("Godsaveme",'?');
}运行结果:
Piis:3.14159
Areyousure?
Y
Godsaveme?若在例6-10的派生类Derived中,将SHow函数名也命名为Show,那么派生类中的SHow就与基类中的Show同名,这就是重定义。在派生类中只有用基类名来引导以示区分,才能访问基类中的Show,此时对上例的main函数修改如下:
voidmain()
{Derived<char*,float>obj;
obj.Show("Piis:",3.14159f); //调用基类成员函数
Derived<char*,char>OBj;
OBj.Show("Areyousure",'?'); //调用派生类成员函数
OBj.Base<char>::Show(89); //用类名引导,调用基类成员函数
}
【例6-11】本例介绍类模板的带参数构造函数的使用以及在派生类中的激活方式。
#include<iostream>
usingnamespacestd;
template<classType>
classBase
//定义了类模板,类名是Base
{protected:
Typeb;
//成员类型由Type决定public:
Base(Typex){b=x;} //带参数构造函数
voidShow(){cout<<b<<“\n”;}
};
template<classType1,classType2>
classDerived
:publicBase<Type2>
//指定Derived是Base<Type2>的公有派生类
{protected:
Type1d;public:
Derived(Type1y,Type2z)
:Base<Type2>(z) //激活基类构造函数,并将z传给基类
{d=y;}
voidSHow(){cout<<d<<""<<b<<endl;}
};voidmain()
{Base<float>obj1(3.14f); //定义基类对象obj1并用3.14f初始化
obj1.Show(); //调用基类的Show函数
Derived<char*,double>obj2("Piis:",3.14159);
//构造派生类对象,并激活基类构造函数
obj2.SHow(); //调用派生类的SHow函数
}6.9.1使用规则
在C++中,基类指针可以直接指向公有派生类对象,而派生类指针必须经过强制转换,转换为基类指针后方可指向基类对象。前者是系统自动进行的隐式转换,后者是人为的强制转换。对于强制转换,使用时要慎重,它可能带来错误结果。6.9基类和派生类的指针6.9.2使用方法
对于用指针访问对象成员,主要讨论的是派生类指针与基类指针交叉使用的情况,进而从中找出一些新特点与应用。
1.用基类指针指向派生类对象
对于基类A、派生类B,我们可用“是”的关系描述为:B“是”A。既然B是A,那么就可以直接用基类指针指向派生类对象。为此C++中提供了自动转换。但是将基类指针指向派生类对象后,对派生类的访问是受限制的,它只能调用基类成员函数并访问派生类中被继承的基类成员。若想要调用派生类成员函数,访问派生类中独有的成员,就要将该指向派生类对象的基类指针强制转换为派生类指针。这种用基类指针指向派生类对象,调用派生类和基类成员函数的方法是安全可行的。该方法是在继承关系中使指针能发挥优势的常用方法,将在下一章中详细介绍。
2.用派生类指针指向基类对象
对于用派生类指针指向基类对象,C++中规定,必须将派生类指针强制转换为基类指针,否则将产生语法错误。转换后的指针只能调用基类成员函数,并访问派生类中的基类成员。也就是说它还是指向派生类,只不过调用范围被限制,可访问的数据成员也被限制在派生类中的基类成员,没有实际意义,一旦再想调用派生类成员函数就出错。因此,用派生类指针强制转换后,指向基类对象成员是危险的,也是不可取的。
【例6-12】在本例中,点类是基类,它派生出圆类。在main中可以看到,用基类指针指向派生类对象,访问了基类成员函数,正确;用基类指针指向基类对象,访问了派生类成员函数,错误。
#include<iostream.h>
classPoint
{friendostream&operator<<(ostream&,constPoint&);
protected:
floatx;floaty;public:
Point(float=0,float=0); //初始化点的位置
voidsetPoint(float,float); //设定点的位置
floatgetX()const{returnx;} //得到x坐标
floatgetY()const{returny;} //得到y坐标
};
Point::Point(floata,floatb){x=a;y=b;}
voidPoint::setPoint(floata,floatb){x=a;y=b;}ostream&operator<<(ostream&output,constPoint&p)
//友元函数的定义
{output<<'['<<p.x<<","<<p.y<<']';
returnoutput;
}
classCircle:publicPoint
{friendostream&operator<<(ostream&,constCircle&);
protected:
floatradius;public:
Circle(float=0.0,float=0,float=0);
voidsetRadius(float); //设定半径
floatgetRadius()const; //得到半径
floatarea()const; //得到面积
};
Circle::Circle(floatr,floata,floatb):Point(a,b)
{radius=r;}voidCircle::setRadius(floatr)
{radius=r;}
floatCircle::getRadius()const
{returnradius;}
floatCircle::area()const
{return3.14159f*radius*radius;}
ostream&operator<<(ostream&output,constCircle&c)
{output<<"Center=["<<c.x<<","<<c.y<<"];Radius="<<c.radius;
returnoutput;
}voidmain()
{Point*pointp,p(3.5f,5.3f);
Circle*circlep,c(2.7f,1.2f,8.9f);
cout<<“Pointp:”<<p<<“\nCirclec:”<<c<<endl;
pointp=&p; //ok,基类指针指向基类对象
pointp=&c; //ok,基类指针指向派生类
cout<<pointp->getX();
//ok,调用基类成员函数,输出派生类对象中的x为1.2
//cout<<pointp->area();
//error,基类指针不可访问派生类中的新增成员函数
circlep=(Circle*)pointp;
//ok,将该基类指针强制转换为派生类指针
cout<<“\nAreaofcircle:”
<<circlep->area()<<endl;
//ok,经转换后的派生类指针可访问派生类中的成员
cout<<circlep->getX();
//ok,用派生类指针访问基类成员函数,输出x为1.2
cout<<pointp->getX();//ok,基类指针访问基类成员函数
//circlep=&p; //error,派生类指针不能直接指向基类对象
pointp=&p; //ok,基类指针指向基类对象
circlep=(Circle*)pointp; //强制转换
cout<<"\nRadiusofobjectcircle:"<<circlep->getRadius()<<endl;
}运行结果:
Pointp:[3.5,5.3] //初始化的坐标
Circlec:Center=[1.2,8.9];Radius=2.7 //重设置的坐标和半径
1.2
Areaofc(viacirclePtr):22.9022 //输出圆面积
1.21.2
RadiusofobjectcirclePtrpointpointto:9.45831e-039
//输出不确定值● 继承是在现有的类的基础上建立一个新类,通过继承,可以实现类库的可重用性。
● 类以另一个类的对象作为数据成员,称为组合。
● 在继承方式中可用“是”来描述,在组合方式中可用“有”来描述。
● 面向对象的程序设计基于的两个原则就是抽象和分类。
● 抽象是要找出事物的共同的特性和方法,是具体事物描述的一个概括。
● 分类就是要找出事物实体的各自特性和方法,是使概念逐渐细化,即具体化。本章要点● 进行抽象和分类的方法主要从行为和特性两方面考虑,从中找出共性与个性以及它们之间的关系。
● 在继承关系中,被继承的成员函数是共享的,被继承的数据成员将在派生类中生成一份拷贝。
● 在派生类中若新增函数与基类中的成员函数具有相同的原型,这种现象就称为成员函数的重定义。
● 在继承中的重定义可以不改变基类,而在派生类中通过重定义为该函数赋予新的功能。对于不需要继承的功能,可以通过重定义成员函数来予以屏蔽。● 派生类的成员函数不能访问基类的私有成员,可以访问基类的公有或保护成员。
● 在单个类中,private和protected成员没有什么区别。但在继承关系中,基类的private成员不但对应用程序隐藏,甚至对派生类也隐藏。而基类的保护成员则只对应用程序隐藏,对派生类则不隐藏。
● 在派生类中可通过类名引导访问被屏蔽的基类中的保护或公有成员。
● 若希望从基类继承的大多数公有成员变成私有或保护成员,仅其中几个仍保持原性质,就要用恢复访问控制方式。● 构造派生类对象时,必须激活所有基类构造函数。激活时可以向基类给出参数,或由派生类构造函数传出参数,也可让基类以默认参数构造。
● 对于既是派生类又是组合类的对象,它的构造次序是:先构造基类,再构造组合类,然后构造派生类。
● 类与类之间要分工明确,各做各的,以接口作沟通。● 模板是一种高效、安全和实用的重用代码方式。在模板类的继承关系中不仅要给出派生类中的类型参数,而且还要给出基类中的类型参数。
● 基类指针可以直接指向公有派生类对象,若还想要用该基类指针访问派生类中新增的成员,就需要强制转换。
● 把基类指针指向公有派生类对象,再把基类指针强制转换为派生类指针,就可以方便地访问派生类对象了。一、概念
1.什么是继承与组合?说明它们的作用。
2.什么是抽象和分类?
3.在继承中成员之间的关系是:基类的成员函数与派生类对象
,基类的非静态数据成员在派生类对象中则生成一份
。
4.在派生类中,对基类中的成员函数重定义的目的是:
。
5.在派生类中,访问被屏蔽的基类中公有成员函数的方法是:
。练习
6.在派生类成员函数中,可以访问基类对象中的
和
成员。
7.若不标明继承是公有、保护或私有,则默认为
继承。
8.继承关系中,成员的调整是
访问控制方式。
9.在继承与组合关系中,可以用
关系来描述继承,可以用
关系来描述组合。
10.在C++中,基类指针可以
指向派生类对象,派生类指针
指向基类对象。二、分析
1.请说出下例对象中哪些可以用“是”来描述,哪些可以用“有”来描述。
人、房屋、建筑、门、凳子、儿童、男生、女生、课本
2.用程序语言描述人、男生、女生和课本的继承与组合关系。
3.说出下列程序的运行结果。
#include<iostream>
usingnamespacestd;
classB
{public: B(int=0,int=0);
voidPrintb();
private:
inta,b;
};
classA
{public:
A();
A(int,int);
voidPrinta();
private:
Bc;
};A::A(inti,intj):c(i,j)
{cout<<"构造类A"<<endl;}
voidA::Printa(){c.Printb();}
B::B(inti,intj)
{cout<<"构造类B"<<endl;
a=i;b=j;
}voidB::Printb()
{cout<<"a="<<a<<",b="<<b<<endl;}
voidmain()
{Am(10,20);
m.Printa();
}
4.下面的程序有错吗?说出运行结果。
#include<iostream>
usingnamespacestd;
classStudent
{private:
charname[40];
intnum;
public:
Student(char*pName="noname",intx=201);
voidSet(char*);
voidDisplay();
};classSchoolgirl:publicStudent
{protected:
chartext[40];
floatscore;
public:
Schoolgirl(char*pText="C++",float=60);
voidStudy(float);
};Student::Student(char*pName,intx)
{strcpy(name,pName);
num=x;
}
voidStudent::Set(char*pName)
{strcpy(name,pName);}
voidStudent::Display()
{cout<<"num\t"<<"name\t"<<endl;
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 二零二五版门窗行业环保与可持续发展合作合同3篇
- 2025年电梯安装与城市更新项目合同2篇
- 二零二五年度船舶船员劳务合同(海洋工程咨询)3篇
- 二零二五年度厨房电器配件采购合同样本4篇
- 2025年度新能源汽车核心部件承揽合同(GF(2024版)规范)4篇
- 2025年柴油储备与调拨管理合同4篇
- 2025年度面包砖施工技术咨询服务合同4篇
- 2025年度个人无息短期借款合同(紧急资金周转服务协议)11篇
- 2025年度个人医疗贷款债权转让与健康管理服务合同3篇
- 2025年度虫草养生产品研发与销售合同4篇
- 乡村治理中正式制度与非正式制度的关系解析
- 2024版义务教育小学数学课程标准
- 智能护理:人工智能助力的医疗创新
- 国家中小学智慧教育平台培训专题讲座
- 5G+教育5G技术在智慧校园教育专网系统的应用
- 服务人员队伍稳定措施
- VI设计辅助图形设计
- 浅谈小学劳动教育的开展与探究 论文
- 2023年全国4月高等教育自学考试管理学原理00054试题及答案新编
- 河北省大学生调研河北社会调查活动项目申请书
- JJG 921-2021环境振动分析仪
评论
0/150
提交评论