第五章 继承与派生
5.1 继承和派生
继承和派生的概念
继承:在定义一个新的类B时,如果该类与某个已有的类A相似(
指的是B拥有A的全部特点),那么就可以把A作为一个
基类,而把B作为基类的一个
派生类(也称
子类)。
派生类是通过对基类进行修改和扩充得到的。在派生类中,可以扩充新的成员变量和成员函数。
派生类一经定义后,可以独立使用,不依赖于基类。
派生类拥有基类的
全部成员函数和成员变量,不论是private、protected、public 。在派生类的各个成员函数中,不能访问基类中的private成员。
需要继承机制的例子
所有的学生都有的共同属性:姓名、学号、性别、成绩 所有的学生都有的共同方法(成员函数):是否该留级、是否该奖励
而不同的学生,又有各自不同的属性和方法。研究生:导师、系,大学生:系,中学生:竞赛特长加分
如果为每类学生都从头编写一个类,显然会有不少重复的代码,浪费。比较好的做法是编写一个“学生”类,概括了 各种学生的共同特点,然后从“学生”类派 生出“大学生”类,“中学生”类,“研究 生类”。
派生类的写法
class 派生类名:public 基类名
{
};
例子
class CStudent
{
private:
string sName;
int nAge;
public:
bool IsThreeGood() { };
void SetName( const string & name )
{ sName = name; }
//......
};
class CUndergraduateStudent: public CStudent
{
private:
int nDepartment;
public:
bool IsThreeGood() { ...... }; //覆盖
bool CanBaoYan() { .... };
}; // 派生类的写法是:类名: public 基类名
覆盖:派生类中的成员函数和基类中的完全一样,但行为可能需要不一样,所以在派生类中对函数的行为重新编写。这里感觉应该是隐藏,而不是覆盖,至于重载、覆盖、隐藏等的区别,后面有时间专门写个博客总结下。
派生类对象的内存空间
派生类对象的体积,等于基类对象的体积,再加上派生类对象自己的成员变量的体积。
在派生类对象中,包含着基类对象,而且基类对象的存储位置位于派生类对象新增的成员变量
之前。
class CBase
{
int v1,v2;
};
class CDerived:public CBase
{
int v3;
};
继承实例程序:学籍管理
#include <iostream>
#include <string>
using namespace std;
class CStudent
{
private:
string name;
string id; //学号
char gender; //性别,'F'代表女,'M'代表男
int age;
public:
void PrintInfo();
void SetInfo( const string & name_,const string & id_,int age_, char gender_ );
string GetName() { return name; }
};
class CUndergraduateStudent:public CStudent
{//本科生类,继承了CStudent类
private:
string department; //学生所属的系的名称
public:
void QualifiedForBaoyan() { //给予保研资格
cout << “qualified for baoyan” << endl;
}
void PrintInfo() {
CStudent::PrintInfo(); //调用基类的PrintInfo
cout << “Department:” << department <<endl;
}
void SetInfo( const string & name_,const string & id_,int age_,char gender_ ,const string & department_) {
CStudent::SetInfo(name_,id_,age_,gender_); //调用基类的SetInfo
department = department_;
}
};
void CStudent::PrintInfo()
{
cout << "Name:" << name << endl;
cout << "ID:" << id << endl;
cout << "Age:" << age << endl;
cout << "Gender:" << gender << endl;
}
void CStudent::SetInfo( const string & name_,const string & id_,int age_,char gender_ )
{
name = name_;
id = id_;
age = age_;
gender = gender_;
}
int main()
{
CUndergraduateStudent s2;
s2.SetInfo(“Harry Potter ”, “118829212”,19,‘M’,“Computer Science”);
cout << s2.GetName() << “ ” ;
s2.QualifiedForBaoyan ();
s2.PrintInfo ();
return 0;
}
输出结果:
Harry Potter qualified for baoyan
Name:Harry Potter
ID:118829212
Age:19
Gender:M
Department:Computer Science
像在派生类的PrintInfo函数中那样,先调用了基类里面的同名成员函数,完成跟基类有关的事。然后再写一些代码,完成跟派生有关的事。这种做法实际上是特别常见的。
5.2 继承关系和复合关系
类之间的两种关系
- 类之间有三种关系:没关系,继承关系,复合关系。
- 继承:“是”关系。
– 基类 A,B是基类A的派生类。
– 逻辑上要求:“一个B对象也是一个A对象”。 - 复合:“有”关系。
– 类C中“有”成员变量k,k是类D的对象,则C和D是复合关系
– 一般逻辑上要求:“D对象是C对象的固有属性或组成部分”。
继承关系的使用
先写了一个CMan类,后来需要一个CWoman类,不能直接继承,因为一个女人不是一个男人。逻辑上是不合理的。好的做法是
概括男人和女人共同特点,写一个 CHuman类,代表“人”,然后CMan和CWoman都从CHuman派生。
复合关系的使用
几何形体程序中,需要写“点”类,也需要写“圆”类,两者的关系就是复合关系 ---- 每一个“圆”对象里都包含(
有)一个“点”对象,这个“点”对象就是圆心
class CPoint
{
double x,y;
friend class CCircle;
//便于Ccirle类操作其圆心
};
class CCircle
{
double r;
CPoint center;
};
复合关系的使用——复杂版本
如果要写一个小区养狗管理程序,需要写一个“业主”类,还需要写一个“狗”类。而狗是有 “主人” 的,主人当然是业主(假定狗只有一个主人,但一个业主可以有最多10条狗)
- 错误的写法:
class CDog;
class CMaster {
CDog dogs[10];
};
class CDog {
CMaster m;
};
如果我问你一个CMaster的对象,是多少个字节你会算吗?你会说他里面有10个CDog的对象,所以它的体积是10倍的CDog的体积。那么一个CDog的对象又占多少个字节呢?哦,我们看一个CDog的对象,它包含一个CMaster的对象,它的体积应该跟一个CMaster对象一样。这不就
循环定义了吗?
- 另一种写法:为“狗”类设一个“业主”类的成员对象;为“业主”类设一个“狗”类的对象指针数组。
class CDog;
class CMaster {
CDog * dogs[10];
};
class CDog {
CMaster m;
};
我们看到在这种写法里面,狗中有人。两条狗它的主人有可能是共同的一个人,对吧?那在这种情况下我们就会牵涉到一个问题,就是如何维护不同的狗里面,相同的主人他的信息的一致性的问题。就比方说,我,我改变了这条狗里面所包含的主人信息,那么,另外一条狗它的主人和另外一条狗是相同的;而另外一条狗里面的主人的信息也要相应的进行修改。那么,维护相同主人的多条狗里面的所包含的多个主人对象的信息的一致性是一件很罗嗦的事情。所以这个写法是不正确的。
- 凑合的写法:为“狗”类设一个“业主”类的对象指针;为“业主”类设一个“狗”类的对象数组。
class CMaster; //CMaster必须提前声明,不能先写CMaster类后写Cdog类 这里用到了业主类,所以我们前面把业主类提前声明一下。
class CDog {
CMaster * pm;
};
class CMaster {
CDog dogs[10];
};
这样子的话 看上去似乎问题不大,但却是也不太好。在这种情况下,人中有狗。然后,每一条狗都有一个指针,指向它的主人。
这个不好在哪儿呢? 首先事实上从逻辑上来讲,我们要求复合关系一般是这样的。如果有一个类它有成员对象,那么我们就会要求这个成员对象是这个类的一个固有的这个属性,是它的组成部分或者固有的属性。那我们在这个主人类里面有10条狗,有10个狗的对象,我们能说这个 狗它是主人的一部分吗?或者我们能说这个狗是主人的固有属性吗? 好像从逻辑上来讲是有那么一点别扭。
实际上这种做法,还有一个比较严重的不好的地方。就在于在这种做法里面,狗狗都失去了自由,失去了独立的狗格。为什么这么讲呢?因为我们看到所有的狗的对象都被包含在 一个或者多个主人对象里面了。那我们要对这些狗对象操作的话,就要通过他的主人来进行。这些狗就没有自由了,他们不能独立活动了。所以从逻辑上来讲,这个做法是不太好的。
- 正确的写法:为“狗”类设一个“业主”类的对象指针;为“业主”类设一个“狗”类的对象指针数组。
class CMaster; <span style="font-family: Arial, Helvetica, sans-serif;">//CMaster必须提前声明,不能先写CMaster类后写Cdog类 这里用到了业主类,所以我们前面把业主类提前声明一下。</span>
class CDog {
CMaster * pm;
};
class CMaster {
CDog * dogs[10];
};
5.3 基类/派生类同名成员和protected访问范围说明符
基类和派生类有同名成员的情况
class base {
int j;
public:
int i;
void func();
};
class derived : public base{
public:
int i;
void access();
void func();
};
void derived::access()
{
j = 5; //error j是基类的私有成员
i = 5; //引用的是派生类的 i
base::i = 5; //引用的是基类的 i
func(); //派生类的
base::func(); //基类的
}
derived obj;
obj.i = 1;
obj.base::i = 1;
Note: 一般来说,基类和派生类不定义同名成员变量。
访问范围说明符
- 基类的private成员: 可以被下列函数访问
• 基类的成员函数
• 基类的友员函数 - 基类的public成员: 可以被下列函数访问
• 基类的成员函数
• 基类的友员函数
• 派生类的成员函数
• 派生类的友员函数
• 其他的函数
访问范围说明符: protected
- 基类的protected成员: 可以被下列函数访问
• 基类的成员函数
• 基类的友员函数
• 派生类的成员函数可以访问当前对象的基类的保护成员
示例:保护成员
class Father {
private: int nPrivate; //私有成员
public: int nPublic; //公有成员
protected: int nProtected; // 保护成员
};
class Son : public Father {
void AccessFather () {
nPublic = 1; // ok;
nPrivate = 1; // wrong
nProtected = 1; // OK, 访问从基类继承的protected成员
Son f;
f.nProtected = 1; //<span style="color:#ff0000;">wrong, f不是当前对象 因为f.nProtected不是我们当前这个accessFather这个函数所面对的那个对象的,它是另外一个,所以呢,这个不是当前对象的protected成员,我们也一样是不能够访问的。</span>
}
};
int main(){
Father f;
Son s;
f.nPublic = 1; // Ok
s.nPublic = 1; // Ok
<span style="color:#3333ff;">f.nProtected = 1; // error
f.nPrivate = 1; // error
s.nProtected = 1; //error
s.nPrivate = 1; // error</span>
return 0;
}
要注意,protected成员只能是
派生类成员函数在这个
派生类当前的这个对象中间可以被访问到。
5.4 派生类和组合(包含成员对象的类)的构造函数
派生类的构造函数
- 派生类对象 包含 基类对象
- 执行派生类构造函数之前, 先执行基类的构造函数
- 派生类交代基类初始化, 具体形式:
构造函数名(形参表): 基类名(基类构造函数实参表)
{
}
class Bug {
private :
int nLegs; int nColor;
public:
int nType;
Bug (int legs, int color);
void PrintBug () { };
};
class FlyBug: public Bug { // FlyBug是Bug的派生类
int nWings;
public:
FlyBug(int legs, int color, int wings);
};
Bug::Bug( int legs, int color) {
nLegs = legs;
nColor = color;
}
//错误的FlyBug构造函数:
FlyBug::FlyBug (int legs, int color, int wings) {
nLegs = legs; // 不能访问
nColor = color; // 不能访问
nType = 1; // ok
nWings = wings;
}
//正确的FlyBug构造函数:
FlyBug::FlyBug (int legs, int color, int wings):Bug(legs, color) {
nWings = wings;
}
int main()
{
FlyBug fb ( 2,3,4);
fb.PrintBug();
fb.nType = 1;
fb.nLegs = 2 ; // error.nLegs is private
return 0;
}
在 创建 派生类的对象 时,
• 首先需要调用 基类的构造函数:初始化派生类对象中从基类继承的成员• 在执行一个派生类的构造函数之前,总是先执行基类的构造函数
派生类对象生成时调用基类构造函数的两种方式
• 显式方式:
派生类的构造函数中—>基类的构造函数提供参数 derived::derived(arg_derived-list):base(arg_base-list)
派生类的构造函数中—>基类的构造函数提供参数 derived::derived(arg_derived-list):base(arg_base-list)
• 隐式方式:
派生类的构造函数中, 省略基类构造函数时,即派生类的构造函数自动调用基类的默认构造函数
派生类的构造函数中, 省略基类构造函数时,即派生类的构造函数自动调用基类的默认构造函数
派生类的析构函数被执行时, 执行完派生类的析构函数后, 自动调用基类的析构函数
总结:先基类的构造函数,再派生类的构造函数;先派生类的析构函数,再基类的析构函数。
(生成一个派生类对象时,自顶向下地调用基类的构造函数;析构,逆序。)
(生成一个派生类对象时,自顶向下地调用基类的构造函数;析构,逆序。)
class Base {
public:
int n;
Base(int i):n(i)
{ cout << "Base " << n << " constructed" << endl; }
~Base()
{ cout << "Base " << n << " destructed" << endl; }
};
class Derived:public Base {
public:
Derived(int i):Base(i)
{ cout << "Derived constructed" << endl; }
~Derived()
{ cout << "Derived destructed" << endl; }
};
int main()
{
Derived Obj(3);
return 0;
}
输出结果:
Base 3 constructed
Derived constructed
Derived destructed
Base 3 destructed
包含成员对象的派生类的构造函数
class Skill {
public:
Skill(int n) { }
};
class FlyBug: public Bug {
int nWings;
Skill sk1, sk2;
public:
FlyBug(int legs, int color, int wings);
};
FlyBug::FlyBug( int legs, int color, int wings):
Bug(legs, color), sk1(5), sk2(color) {
nWings = wings;
}
创建 派生类的对象 时, 执行
派生类的构造函数
之前:
• 首先调用
基类 的构造函数—>初始化派生类对象中从基类继承的成员
• 然后调用 成员对象类 的构造函数—>初始化派生类对象中成员对象
• 然后调用 成员对象类 的构造函数—>初始化派生类对象中成员对象
执行完
派生类的析构函数 后:
• 首先调用 成员对象类 的析构函数
• 然后调用 基类 的析构函数
• 首先调用 成员对象类 的析构函数
• 然后调用 基类 的析构函数
析构函数的调用顺序与构造函数的调用顺序相反
总结:创建派生类对象时,先基类的构造函数,然后成员对象类的构造函数,再派生类的构造函数;
派生类对象结束时,先派生类的析构函数,然后成员对象类的析构函数,再基类的析构函数。
5.5 public继承的赋值兼容规则
public继承的赋值兼容规则
class base { };
class derived : <span style="color:#ff0000;">public</span> base { };//这个地方写的是public,为公有派生。事实上,也可以写private或protected,不过很少用。
base b;
derived d;
上例中继承时,写的是public,为公有派生。事实上,也可以写private或protected,不过很少用。在public公有派生情况下,有以下几条赋值规则:
1). 派生类的对象可以赋值给基类对象 b = d;
- 一个派生类对象本来就是一个基类对象。那么, 你把派生类对象赋值给基类对象就是没有问题的。
- 这个赋值号,在没有经过重载的情况下,它做的事情是什么呢?那当然就是,我们知道d里面是包含一个base对象的对不对?那么实际上就是把d里面所包含的这个base对象的内容,拷贝到这个b里面去。
- 如果你把这个赋值表达式反过来,写d=b,那就是不行的了。因为一个基类对象,并不是一个派生类对象,所以你不能把基类对象赋值给派生类对象。
2). 派生类对象可以初始化基类引用 base & br = d;
- 既然一个派生类对像又是一个基类对象,那么你让一个基类的引用去引用一个派生类的对象,也是没有问题的。
- 你可以认为,这个基类的引用,实际上它就引用了派生类对象里面,所包含的那个基类对象。
3). 派生类对象的地址可以赋值给基类指针base * pb = & d;
- 那基类的指针指向了一个派生类的对象。因为派生类对象就是一个基类对象,所以这个没有问题。
- 你也可以认为,这个指针,就指向了派生类对象里面所包含的那个基类对象。那本来派生类对象里面所包含的那个基类对象就是放在派生类对象存储空间的最前面,也就是说那个基类对象的起始地址就是整个派生类对象的起始地址,所以你让基类的指针指向派生类对象,没有问题。
•
如果派生方式是 private或protected,则上述三条不可行。
直接基类和间接基类
类A派生类B,类B派生类C,类C派生类D,……
– 类A是类B的直接基类
– 类B是类C的直接基类,类A是类C的间接基类
– 类C是类D的直接基类,类A、B是类D的间接基类
– 类A是类B的直接基类
– 类B是类C的直接基类,类A是类C的间接基类
– 类C是类D的直接基类,类A、B是类D的间接基类
在声明派生类时,只需要列出它的直接基类
– 派生类沿着类的层次自动向上继承它的间接基类
– 派生类沿着类的层次自动向上继承它的间接基类
– 派生类的成员包括
• 派生类自己定义的成员
• 直接基类中的所有成员
• 所有间接基类的全部成员
• 派生类自己定义的成员
• 直接基类中的所有成员
• 所有间接基类的全部成员
示例
#include <iostream>
using namespace std;
class Base {
public:
int n;
Base(int i):n(i) {
cout << "Base " << n << " constructed" << endl;
}
~Base() {
cout << "Base " << n << " destructed" << endl;
}
};
class Derived:public Base
{
public:
Derived(int i):Base(i) {
cout << "Derived constructed" << endl;
}
~Derived() {
cout << "Derived destructed" << endl;
}
};
class MoreDerived:public Derived {
public:
MoreDerived():Derived(4) {
cout << "More Derived constructed" << endl;
}
~MoreDerived() {
cout << "More Derived destructed" << endl;
}
};
int main()
{
MoreDerived Obj;
return 0;
}
输出结果:
Base 4 constructed
Derived constructed
More Derived constructed
More Derived destructed
Derived destructed
Base 4 destructed
在这个MoreDerived里面,也有构造函数,在这构造函数的初始化列表里面,它只需要指名直接基类是如何初始化的就行了。它不需要指名那些间接基类如何初始化。那些间接基类如何初始化是由它的直接基类的构造函数来决定的。