面向对象程序设计有4个特点:抽象、封装、继承和多态性。其中继承是面向对象设计的最重要特征。继承实现了代码重用和代码扩充,因此,类的继承使得软件的可重用性成为可能。在C++中,给我们提供了这种继承机制, 理解继承是理解面向对象程序设计的关键。
1. 继承和派生
获得一个已有类的特性,称为继承,相应地,从一个已有类产生一个新的子类,称为派生。已有类称为父类或基类,新产生的类称为子类或派生类。一个派生类只从一个基类中继承,称为单继承,从多个基类继承称为多重继承。
派生类的声明方式是:calss 派生类名:[继承方式] 基类名{}。继承方式包括public、protected和private,后面会介绍。 如果类A和类B已定义,则单继承得到的类C和多重继承得到的类D的声明方式如下:
单继承声明方式:class C: public A { //类C新增加的成员 }
多重继承声明方式:class D: public A, private B { //类D新增加的成员 }
在大括号中,类的新增成员是派生类功能的扩展,可以增加新的成员函数和数据成员,也可以对基类的成员函数进行重写,覆盖原来的功能。
2. 类的继承方式和访问属性
类的成员可以有三个属性,public、protected和private。在成员前加上这三个访问限定符,成员就具有了不同的被访问属性。
public:公用的,加上这个修饰的类成员,可以在任何地方访问;
private:私有的,加上这个修饰的类成员,只能在本类里访问,别的地方不能访问;
protected:受保护的,加上这个修饰的类成员,只能在本类和派生类访问,其余地方不能访问。
对于类的继承方式也有三种,public(公用继承)、protected(受保护的继承)和private(私有继承)。不同的类成员访问属性和不同的继承方式结合,产生的派生类的成员具有不同的访问属性。基类的成员在派生类的访问属性可以用下表总结。
基类的访问特性 | 类的继承特性 | 子类的访问特性 |
Public Protected Private | Public | Public Protected No access |
Public Protected Private | Protected | Protected Protected No access |
Public Protected Private | Private | Private Private No access |
可以这样理解记忆表格,public权限最大,protected权限次之,private权限最小。对基类成员的访问属性和继承方式作交集,就得到了派生类相应成员的访问属性。对于多级派生,成员访问属性可以一级一级往下推,派生类A就从派生它的基类B得到,派生类B就从派生它的基类C得到,以此类推。
3. 派生类的构造函数和析构函数
构造函数
(1) 普通的派生类的构造函数
这里普通派生类指的是单继承。在类继承中,基类的构造函数是不能继承的,所以在声明派生类的时候,派生类的构造函数也要重写。读者会想,由于C++的封装性,我看不到基类构造函数的定义,不知道基类构造函数做了什么工作,怎么能自己去定义派生类的构造函数,但读者不用担心,C++给我们提供了相应的机制,我们可以在派生类的构造函数中可以调用基类的构造函数,问题就迎刃而解了。用基类的构造函数初始化从基类继承的数据成员,派生类中新增加的成员需要自己初始化。这样就不用知道基类的构造函数做了什么工作,而同样可以完成派生类的构造函数的初始化。
class people
{public:
people(int a,intb):weight(a),height(b){}
//people(int a,int b){weight=a;height=b;}
void disp(){cout<<"jilei"<<endl; }
private:
int weight;
int height;};
class student:public people
{public:
student(int d,int e,intc):people(d,e){age=c;}
//student(int d,int e,intc):people(d,e),age(c){}
voiddisp(){cout<<"zilei"<<age<<endl;}
private:
int age;};
派生类构造函数的形式为:
派生类构造函数名(总参数表):基类构造函数名(参数表){ 新增成员初始化 } 如:student(int d,int e,int c):people(d,e){ age=c; }
也可以用参数列表的方式,将函数体内的赋值语句和基类构造函数参数表放一起:派生类构造函数名(总参数表):基类构造函数名(参数表),新增成员初始化参数表{ } 如: student(int d,int e,intc):people(d,e),age(c){}
需要注意的如果是①在派生类中对构造函数声明时,不包括基类构造函数和参数列表,而是在定义时才用它。②派生类对象建立时,派生类构造函数执行过程是:先调用基类的构造函数,再执行派生类构造函数本身。派生类对象释放时,先调用派生类析构函数,再调用基类析构函数。
(2) 多层次继承的派生类的构造函数
多层次继承的原理和普通继承的原理类似,只要把上面普通继承(一次继承)过程看明白,对于多层次继承就是一层一层往下继承。在上面例子的基础上,添加一个类,它从student类继承而来。
class student: public people{ };
class graduate: public student
{public:
graduate(inta1,int b,int c,int d):student(a1,b,c){socore=d;}
private:
intsocore;};
需要注意的是,多层次继承中的构造函数,只写它上一层的构造函数。定义graduate对象时,调用graduate类的构造函数,graduate类的构造函数调用student的构造函数,student的构造函数调用people类的构造函数,这样一层一层往上,直到最后的基类。因此数据成员的初始化顺序也是先初始化people类的数据成员,然后初始化student类新增的数据成员,最后初始化graduate类新增的数据成员。
(3) 含有子对象的派生类的构造函数
前面介绍的类,数据成员都是标数据类型,如int、float、string等,在抽象数据类型中,类也可以看成是一种数据类型,因此,可以在定义派生类A的时候,将一个已定义的类B作为这个类的一个成员,称A为含有子对象的类。对于这样的类应该怎么写构造函数初始化对象呢?
这种情况下,派生类的构造函数包括三个任务,对基类数据成员初始化,对子对象数据成员初始化和对新增数据成员初始化。派生类构造函数的一般形式为:派生类构造函数名(总参数表):基类构造函数名(参数表),子对象名(参数表),新增数据成员(初始化参数){ }
和前面一样,新增数据成员的初始化可以写在函数体里面。如果有多个子对象,派生类构造函数应列出每一个子对象名及其参数列表。其中基类的构造函数表列和子对象参数表列排序并没有关系,但一般将基类构造函数表列写在前面。
(4) 多重派生类的构造函数
前面讲的都是单继承,即派生类是从一个基类继承得到的,如果一个派生类从多个类继承得到,称多重继承,这和多层次继承是不一样的概念。多重继承的构造函数写法:
派生类构造函数名(总参数表):基类1构造函数(参数列表)基类2构造函数(参数列表)…基类N构造函数(参数表列),新增成员(初始化参数){}
下面这个例子给出了多重继承时派生类构造函数的写法,对于总参数表中的参数,可以多次传递给基类的构造函数或新增成员参数初始化表,如例子里派生类student的构造函数中参数str分别用了两次。
#include<iostream>
#include<string>
using namespace std;
class people
{public:
people(int a,int b,stringstr):weight(a),height(b),name(str)
{
cout<<"people:"<<name<<endl<<endl;
}
protected:
string name;
int weight;
int height;};
class grade
{public :
grade(int a):socore(a){}
protected:
int socore;};
class student:public people,public grade
{public:
student(int a,int b,int c,string str):people(a,b,str),grade(c),name(str){}
void disp()
{
cout<<"name:"<<name<<endl;
cout<<"weight:"<<weight<<endl;
cout<<"height:"<<height<<endl;
cout<<"socore:"<<socore<<endl;
}
private:
string name;};
int main()
{ studenta(65,178,80,"haha");
a.disp();
return 0; }
多重继承产生的二义性
如果类C是从类A和类B继承得到,在A和B中都存在同名变量,int age,且在C中都继承了下来。如下图所示。对于类C实例化的对c,应该怎么访问从A或B中继承得到的变量。这时需要用到作用域限定符“::”来限定具体的变量。如:
C c;
c.age=0; //访问类C的新增变量age
c.A::age=1; //访问类C中从类A中继承的变量age
c.B::age=2; //访问类C中从类B中继承的变量age
如果在类C的成员函数中访问age,则可以省去“c.”,只用A::age=1。对于同名的成员函数,访问方法和数据成员类似,不再赘述。
多重继承中使用虚基类
如果类A和类B是从同一个基类C派生的,类N是从类A和类B多重派生的,如下图所示。则类N就相当于间接地从类C中继承了两次同样的成员,相当于多了一份拷贝,这会浪费我们的存储资源,而且在访问的时候需要加上作用域限定符。C++提供的虚基类可以间接继承同一个类的时候只保留一份成员。
虚基类的声明方式为:
class A{ A(int a)};
class B:virtual public A{ B(int a):A(a)};
class C:virtual public A{ C(int a):A(a)};
class D:public B,public C{ B(int a):A(a),B(a),C(a)};
虚基类声明时,不是在声明基类时声明的,而是在声明派生类声明的。而且在构造函数初始化中,不但要对直接基类进行初始化,还有对虚基类初始化。这时因为虚基类在派生类中只有一份数据,如果用类B和C进行初始化虚基类,可能不存在数据成员。
析构函数
析构函数的作用是对象在销毁时完成清理工作,析构函数不能重载,不带返回值类型和参数列表。和构造函数类似,派生类也不能继承基类的析构函数。派生类析构函数清理派生类新增成员,基类析构函数清理基类数据成员。我们可以自己定义析构函数,或者由编译系统自动产生析构函数。
对于普通的继承而言,析构函数的调用顺序和构造函数相反,一般会先调用派生类的析构函数,然后调用基类的析构函数。
4. 关于继承和派生的几点说明
(1) 继承与派生的转换
虽然继承方式有三种,public、protected和private,但一般用的最多还是public继承,它除了基类的私有成员外,都按照原样保留了下来。那么这种public继承方式产生的子类和基类之间能不能进行抽象数据类型的转换。可以转换的情况具体表现在下面几个方面。
①派生类对象可以通过赋值操作符“=”向基类对象或基类对象的引用赋值。在数据类型转换中我们提到过,运算符重载可以被编译系统隐式的执行,但对于自己定义的抽象数据类型,需要用户自己写转换函数。这里继承产生的子类和基类之间的赋值,虽然没写转换函数,但也可以转换,这是操作系统自己完成的。值得注意的是,只能用子类对象去赋值给基类对象,反之不行,原因很简单,基类对象中不存在相应子类对象中新定义的数据成员,自然是不能用基类对象赋值子类对象。子类对基类赋值也是一直“截断赋值”,即只对继承过来的数据成员赋值,而子类新增加的数据成员舍弃不用。
②函数传参中,如果参数是基类对象或基类对象的引用,实参可以用子类对象。
③派生类对象的地址可以赋值给指向基类对象的指针,指针的值为基类对象的地址,但反之不行。也就是说,派生类对象地址可以转换为基类对象地址,基类对象地址不能转换为派生类对象地址。(单向的强制类型转换)如:
定义两个类:class people 和 class student:public people
实例化两个对象: people A; studentB;
用子类对象的地址赋给基类指针:people* pt=&B; //pt指向基类对象A的地址
但如果反过来:student* pt=&A; //报错cannot convert from 'class people *' to 'classstudent *'
(2) 函数的覆盖和重载
覆盖(重载)函数的函数名必须是一样的,覆盖函数的参数表必须和被覆盖的函数的参数表一样,重载函数的参数表必须和被重载函数的参数表不一样。重载是让同名的方法根据不同的参数类型进行不同的处理并可能返回不同类型的数据。而覆盖则与作用域有关了,在子类中与父类同名的方法,在子类中可以重写从父类继承下来的方法,这样子类中父类的方法被屏蔽了。覆盖和重载可以总结为下面几点。
1、方法的覆盖是子类和父类之间的关系,是垂直关系;方法的重载是同一个类中方法之间的关系,是水平关系。
2、覆盖只能由一个方法,或只能由一对方法产生关系;方法的重载是多个方法之间的关系。
3、覆盖要求参数列表相同;重载要求参数列表不同。
4、覆盖关系中,调用那个方法体,是根据对象的类型(对象对应存储空间类型)来决定;重载关系,是根据调用时的实参表与形参表来选择方法体的。
对于覆盖和重载会在多态性和虚函数继续讲解。
(3) 继承与组合
类包括数据成员和成员函数,一个类是一个抽象数据类型,它可以像基本数据类型那样作为另一个类的数据成员,像这样的在一个类中以另一个类的对象作为数据成员,称为类的组合。如:
class people
{public:
people();
private:
intweight;
intheight;};
class student
{public:
student();
private:
peoplezhangsan;
int age;};
类的继承和组合都扩展了已有类的利益率,但两者是不同的概念,继承类似于遗传关系、父与子的关系;而组合类似于包含的关系,像上面的例子,类student就包含了类people的属性。
(4) 派生类构造函数的特殊形式
我们知道构造函数最重要的作用是产生实例化的对象,最主要的作用是初始化对象的数据成员。当我们定义一个类的时候,如果只想产生这个对象,而不给对象的成员初始化,可以不用定义构造函数,使用默认的构造函数。所以,在派生类的构造函数中,就出现了不需要初始化数据成员的现象。
①如果派生类新增的数据成员不需要初始化,那么派生类的构造函数体可以为空。
②在继承过程中,如果基类没有定义构造函数,即基类使用系统提供的默认构造函数,那么派生类的构造函数可以不写基类的构造函数,只完成对派生类新增数据成员的初始化工作。即派生类构造函数不向基类构造函数传递参数,编译系统自动调用基类的构造函数。
③如果基类没有显示定义构造函数,派生类中新增数据成员也不用初始化,则可以不定义派生类的构造函数,编译系统自动产生派生类默认构造函数。定义派生类对象时,派生类默认构造函数调用基类默认构造函数,产生派生类的对象。
④如果基类定义了构造函数或进行了构造函数的重载,则在派生类里必须定义派生类的构造函数,并可以通过基类的构造函数的参数决定调用哪一个基类的构造函数。
(5) 尽量用简单继承
继承虽然有简单继承、多层次继承和多重继承,但一般使用简单继承的情况最多。因为多继承还是比较复杂的,无论是派生类成员的操作还是构造函数都比简单继承复杂一些,因此,提倡尽量用简单继承解决问题。
参考文献:
http://blog.csdn.net/insistgogo/article/details/6645215
http://blog.csdn.net/candyliuxj/article/details/4598640