继承和派生类
继承的概念
继承是面向对象程序设计中最重要的机制,它支持层次分类的观点.继承使得程序员可以在一个较一般的类的基础上很快地建立一个新类,而不必从零开始设计每个类,在现实世界中,许多实体或概念不是孤立的,它们具有共同的特征,但也有细微的差别,人们使用层次分类的方法来描述这些实体或概念之间的相似点和不同点.如下图:
图片1
表示昆虫学家对昆虫进行分类后形成的一棵分类树.在面向对象的语言中,继承类似于昆虫之间的关系,只是它是针对类而言的,类可以从另一个类中继承特征.在C++中,一个类从另一个类继承特征,我们称为派生一个类,所派生的类称派生类,另一个类称为基类,类的派生可以无限继续下去.派生类不但继承了基类所有的成员变量和成员函数而且可以添加新的成员变量和成员函数. 在C++中,一个派生类可以从一个基类派生,也可以从多个基类派生,从一个基类派生的继承称为单继承;从多个基类派生的继承称为多继承.单继承形成了类的层次,像一棵倒挂的树;多继承形成一个有向无环圆.
单继承
我们定义一个类,用它来记录某公司雇员的姓名、年龄、薪水等,并打印这些项目.可以定义如下:
extern "c"{
#include
}
#include
class employee
{
private:
char *name;
short age;
float salary;
public:
employee()
{
name=0;
age=0;
salary=0.0;
}
employee(char *name1,short age1,float salary1)
{
name=new char[strlen(name1)+1];
strcpy(name,name1);
age=age1;
salary=salary1;
}
~employee()
{
delete []name;
}
};
现在假设除了雇员类之外,还需要表示公司的管理人员.从广义上来讲,他们也是雇员,有姓名、年龄和薪水等.然而,他们与普通雇员有不同之处,例如具有行政级别,可以管理其他雇员等.为此需要定义一个名为manager的新类.
当然可以把manager整个作成一个新类,但这样会重复employee中的内容.为了避免重复编码,C++支持从已有类派生新的类.这种从原有类派生出的类,就叫派生类.原有的类就称为基类,当一个类只有一个直接基类叫单继承.每当派生一个新类时,派生类继承原有类中所有数据成员和成员函数.例如,我们可以将manager定义如下:
class manager:public employee
{
};
表达式:public employee表示manager是从employee类中派生出来的,它继承了所有属于employee的数据成员和成员函数(关键字public使employee的所有公有成员再manager中仍为公有).因而,尽管对manager的定义为空,但它实际上具有employee的所有数据成员和成员函数,因此,下面的调用使合法的:
manager m;
m.print(); //打印manager的name,age和salary
在此处,manager类为派生类,而employee则为manager的基类.
一般情况下,派生类不会仅仅简单的继承基类的特点,继承时也应有"变异",例如, manag- er中的数据成员应能描述行政级别.因此,应在派生类中增加新的数据成员或成员函数.我们将manager类重新定义如下:
class manager:public employee
{
private:
int level;
public:
void print_level()
{
cout
}
};
这样,派生类manager除了继承employee类中的特征,还加入了私有成员level和公有成员函数print_level(),分别表示行政级别和级别打印.派生类继承了基类所有成员,它的公有成员与基类的公有成员一起提供了操纵派生类的公有接口.
派生类的初始化
派生类也可以有构造函数.为了初始化基类中的成员,派生类的构造函数常要使用初始化符表.例如在manager类中,就需要一个构造函数,用它来设置数据成员的值:
manager::manager(char *name1,short age1,float salary1,int level1):
employee(name1,age1,salary1)
{
level=level1;
}
这个构造函数的成员初始化符表调用基类的构造函数employee,传给它的值要赋给该基类中定义的数据成员.在初始化完employee后,manager接着初始化了level数据成员.现在就可以用这个构造函数来创建manager的实例了:
manager m("wang",31,457.30,19);
这样,m实例中所有的数据成员都设置了值.
派生类也可以使用缺省构造函数:
manager::manager()
{
level=0;
}
由于这个构造函数没有显式初始化它的基类(成员初始化符表都是空的),编译器会自动调用基类的缺省构造函数[employee::employee()],它将所有定义在基类中的数据成员设置为0.如果基类没有缺省的构造函数,就会出现错误结果.现利用manager的缺省构造函数来创建所有数据成员皆为0的实例:
manager m; //创建manager对象,所有数据成员设置为0
在创建一个派生类的实例时,编译器按以下次序调用构造函数:
1.先调用基类的构造函数;
2.再调用数据成员是类对象的构造函数,其调用次序按在类中定义的先后次序;
3.派生类自己的构造函数.
析构函数在定义它的地方调用,其次序和以上次序完全相反.这样,当某个具体的构造函数体被执行时,我们就可以知道基类和成员对象已经被初始化了,使用是安全的.同样,当某个具体的析构函数体被执行时,我们也可以知道基类和成员对象没有被撤消,仍可使用.
在C++中,派生类几乎可以继承基类的所有特征,但也有例外.下面的这些特征不为派生类继承:
1.构造函数
2.析构函数
3.用户定义的new运算符
4.用户定义的赋值运算符
5.友员关系
应注意,尽管基类的构造函数在实例化派生类时会自动执行,但一旦执行完毕后,就不能再在派生类中显式的执行它:
#include
extern "c"{
#include
}
class parent{
int value;
public:
parent(int v){value=v;}
};
class child:public parent{
int total;
public:
child(int t){total=t;}
void setTotal(int);
};
void Chind::setTotal(int t)
{
parent::parent(t); //出错,不能继承基类的构造函数
total=t;
}
类似的,也只有当对象出作用域时才自动执行基类的析构函数,但不允许在程序显式的使用它.友员关系也不能继承,这与现实生活很相似.你父亲的朋友不一定就自动成为了你的朋友,同样的道理,派生类也不能自动具有基类的友员关系.
多继承
在公司雇员的例子中,为了表示临时工,定义了一个temporary类:
class temporary{
protected:
int day;
public:
virtual void print_it() const
{
cout
}
temporary(){day=0;}
temporary(int d){day=d;}
//...
};
假定临时工的工作时间是按天计,temporary中定义了day数据成员来记录临时工工作时间,并提供公有成员函数print_it()来显示它.
现在需要增加一个顾问类.顾问既可以看成是管理者,它又是临时的.单从manager或temporary中的任一类派生它,都不能很好的重用原来的类,为此,将它同时从employee和manager中派生,定义如下:
class consultant:public manager,public temporary{
//...
};
结果,consultant具有两个直接基类manager和temporary.象这样,一个类如有一个以上的直接基类就叫多继承.相应的,当一个类只有一个直接基类叫单继承.在声明有多继承的类时,要用逗号把各个基类隔开,每个基类则由两部分组成:一部分是关键字private或public,它说明对基类是按私有方式继承还是按公有方式继承(省去时缺省按私有方式继承),另一部分是基类的名字.在consultant中,就声明它继承两个公有的基类manager和temporary.
有了consultant类之后,就可以使用它从manager和temporary中继承的成员(可访问
的):
consultant con;
con.print(); //调用manager::print()成员函数
con.print_it(); //调用temporary::print_it()成员函数
也可在派生类中添加成员,还可修改基类中的成员函数:
class consultant:public manager,public temporary
{
private:
float apsalary; //附加津贴
public:
void whoamI() //修改从manager继承的whoamI()成员函数
{
cout
}
void print_it() const //修改从temporary继承的成员函数
{
cout
cout}
//...
};
consultant类用构造函数来初始化成员时,由于要初始化基类,应使用成员初始化符表来调用各基类的构造函数:
consultant::consultant(char *name1,int age1,
float salary1,int level1,float apsalary1,
int day1):manager(name1,age1,salary1,level1),
temporary(day1){
apsalary=apsalary1;
}
在创建consultant的对象时,先调用基类的构造函数,由于consultant的定义中,manager基类比temporary基类先说明,因而先调用manager的构造函数.在初始化manager时,发现manager是从employee中派生出来的,先要调用employee的构造函数,接着才回来调用manager中的构造函数.在manager构造函数调用完毕之后,再调用temporary的构造函数,最后调用consultant自己的构造函数.在多继承中,构造函数也遵循先基类后派生类的规则.对于有多个直接基类的派生类,按在派生类定义中说明的顺序依次初始化.析构函数的调用顺序与构造函数的调用顺序相反.
二义性问题
在多继承中,如果两个基类之间有同名的成员,可能会产生二义性,例如:
class A{
public:
int a;
void f();
//...
};
class B{
public:
int a;
void f();
//...
};
class C:public A,public B{
//...
};
这时,下面的程序会产生二义性错误:
C cobject;
cobject.a=10; //出错,具有二义性
cobject.f(); //出错,具有二义性
这时因为编译器不知道cobject要使用A中的成员还是要使用B中的成员,为了消除此种二义性,应该用作用域分辨符::指明要用那个类中的成员:
C cobject;
cobject.A::a=10; //正确
cobject.B::f(); //正确
另外,如果派生类重新定义了同名成员,它将覆盖对基类的定义.这时再使用重新定义后的成员不会出错,编译器认为使用的是派生类中定义的版本.如果要使用基类中定义的版本,必须要用作用域分辨符::予以指明.例如,假如类C按下面形式定义:
class C:public A,public B{
public:
int a;
void f(){
//...
}
//...
};
下列语句都是正确的:
C cobject;
cobject.a=10; //使用C::a
cobject.f(); //调用C::f();
cobject.A::a=10; //使用A::a
cobject.B::f(); //调用B::f();
虚基类
假设有一个窗口类Window,类中存有窗口的一些信息,如长度、宽度等:
class Window{
protected:
float length;
float width;
//...
};
为了能在窗口内进行文字编辑,由Window类派生出了一个新类EditWindow:
class EditWindow:public Window{
//...
};
为了能在窗口内作图,还从Window类派生出另一个类PictureWindow:
class PictureWindow:public Window{
//...
};
现在需要生成一个用于排版的窗口,它不仅能进行文字处理,还能进行绘图,以便实现图文混排.这时,简单的把PictureWindow和EditWindow作为其基类是有问题的.因为一个窗口只能有唯一的长宽等特征,而新生成的排版窗口却保留有这些特征的两个拷贝.如果改变了其中的一个拷贝而没改变另一拷贝,很可能会导致不一致,况且,冗余的拷贝也很占存储空间.我们希望排版窗口仅保留Window的一个拷贝,也就是说Window的成员是由EditWindow和PictureWindow共享的.这可以通过把Window声明成虚基类来实现.具体的做法是,在声明Window的派生类时,在前加上virtual关键字.例如,下例说明Window是EditWindow和PictureWindow的虚基类:
class EidtWindow:public virtual Window{
//...
};
class PictureWindow:public virtual Window{
//...
};
现在,就可以声明只有一个Window拷贝的排版类:
class Edit_Picture_Window:public EditWindow,public Picture Window{
//...
};
如果一个类在它所有的派生类中都说明为虚基类,那么无论派生类对它继承了多少次,它始终只产生一个拷贝.例如,下面的定义与上例中排版类的定义是等价的:
class Edit_Picture_Window:public EditWindow,public Picture Window,
public virtual Window{
//...
};