第二章2527.docx
- 文档编号:26248717
- 上传时间:2023-06-17
- 格式:DOCX
- 页数:22
- 大小:61KB
第二章2527.docx
《第二章2527.docx》由会员分享,可在线阅读,更多相关《第二章2527.docx(22页珍藏版)》请在冰豆网上搜索。
第二章2527
2.5类和继承
2.5.1简述
继承机制是组织构造和重用类的一工具,如果没有继承概念的支持,则所有的类都象一盘散沙,分别是一个个独立的实体,每次软件开发仍要人从“一无所有”开始,这时,由于类开发者在构造方法时仍然各自为政,使得类和类之间没有什么联系,即使有,也只是程序员之间的某种约定的结果。
由于继承机制是个相关的关系来组织事物的,可以减少我们对相似事物运行说明和记忆的规模,所以它为我们提供了一种有效的简化手段。
彼此独立开发的事物常常是不一致的,其中会有许多信息要求人们去记忆,这当然容易出差错。
继承是面向对象语言中功能最强大、最有代表性的特性。
首先继承使新的类变得简洁明了;其次通过继承可以重复使用和扩展那些经过测试的已有的类,实现重用。
最后是利用继承可以增强处理的一致性。
子类和继承是两个不可分割的概念,继承是在类、子类以及对象间共享数据和方法的一种重用性(reusability)机制。
在设计新类时,只需考虑与已存在的类所不同部分,可以继承存在的类的内容作为自己的内容。
已存在的类通常称作超类,新的类通常称作子类。
子类不仅可以继承超类的方法,也可以继承超类的属性。
如果超类中的某些方法不适合与子类,则可以重置这些方法,即重写方法新的实现部分并予以存贮。
如果类C能使用类B中的方法及属性,称B是C的超类(superclass),C是B的子类(subclass),也称类C继承类B。
参见图2-5。
从语义上讲,继承表示is-a联系。
例如一个正方形is-a(是一个)长方形。
类间的继承关系是可以传递的:
如果C的超类是B,B的超类是A,则A是C的间接超类,C是A的间接子类。
而B是C的直接超类,C是B的直接子类。
继承可分为单继承和多继承,如果一个类只有一个直接超类,则这种继承叫做单继承,如果一个类有多于一个的直接超类,这种继承叫做多继承。
如下图:
由上图可知,单继承构成类之间的关系是一棵树,多继承构成的类之间的关系是一个网格。
从上图可以看出,继承可以用图来表示,这个图叫做类层次(classhierarchy)。
在类层次上指定每一个类为一个节点。
2.5.2继承的说明
通过继承,可以找出类间的共性,将这些共性在一个类中指出,另外的类可以继承这些共性,可以重用这些共同的描述。
在软件工厂中,继承被看作重用的核心思想。
在许多面向对象的系统中并不支持多继承,如Smalltalk-80。
虽然多继承并不频繁需要,但没有这种机制,系统就会显得很不方便。
面向对象的系统中,重置(overriding)是一个非常重要的概念。
重置可以重新修正从超类继承下来的属性及方法。
重置是修改利用已存在类的一种简单而灵活的方法,但使得类层次不易理解,因为重置使得具有相同名字的方法在不同类中具有不同的语义。
因而,对类进行重置后,继承便变得不具有可传递性了,这时子类并没有完全继承超类的所有特征,有些特征被继承,有些特征则被重置或重定义了。
实际上,重定义(redefinition)和重置有一定区别,在重定义时,操作的表示和操作的实现体将都改变,而在重置中,只有操作的实现体被改变,而操作的说明及表示仍与以前一样。
通过继承还可以很容易加入一个新类,只要通过描述已存在类有什么变化即可。
但是,增加一个新类可能有时引起继承层次的重构。
在面向对象的程序设计语言中,有一个非常重要的原理叫做替换原则(principleofsubstitution)。
替换原则指对于类A和类B,如果B是A的子类,那么在任何情况下都可以用类B来替换类A。
所有的面向对象程序设计语言都支持替换原则,尽管某些语言在重置方法时需要附加的语法。
大多数语言以一种非常直接的方式来支持替换原则。
对于C++语言,只有指针和引用真正地支持替换原则,声明为值的变量不支持替换原则。
通过抽取及共享共同特征,可用泛化(generalization)原理将这些共性抽取出作为超类放在继承层次的上端。
抽取出的超类称作抽象类(abstractclass),抽象类一般没有实例。
在增加新类时,发现已有类已提供了新类所需的某些特征,则新类可继承旧类的特征。
新类作为旧类的子类,这个过程叫做特化(specialization)。
对象的类是通过说明对象的属性来描述结构的,而子类的实例必须保持超类中所描述的属性的型。
这样在实现继承时,子类能否直接访问超类中的属性就成了一个有争议的问题。
如果允许则破坏封装性,如果不允许,就很难解决结构继承问题。
对于一个类来说,它的客户(client)可以分成两大类:
一是生成类的实例以及用类的方法来操纵实例,这与继承没有什么关系二是一个类的子类继承这个类的结构和行为。
这两类客户分别称作实例化客户及继承客户。
对于这两类客户不同的系统采用不同的策略。
SMALLTALK的策略是:
对于继承客户,允许对属性不加限制地访问,而对实例化客户则加以完全的限制。
如以A表示超类,x是超类的属性,B、C是A的两个子类,那么B和C中的任何方法都可以直接对x进行访问或予以更新。
对于A的一个实例o1,
则只能用消息传递间接地使用对象o2的x。
在C++中,将所需要的保护留给实现去完成,设置了
(1)Public当一个属性或方法说明为public,任何客户都可直接对它们访问、操纵或调用
(2)Private当一个实际属性或方法被说明成private时,没有一个客户可以直接对它们访问、操作或调用。
(3)Protected当一个实例变量或方法被说明为protecte时,它们只能被继承客户直接访问、操纵或调用。
多继承
多继承的主要缺点是降低了类继承层次的易懂性。
特别是当两个超类都有同名的方法,但定义不同时将引起很大的问题。
我们必须定义一个新的方法或显式地选择一个已经存在的定义。
在大多数支持多继承的面向对象的程序设计语言中,用户被迫重新定义方法名使方法名唯一。
这是最容易被接受的方案,一种特殊情况是同一方法存在于两个超类中,而这两个超类又具有同一个超类,见下图,这种继承称作重复继承(repeatedinheritance)即发生重名冲突
在多继承中必须解决重复继承问题,解决方法随着语言的不同有着不同的解决方法。
一种途径是发现这种冲突时发出出错信号,并要求子类对冲突的方法予以显式指明,指明方法是从哪个超类中继承来的。
2.6继承与子类在面向对象程序设计语言中描述
2.6.1单继承
为了定义一个类是另一个类的子类,必须在类中定义一个继承部分,用于指明其超类。
在C++中超类称作基类,子类称作导出类。
子类(导出类)的定义与普通类基本一致,所不同的是必须在类头部分指明其超类其形式如下
classderivedclass:
publicbase_class
其中derived_class为导出类(子类)名,base_class为基类(超类)名。
冒号用于建立子类devrived_class与超类base_class的层次结构。
子类在类体中也可以定义数据成员(属性)及成员函数(方法),但子类除具有自身定义的成员(特征)外,还继承了其超类的所有成员(特征)。
我们可以在类employee的基础上定义经理Manager类,定义如下
classManager:
publicEmployee{
intLevel;
public:
voidChangeLevel(intn);
Manager(char*name,intage,intlevel);
~Manager();
};
在上例中,子类Manager继承了超类Employee所定义的数据结构和方法,因而类Manager的实例至少具有类Employee的实例所具有的哪些特征。
这时称类Manager为Employee的一个直接子类(派生类)。
类Manager本身所定义的属性及方法所表示的仅仅是经理于一般职员之间的差别,例如Level表示经理的级别。
子类的实例化过程不仅与自身的定义有关,而且与超类有关。
类Manager的构造函数可以有以下实现
Manager:
:
Manager(char*name,intage,intlevel):
(name,age)
{Level=(level>=0)?
Level:
0;}
要进行一次实例生成:
Managerm1("DavidBlack",35,3);
这里m1代表名叫DavidBlack35岁级别为3的一位经理对象实例。
在生成这个实例时,先要调用超类Employee的构造函数,因为子类实例的数据结构通常还包含从超类继承的数据成员(属性)。
这里Manager实际有三个数据成员:
Name,Age,Level,其中前两个从超类Employee继承而来。
对于继承下来的数据成员进行操作,应当由定义这些数据成员的类的方法来完成,这是保持封装性的一种基本方法。
因此,在Manager的构造函数的参数列表之后出现了":
(name,age)"的语法现象,编译程序将其翻译成对Manager的超类Employee的构造函数的一次调用。
我们还可以看到,子类虽然继承了超类所有的特征,但超类的私有部分对子类仍然是不可见的。
子类的方法并不能直接访问它们,而只能访问超类的公用部分,因此子类的方法也只能通过超类公用部分提供的方法访问超类的私有部分。
子类客户不能直接访问超类的私有特征,但似乎可以访问超类的公用特征。
事实上,是否如此,应由定义子类时的标识符public及private来决定。
当用public来定义子类时,超类的保护部分及公用部分由子类继承为保护部分,此超类的公用部分对用户是可见的。
当用private来定义子类时,则超类的保护部分及公用部分由子类继承为其私有部分,因此,子类的用户就不可以直接访问超类的保护和公用部分。
在有些面向对象程序设计语言中,还可以对继承特征重新命名,改变继承特征的有效定义状态,对继承特征的实现进行重定义及去掉重复继承中的命名冲突。
2.6.2重置(overriding)
当子类继承超类的特征时,如果超类的特征不完全符合子类,则子类可以对已有的特征进行修改。
例如在设计Manager时,发现从Employee继承而来的方法Retire不能满足经理的退休条件,因为普通职工55岁退休,经理则要到岁退休。
如果不考虑继承机制的特点,可以简单地为类Manager设计一个方法MRetire,规定经理60岁退休。
这样作仅仅是为了与继承而来的Retire相区别。
这样作就损害了面向对象程序设计语言的自然性,因而,不主张用这种方法解决问题。
面向对象的程序设计语言提供一种机制,对从超类继承来的方法进行修改,并且不影响超类,这种机制叫做重置(overriding)。
它通过动态联编,使得在子类继承超类接口定义的前提下,用适合于自己要求的实现去置换超类中的相应实现。
在C++中通过虚函数(virtualfunction来实现重置)。
下面用重置方式来修改类Employee和Manager的定义。
classEmployee{
protected:
char*Name;
intAge;
public:
voidChange(char*name,intage);
virtualvoidRetire();
Employee(char*name,intage);
~Employee();
};
classManager:
publicEmployee{
intLevel;
public:
voidRetire();
voidChangeLevel(intn);
Manager(char*name,intage,intlevel);
~Manager();
};
voidManager:
:
Retire();
{
if(Age>60)
deletethis;
}
在Employee的方法Retire的接口定义加上了说明符virtual,从而将这个方法在这个类以及所有子类中的出现都定义成了虚函数,这将告诉编译程序这个方法允许在子类中被重置。
在Manager中增加了方法Retire的接口定义,并且与Employee中的相应接口定义完全相同,这将告诉编译程序,这个方法在这个类中要重新定义方法的实现进行重置,并且重新定义方法Retire的实现。
将Employee中原来的private改为protected是为了在Manager:
:
Retire方法的实现中直接引用Employee的成员Age。
类Employee的构造没有发生本质的变化,就在重置机制下使得Manager对Employee实现了有选择的继承。
从以上可以看出:
(1)重置时子类不改变超类中的已有接口定义,所以无论Employee有多少子类,也无论这些子类中关于方法Retire的语义要求相同或不同,在Employee及其所有子类中关于退休的操作都是通过方法Retire进行的。
(2)重置机制是基于动态联编的。
原程序中对一个方法名的引用,在运行时才根据当前接受消息的那个对象的类来决定和那个方法的实现进行联编。
因此,即使在用超类定义的方法对子类的实例进行操作时,方法实现中如果引用了被子类重置了的方法,也将与子类中重新定义的方法实现进行联编。
(3)重置机制并不强加给程序员,程序员可以根据应用的要求,灵活地决定子类中需要对那些方法进行重置,那些需要继承超类中定义的方法语义,包括继承那些被超类重置后的方法语义。
在程序设计中还可能产生这样的要求:
在子类中既需要重置,也需要继承超类中的某些方法。
对此,C++语言也提供了相应的支持。
在子类的任何一个方法实现中,如果直接引用的是方法名,而这个方法又是一个虚函数,则采用重置后的语义;如果在引用时这个方法名之前出现了某个超类名以及类的域分辨符,则意味着采用这个方法在超类中的语义。
例如,假如在类Manager中方法Retire被重置,那么在Manager中的某个方法实现中,如果出现了Employee:
:
Retire();和Retire()方法的实现静态联编,而后条语句要进行动态联编,至于是与Manager:
:
Retire还是与Manager的某个子类对Retire的重置结果进行联编,则取决于当前接收消息的对象类型。
反之,如果在类Manager及其子类中都没有重置Retire,那么尽管上面两条语句的联编方式不同,但效果等价。
2.6.3抽象类(abstractclass)
在面向对象的方法中,有一个重要的概念,称之为泛化(generalization),其含义是通过将若干类的所共享的公共特征抽取出来,形成一个新类,并且将这个类放到类继承层次的上端以供更多的类所重用,这样构造出的类叫做抽象类(abstractclass)。
通常,抽象类不能创建实例,所以抽象类没有实例。
抽象类可由下例说明:
在例中,类Employe定义了姓名、年龄等,实际上,这些属性并不是雇员的所有特有。
现在要求在同一应用中还要引入学生这样的对象的类。
显然学生同样具有以上特征,假设学生类的名字为Student,它仍然具有Name、Age属性。
classStudent{
private:
char*Name;
intAge;
public:
.
.
.
Student(char*name,intage);
.
.
.
};
我们可以对类Employee及Student进行分析,可以发现这两个类有共同的属性及相关操作,然后将这样的特征抽象出,构造一个抽象类Person。
在应用中,一般不存在“人”这样的实体,类Person产生实例也就没有实际意义,所以抽象类Person只有内涵没有外延。
定义抽象类Person的目的是让子类共享特征。
下面将类Person写出,并重写类Employee及Student。
classPerson{
protected:
char*Name;
intAge;
intPrintwidth;//输出宽度
public:
/*...*/
voidChange(char*name,intage);
virtualvoidPrintOn()=0;//纯虚函数
Person();
Person(char*name,intage);
~Person();
}
classEmployee:
publicPerson{
/*...*/
public:
virtualvoidRetire();
/*...*/
voidPrintOn();
/*...*/
}
classStudent:
public:
Person{
/*...*/
public:
/*...*/
voidPrintOn();
}
在类Person中,有一个接口说明中有“=0”的虚函数PrintOn,按照C++的语法规则,这是一个纯虚函数的说明。
所谓纯虚函数是不定义实现(实际上无法定义实现)的虚函数,这样的虚函数不许在相应的子类中定义其实现,否则不能引用。
C++巧妙地将纯虚函数的与抽象类机制结合在一起,使其具有纯虚函数的类都是抽象类,因而抽象类不一定是基类,在上例中,如果将PrintOn仍然定义成纯虚函数:
classStudent:
publicPerson{
/*...*/
public:
/*...*/
virtualvoidPrintOn()=0;
/*...*/
}
那么Student仍然是一个抽象类,不允许有实例。
2.6.4多继承
多继承允许一个类有多于一个的直接超类,子类则继承了所有超类的特征,在客观世界中,经常发现某种相对特殊的概念同时具有多种相对一般的概念特征。
例如,在职进行学习的雇员,既保持着雇员的特征,又有学生的特征,因为为这样的人员定义一个类为Empstudent,它是类Employee及类Student的共同的直接子类。
classEmpstudet:
publicEmployee,publicStudent{
/*...*/
public:
/*...*/
Empstudent();
Empstudent(char*name,age);
~Empstudent();
};
定义基类Person时,printon是纯虚函数,所以相关的属性printwith的值一般应由子类来设置。
假设在Employee的构造函数中将Printwidth的值置成60,在student的构造函数中将Printwidth的值定义为40,在Empstudent的构造函数中不对Printwidth进行设置。
这时,由于Empstudent对象中只有一个Printwith的存贮位置,因而就产生了二义性。
同样在Empstudent的对象中,执行Printon时,执行Employee定义的Printon还是
Student定义的Printon,也产生二义性。
多继承中多个超类在保护和公用部分定义了相同的属性及方法名都会产生二义性。
C++规定,多继承时,直接超类的构造函数的调用次序是:
(1)抽象超类。
若有多个抽象超类,按继承说明次序从左到右。
(2)非抽象超类:
若有多个非抽象超类,按继承顺序从左到右。
对于上述的重名方法产生的二义性,C++允许这种二义性存在,但不能直接使用具有二义性的方法名。
解决方法是用相应的类名来加以标识,以消除二义性,例如,es是类Empstudent的实例
es.printon就会产生二义性,而
es.Employee:
:
printon()及
es.Student:
:
Printon就不会产生二义性。
2.7多态(Polymorphism)
2.7.1.简述
多态是程序设计语言中一个重要的概念,“多态”一词是C.Strachey在60年代引入的,用以刻化多态函数,即函数的参数可以取多种类型。
多态的一般含义是:
当一问题领域中的元素可以有多种解释,如果考虑的元素是名字,则多态的含义是一名多用,只用同一个方法名,可以有不同的语义及实现,这也叫做重载(overloading)多态。
语言含有重载多态的原因是可以使用传统的,自然的记法,还可以让多个程序员在各自设计的程序中使用相同的名或操作,把它们组成一个程序而不致引起混乱。
只要有办法能区分重载的各个实例,重载就允许使用。
多态还具有另一种特征,这种特征使得一个属性或变量可以在不同时期表示不同类的对象。
面向对象的方法引入多态的概念是为了得到更为灵活的方式使表示的形式尽可能与所表示的内容无关。
当属于某个类的对象要与某个对象通讯时,要给这个对象传递消息,包括要调用这个对象的方法,给该对象传递信息,这时只知道这个对象的标识,但不知该对象属于那个类。
因此多态可以认为是传递消息的实例对象属于那个类,接收消息的实例对象可以属于任意的类。
消息传递亦可方法所调用,多态可以描述为当某一对象调用另一对象的方法时,不必知道该对象属于哪个类,即方法是定义在那个类的方法,系统将自动进行方法搜索以确定该方法是定义在哪个类上的方法。
一个消息(或称方法)可以用不同的方式来解释,这决定于消息的接收对象属于那个类。
多态通常被认为是一种方法在不同的类中可以有不同的实现,甚至在同一类中仍可能有不同的定义及实现。
如果消息的接收对象可以属于多个类,但这些类是一个已知的类的有限集合,这样的多态称作有限多态(limitedpolymorphism)。
在多态的概念中一种方法可在不同的类中有不同的实现,因此,在系统编译或运行过程中消息接收的实例对象负责搜索并且找出方法是定义在哪个类的方法。
将接收到的消息与实际执行的操作相联系的过程称作联编(binding)。
如果这种联编发生在编译时刻,称作静态联编(staticbinding)或提前联编(earlybinding)多态性有时在编译时刻不能确定消息接收对象属于那个类,即不能确定到底执行方法的那个具体操作。
这时需在系统运行时刻决定执行的方法到底是定义于那个类的方法,这时就存在运行时方法搜索,进行联编,这就叫动态联编(dynamicbinding)也称迟后(late),延迟(delayed),或虚(virtual)联编。
动态联编非常灵活,但性能有所下降;在原理上,方法搜索算法是在运行时刻进行的,这就意味着每传递一次消息(每调用一次方法),方法搜索算法就要执行一次。
然而,再大多数语言实现中,复杂的缓冲策略或特殊的功能表用来减少这种离开销。
动态联编的优势在于使系统变的非常灵活这种灵活性对于经常进行有规则修改的系统是非常有用的,执行前不进行联编,许多修改都不影响发送消息(调用方法)的对象。
然而,静态联编是比较安全和有效的。
如果对于有类型的语言,由于错误是在编译时刻被发现,在运行时刻就不会引起故障,因而是比较安全的,由于方法查找算法在编译时刻只执行一次,因而是比较有效的。
对于在编译时刻不能确定方法的多态,要求在运行时刻将消息(方法调用的名称及参数)真正地传给接收消息的对象,因为只有在运行时刻接收消息的对象才得以知道属于那个类,即所调用的方法定义在那个类上,因此,在此之前就不能将消息与真正的操作(方法的实现)进行联编,因而,需要使用动态联编技术,动态联编是实现多态性的一个好的方式。
多态的英语名称是polymorphism,它源于古希腊语,意思是“许多形式”
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 第二 2527