原文链接:http://blog.163.com/zhoumhan_0351/blog/static/39954227201002854625174/
1、相关概念
在C++中可重用性(software reusability)是通过继承(inheritance)这一机制来实现的。一个新类从已有的类那里获得其已有特性,这种现象称为类的继承。从已有的类(父类)产生一个新的子类,称为类的派生。派生类是基类的具体化,而基类则是派生类的抽象。
基类名前面有public的称为“公用继承(public inheritance)”。
声明派生类的一般形式为
class 派生类名: [继承方式] 基类名
{
派生类新增加的成员
} ;
继承方式包括: public(公用的),private(私有的)和protected(受保护的),此项是可选的,如果不写此项,则默认为private(私有的)。所谓公用的,是说所定义的变量或函数在类外可以引用,而私有的,则说明定义的变量或函数只能在类内使用,受保护的意为不能被外界引用,但可以被派生类的成员引用。
构造一个派生类包括以下3部分工作:
(1) 从基类接收成员。派生类把基类全部的成员(不包括构造函数和析构函数)接收过来。
(2) 调整从基类接收的成员。接收基类成员是程序人员不能选择的,但是程序人员可以对这些成员作某些调整。
(3) 在声明派生类时增加的成员。
此外,在声明派生类时,一般还应当自己定义派生类的构造函数和析构函数,因为构造函数和析构函数是不能从基类继承的。
在派生类中,成员有4种不同的访问属性:
① 公用的,派生类内和派生类外都可以访问。
② 受保护的,派生类内可以访问,派生类外不能访问,其下一层的派生类可以访问。
③ 私有的,派生类内可以访问,派生类外不能访问。
④ 不可访问的,派生类内和派生类外都不能访问。
2、继承方式
(1) 公用继承(public inheritance)基类的公用成员和保护成员在派生类中保持原有访问属性,其私有成员仍为基类私有。
(2) 私有继承(private inheritance)基类的公用成员和保护成员在派生类中成了私有成员。其私有成员仍为基类私有。
(3) 受保护的继承(protected inheritance)基类的公用成员和保护成员在派生类中成了保护成员,其私有成员仍为基类私有。保护成员的意思是: 不能被外界引用,但可以被派生类的成员引用。
在定义一个派生类时将基类的继承方式指定为protected的,称为保护继承,用保护继承方式建立的派生类称为保护派生类(protected derived class),其基类称为受保护的基类(protected base class),简称保护基类。
stud.display( ); //调用基类的公用成员函数,输出基类中数据成员的值
stud.display_1();//调用派生类的公用成员函数,输出派生类中数据成员的值
对于不需要再往下继承的类的功能可以用私有继承方式把它隐蔽起来,这样,下一层的派生类无法访问它的任何成员。可以知道: 一个成员在不同的派生层次中的访问属性可能是不同的。它与继承方式有关。
私有继承和保护继承方式在使用时需要十分小心,一般不常用。在实际中,常用的是公用继承。
基类的私有成员被派生类继承后变为不可访问的成员,派生类中的一切成员均无法访问它们。如果需要在派生类中引用基类的某些成员,应当将基类的这些成员声明为protected,而不要声明为private。
3、继承类型
1)单继承
一个派生类只从一个基类派生,这称为单继承(single inheritance)。
2)多继承
一个派生类有两个或多个基类的称为多重继承(multiple inheritance)。应避免深层次继承和多继承。
如:
class D:public A,private B,protected C
{类D新增加的成员}
构造函数形式为:
派生类构造函数名(总参数表列): 基类1构造函数(参数表列), 基类2构造函数(参数表列), 基类3构造函数 (参数表列)
{派生类中新增数成员据成员初始化语句}
各基类的排列顺序任意。派生类构造函数的执行顺序同样为: 先调用基类的构造函数,再执行派生类构造函数的函数体。调用基类构造函数的顺序是按照声明派生类时基类出现的顺序。
Graduate(string nam,int a,char s, string t,float sco,float w):
Teacher(nam,a,t),Student(nam,s,sco),wage(w) { }
nam赋给了Teacher和Student两个类的名字属性,实质上是一个人。
而在成员函数中引用数据成员时指明其作用域,如
cout<<″name:″<<Teacher::name<<endl;则不会引起二义性。
A)二义性问题
(1) 两个基类有同名成员
可以用基类名来限定:
c1.A::a=3; //引用c1对象中的基类A的数据成员a
c1.A::display(); //调用c1对象中的基类A的成员函数display
(2)两个基类和派生类三者都有同名成员
基类的同名成员在派生类中被屏蔽,成为“不可见”的。
c1.display( );
c1.A::display(); //表示是派生类对象c1中的基类A中的成员函数display
c1.B::display(); //表示是派生类对象c1中的基类B中的成员函数display
(3) 如果类A和类B是从同一个基类派生的
c1.A::a=3; c1.A::display();//要访问的是类N的派生类A中的基类成员
4、派生类的访问属性
(1) 基类的成员函数可以访问基类成员。
(2) 派生类的成员函数可以访问派生类自己增加的成员。
(3) 基类的成员函数访问派生类的成员:
基类的成员函数只能访问基类的成员,而不能访问派生类的成员
(4) 派生类的成员函数访问基类的成员:
如继承中所讨论的。
(5) 在派生类外访问派生类的成员:
在派生类外可以访问派生类的公用成员,而不能访问派生类的私有成员。
(6) 在派生类外访问基类的成员:
如继承中所讨论的。
5、派生类的构造函数和析构函数
在设计派生类的构造函数时,不仅要考虑派生类所增加的数据成员的初始化,还应当考虑基类的数据成员初始化:在执行派生类的构造函数时,调用基类的构造函数。
简单的派生类只有一个基类,而且只有一级派生(只有直接派生类,没有间接派生类),在派生类的数据成员中不包含基类的对象(即子对象)。
1)简单的派生类的构造函数
其一般形式为
派生类构造函数名(总参数表列): 基类构造函数名(参数表列)
{派生类中新增数据成员初始化语句}
如
Student1(int n,string nam,char s,int a,string ad):Student(n,nam,s)
//派生类构造函数
{age=a; //在函数体中只对派生类新增的数据成员初始化
addr=ad;
}
在类中对派生类构造函数作声明时,不包括基类构造函数名及其参数表列,只在定义函数时才将它列出。
不仅可以利用初始化表对构造函数的数据成员初始化,而且可以利用初始化表调用派生类的基类构造函数,实现对基类数据成员的初始化。也可以在同一个构造函数的定义中同时实现这两种功能。
Student1(int n, string nam,char s,int a, string ad): Student(n,nam,s), age(a),addr(ad){}
执行构造函数的顺序是: ①派生类构造函数先调用基类构造函数;②再执行派生类构造函数本身(即派生类构造函数的函数体)。
在派生类对象释放时,先执行派生类析构函数~Student1( ),再执行其基类析构函数~Student( )。
2)有子对象的派生类的构造函数
定义派生类构造函数的一般形式为
派生类构造函数名(总参数表列): 基类构造函数名(参数表列),子对象名(参数表列)
{派生类中新增数成员据成员初始化语句}
执行派生类构造函数的顺序是:
① 调用基类构造函数,对基类数据成员初始化;
② 调用子对象构造函数,对子对象数据成员初始化;
③ 再执行派生类构造函数本身,对派生类数据成员初始化。
派生类构造函数的总参数表列中的参数,应当包括基类构造函数和子对象的参数表列中的参数。基类构造函数和子对象的次序可以是任意的,如果有多个子对象,派生类构造函数的写法依此类推,应列出每一个子对象名及其参数表列。
Student1(int n, string nam,int n1, string nam1,int a, string ad)
:Student(n,nam),monitor(n1,nam1) //派生类构造函数
{......}
#include <iostream>
#include <string>
using namespace std;
class Student
{
public:
Student(int n,string nam)
{
num=n;
name=nam;
}
void display()
{
cout<<"num:"<<num<<endl<<"name:"<<name<<endl;
}
protected:
int num;
string name;
};
class Student1:public Student
{
public:
Student1(int n,string nam,int n1,string nam1,int a,string ad):Student(n,nam),monitor(n1,nam1)
{
age=a;
addr=ad;
}
void show()
{
cout<<"this student is:"<<endl;
display();
cout<<"age:"<<age<<endl;
cout<<"addr:"<<addr<<endl<<endl;
}
void show_monitor()
{
cout<<endl<<"Class monitor is:"<<endl;
monitor.display();
}
private:
Student monitor;
int age;
string addr;
};
int main()
{
Student1 stud1(10010,"walg le",10001,"Li-sum",19,"115 beijing road,shanghai");
stud1.show();
stud1.show_monitor();
return 1;
}
3)多层派生时的构造函数
Student1(int n,char nam[10],int a):Student(n,nam)//派生类构造函数
Student2(int n, string nam,int a,int s):Student1(n,nam,a)
在声明Student2类对象时,调用Student2构造函数;在执行Student2构造函数时,先调用Student1构造函数;在执行Student1构造函数时,先调用基类Student构造函数。初始化的顺序是:
① 先初始化基类的数据成员。
② 再初始化Student1的数据成员。
③ 最后再初始化Student2的数据成员。
当不需要对派生类新增的成员进行任何初始化操作时,派生类构造函数的函数体可以为空,即构造函数是空函数
Student1(int n, strin nam,int n1, strin nam1):Student(n,nam),
monitor(n1,nam1) { }
如果在基类中没有定义构造函数,或定义了没有参数的构造函数,那么在定义派生类构造函数时可不写基类构造函数。
4)派生类的析构函数
在派生时,派生类不能继承基类的析构函数,需要通过派生类的析构函数去调用基类的析构函数。在派生类中可以根据需要定义自己的析构函数,用来对派生类中所增加的成员进行清理。在执行派生类的析构函数时,系统会自动调用基类的析构函数和子对象的析构函数,对基类和子对象进行清理。调用的顺序与构造函数相反: 先执行派生类自己的析构函数,对派生类新增加的成员进行清理,然后调用子对象的析构函数,最后调用基类的析构函数。
6、虚基类
从上面的学习中,我们发现如果一个派生类有多个直接基类,而这些直接基类又有一个共同的基类,则在最终的派生类中会保留该间接共同基类数据成员的多份同名成员。C++提供虚基类(virtual base class)的方法,使得在继承间接共同基类时只保留一份成员。
class A//声明基类A
{…};
class B :virtual public A //声明类B是类A的公用派生类,A是B的虚基类
{…};
class C :virtual public A //声明类C是类A的公用派生类,A是C的虚基类
{…};
注意: 虚基类并不是在声明基类时声明的,而是在声明派生类时,指定继承方式时声明的。因为一个基类可以在生成一个派生类时作为虚基类,而在生成另一个派生类时不作为虚基类。
声明虚基类的一般形式为
class 派生类名: virtual 继承方式 基类名
经过这样的声明后,当基类通过多条派生路径被一个派生类继承时,该派生类只继承该基类一次。为了保证虚基类在派生类中只继承一次,应当在该基类的所有直接派生类中声明为虚基类。
如果在虚基类中定义了带参数的构造函数,而且没有定义默认构造函数,则在其所有派生类(包括直接派生或间接派生的派生类)中,通过构造函数的初始化表对虚基类进行初始化。
class A//定义基类A
{A(int i){ } //基类构造函数,有一个参数
…};
class B :virtual public A //A作为B的虚基类
{B(int n):A(n){ } //B类构造函数,在初始化表中对虚基类初始化
…};
class C :virtual public A //A作为C的虚基类
{C(int n):A(n){ } //C类构造函数,在初始化表中对虚基类初始化
…};
class D :public B,public C //类D的构造函数,在初始化表中对所有基类初始化
{D(int n):A(n),B(n),C(n){ }
…};
在最后的派生类中不仅要负责对其直接基类进行初始化,还要负责对虚基类初始化。C++编译系统只执行最后的派生类对虚基类的构造函数的调用,而忽略虚基类的其他派生类(如类B和类C) 对虚基类的构造函数的调用,这就保证了虚基类的数据成员不会被多次初始化。
class Student1:virtual public Person
{
Student1(string nam,char s,int a,float sco): Person(nam,s,a), score (sco){}
protected:
float socre;
}
不提倡在程序中使用多重继承,只有在比较简单和不易出现二义性的情况或实在必要时才使用多重继承,能用单一继承解决的问题就不要使用多重继承。也是由于这个原因,有些面向对象的程序设计语言(如Java,Smalltalk)并不支持多重继承。
7、基类与派生类的转换
由于派生类中包含从基类继承的成员,因此可以将派生类的值赋给基类对象,在用到基类对象的时候可以用其子类对象代替。
(1) 派生类对象可以向基类对象赋值。
可以用子类(即公用派生类)对象对其基类对象赋值。如
A a1; //定义基类A对象a1
B b1; //定义类A的公用派生类B的对象b1
a1=b1; //用派生类B对象b1对基类对象a1赋值
在赋值时舍弃派生类自己的成员。赋值只是对数据成员赋值,对成员函数不存在赋值问题。子类型关系是单向的、不可逆的。B是A的子类型,不能说A是B的子类型。只能用子类对象对其基类对象赋值,而不能用基类对象对其子类对象赋值,因为基类对象不包含派生类的成员,无法对派生类的成员赋值。同理,同一基类的不同派生类对象之间也不能赋值。
(2) 派生类对象可以替代基类对象向基类对象的引用进行赋值或初始化。
A a1; //定义基类A对象a1
B b1; //定义公用派生类B对象b1
A& r=a1; //定义基类A对象的引用变量r,并用a1对其初始化
A& r=b1;//定义基类A对象的引用变量r,并用派生类B对象b1,对其初始化
此时r并不是b1的别名,也不与b1共享同一段存储单元。它只是b1中基类部分的别名,r与b1中基类部分共享同一段存储单元,r与b1具有相同的起始地址。
(3)如果函数的参数是基类对象或基类对象的引用,相应的实参可以用子类对象。(4) 派生类对象的地址可以赋给指向基类对象的指针变量,也就是说,指向基类对象的指针变量也可以指向派生类对象。
通过指向基类对象的指针,只能访问派生类中的基类成员,而不能访问派生类增加的成员。
8、继承和组合
对象成员的类型可以是本派生类的基类,也可以是另外一个已定义的类。在一个类中以另一个类的对象作为数据成员的,称为类的组合(composition)。
继承是纵向的,组合是横向的。
class Professor:public Teacher //教授类
{public:
┆
private:
BirthDate birthday; //BirthDate类的对象作为数据成员
};
对类库中类的声明一般放在头文件中,类的实现(函数的定义部分)是单独编译的,以目标代码形式存放在系统某一目录下。