C++ 继承和派生介绍
继承(inheritance)是软件重用的一种方式,程序员通过继承可以吸收已有类的数据和行为来创建新类,并可以添加新的数据和行为来增强类的功能。创建新类时,并不需要创建全新的数据和成员函数,我们可以指明这个新类应当继承现有类的成员。此时,现有的类称为“基类”,继承实现的新类称为“派生类”。派生类代表了一组更加特殊化的对象,它包含了从基类继承来的行为,并进行了扩充。
介绍:
类是对现实中事物的抽象,类的继承和派生的层次结构则是对自然界中事物分类、分析的过程在程序设计中的体现。下图说明了某个公司雇员的派生关系。位于最高层的雇员其抽象程度最高,是最具一般性的概念。最下层抽象程度最低,最具体。从上层到下层是具体化的过程,从下层到上层是抽象话的过程。面向对象设计中上层与下层是基类与派生类的关系。
依照面向对象的理念:当某个概念用语言的基本类型不能具体表示时,就应当定义一个新的类型(class)。一个概念一般不是独立存在的,通常与其相关的概念共存,以此发挥更大的作用。当某个概念与其他概念之间存在关系时,应当在相应的类型之间也表示出这样的关系。
类之间的关系主要有以下两种:
(1)“has a”:A car has wheels, engines, …
(2)“is a”:A Manager is an Employee.
‘“has a”的表示方式-聚集(Integration)
一个车有四个轱辘一个引擎。
这里的关系实质上是聚集中的组合。整体拥有各部分,部分与整体共存,如果整体不存在了,部分也会消失,这称之为组合。
class Car { Wheel ws[4]; Engine e[1]; // ... };
孤立的类只能描述实体集合的特征同一性,而客观世界中实体集合的划分通常还要考虑实体特征方面有关联的相似性。
“相似”无非是既有共同点,又有差别:
(1)内涵的相似性:在客观世界中具有一般——特殊的关系(is a)。例如:雇员(Employee)—经理(Manager)。
(2)结构的相似性:具有相似的表示。例如:array、vector。
那如何表示相似的事物?
如果将相似的事物用不同的类型来表示,能够表示其差别,但体现不了它们之间存在共性的事实,且共性的表示也可能不一致。当扩充维护过程中需要对其共性部分进行修改时,就面临着保持一致性的问题。
如果将相似的事物用相同的类型来表示,则体现其差别就十分困难,且失去了类型化的支持。一旦需要扩充和修改,将影响用此类型表示的所有事物。
方式1、重复定义共性来表示“is a”
方式2、-将共性表示为一种类型来表示“is a”
方式3、通过继承来表示“is a”
上面两张方式无论是那种方式:对于人来讲,虽然能表示出类之间的共性部分 – 但都无法告诉编译器或是其它工具Manager is also an Employee.
由于用了两个(没有指明存在共性关系的)类型来表示:A Manager* is not an Employee*, 所以在使用其中一个的地方就不能使另外一个,特别是,如果不写一些代码不能将 Manager放入Employee的列表(list)中。
正确的方法是通过附加一点信息,显示地声明 Manager is an Employee:
The Manager is derived(派生) from Employee, and conversely, Employee is a base class(基类) for Manager.
除自己的成员以外,类 Manager 还拥有 Employee类的成员。一个派生类被认为是从基类继承了基类的属性,所以这种关系被称为继承Inheritance。
因为基类的成员被派生类所继承,所以继承有益于代码重用(code reuse)。
“has a”关系即组合关系,通过定义类的属性的方式实现的;“is a”关系即继承关系,通过类继承实现。
如果你确定两件对象之间是is-a的关系,那么此时你应该使用继承。比如菱形、圆形、正方形都是形状的一种,那么它们应该服从形状的继承而不是组合。
在实际应用中还会有“has a”和“is a”两者同时用的情况,一个类在接口上表现为“is a”的继承关系,但在实现上又表现为“has a”的组合关系。
继承允许设计者在一个共同的基类中表达多个类的组合特征,例如交通工具就可以作为自行车、汽车等多个类的共同基类。
一个派生类不仅可以从一个基类派生,也可以从多个基类中派生。一个派生类有两个或多个基类的称为多个基类能够多重继承(multiple inheritance)。
关于基类和派生类的关系,可以表述为:派生类是基类的具体化,而基类则是派生类的抽象。
使用继承必须满足的逻辑关系:
如果写了一个基类 A,又写了基类A的派生类B, 那么要注意,“一个B对象也是一个A对象”这个命题从逻辑上成立,是A派生出B 为合理派生的必要条件。
使用继承容易犯的错误:如果写了一个CMan类代表男人,后来又发现需要一个CWoman类来代表女人,仅仅因为CWoman类和CMan类有共同之处,就让CWoman类从CMan类派生而来,是不合理的。因为“一个女人也是一个男人”从逻辑上不成立。
基本概念与语法:
class <类名>:<继承方式><基类名1>, <继承方式><基类名2>,...
{ ... }
构造一个派生类包括以下3部分工作 :
(1) 从基类接收成员。派生类把基类全部的成员(不包括构造函数和析构函数)接收过来,也就是说是没有选择的,不能选择接收其中一部分成员,而舍弃另一部分成员。
(2) 处理从基类接收的成员。接收基类成员是程序人员不能选择的,但是程序人员可以对这些成员作某些处理。
(3) 在声明派生类时增加的成员。这部分内容是很重要的,它体现了派生类对基类功能的扩展。要根据需要仔细考虑应当增加哪些成员,精心设计。
在设计时要根据派生类的需求慎重选择基类,使冗余量最小。
#include <iostream> using namespace std; class A { public: int a; int b; private: int c; protected: int d; }; class B: public A { int c; }; main( ) { cout << ” size of A is” << sizeof(A); cout << ” size of B is” << sizeof(B); }
输出:
size of A is 16
size of A is 20
派生类的存储结构与基类的存储结构存在着“粘接(splice)”关系:
sizeof( Base Class ) <= sizeof ( Derived Class )
只有公用派生类才是基类真正的子类型,它完整地继承了基类的功能。基类与派生类对象之间有赋值兼容关系,由于派生类中包含从基类继承的成员,因此可以将派生类的值赋给基类对象,在用到基类对象的时候可以用其派生类对象代替。赋值兼容存在以下三种形式:
(1)派生类对象可以向基类对象赋值;
(2)派生类对象可以替代基类对象向基类对象的引用进行赋值或初始化;
(3)派生类对象的地址可以赋给指向基类对象的指针变量,也就是说,指向基类对象的指针变量也可以指向派生类对象。
赋值兼容仅是针对public派生,如果派生方式是private或protected,则上述三条不可行。
可以用子类(即公用派生类)对象对其基类对象赋值,如:
A a;
B b;
A = b;
如已定义了基类A对象a,可以定义a的引用变量:
A a;
B b;
A& r=b;
当通过基类指针或引用操作时,派生类的对象可被当做基类的对象看待。
class Employee { public: string name; short department; // ... }; class Manager : public Employee { short level; // ... }; void f( Employee* pe ) {}; void main( ) { Employee e; Manager m; f( &e ); f( &m ); Employee *pe = &m;//基类对象指针指向派生类,派生类对象被当做基类对象看待了 pe -> department = 10; // OK pe -> level = 1; // error C2039: “level”: 不是“Employee”的成员 }