五、继承与派生
1. 继承与派生
在C++中,比如有两个类,新类拥有原有类的全部属性叫做继承!原有类产生新类的过程叫做派生!
把原有的这个类称之为父类或基类,由基类派生出的类叫做派生类或者叫做子类。从名字上就可以看出他们的关系。
那么继承和派生有什么好处呢?为什么C++要有这种机制呢?
- 体现面向对象的编程思想,更好的表达各类型之间的关系。
- 派生类除了可以继承基类的全部信息外,还可以添加自己的那些不同的、有差异的信息,就像生物进化的道理一样,派生类在拥有基类的全部基础之上还将更强大。
- 派生类继承到基类的成员是自动、隐藏的拥有,即不需要我们重新定义,这就节省了大量的代码,体现了代码重用的软件工程思想。
下面,我们来看实际代码的实现,如何继承,实现派生类。
#include<iostream>
using namespace std;
class Clock
{
private:
int H;
int M;
int S;
public:
int SetTime(int h,int m,int s)
{
this->H = h;
this->M = m;
this->S = s;
return 0;
}
int ShowTime()
{
cout<<"Now:"<<H<<":"<<M<<":"<<S<<endl;
return 0;
}
};
class AlarmClock:public Clock
{
private:
int AH;
int AM;
public:
int SetAlarm(int AH,int AM)
{
this->AH = AH;
this->AM = AM;
return 0;
}
int ShowAlarm()
{
cout<<"AlarmTime:"<<AH<<":"<<AM<<endl;
return 0;
}
};
int main()
{
AlarmClock A;
A.SetTime(19,15,50);
A.ShowTime();
A.SetAlarm(5,30);
A.ShowAlarm();
return 0;
}
2. 三种继承方式
在上一节中,我们看到了派生类的定义方法,用到了public的公有继承,实际这里一共有三种方式,分别是 公有继承、私有继承、保护继承。
不同的继承方式,主要区别在于基类中不同访问权限的成员在派生类中的访问权限变化情况。
先说说公有继承,特点如下:
- 基类中的公有成员,在派生类中仍然为公有成员。当然无论派生里的成员函数还是派生类对象都可以访问。
- 基类中的私有成员,无论在派生类的成员还是派生类对象都不可以访问。
- 基类中的保护成员,在派生类中仍然是保护类型,可以通过派生类的成员函数访问,但派生类对象不可以访问!
第二种,私有继承。
在私有继承的情况下,公有类型、私有类型、受保护类型三种成员的访问权限如下:
- 基类的公有和受保护类型,被派生类私有继承吸收后,都变为派生类的私有类型,即在类的成员函数里可以访问,不能在类外访问。
- 而基类的私有成员,在派生类无论类内还是类外都不可以访问。
可以看出来,如果为私有派生,则基类的私有成员在派生类甚至再派生出的子类中,都无法再使用。没有什么存在意义,故这种使用情况比较少。
第三种,保护继承
保护类型的继承,特点如下:
- 基类的公有成员和保护类型成员在派生类中为保护成员。
- 基类的私有成员在派生类中不能被直接访问。
可以看出,派生类里的成员函数可以访问基类的公有成员和保护成员,但在类外通过派生类对象则无法访问它们。同样,无论派生类里的成员函数还是通过类对象都无法访问基类中的私有成员。
是不是看混了?下面总结一下
- 基类私有成员,无论什么派生权限,派生类内成员函数和类外都是不可以访问的。
- 私有继承,无论基类原来什么类型,在派生类外都不可以访问。
- 派生类从基类中吸收的成员的访问权限为基类中访问权限和派生时派生权限两者之中最低的一种。
如下表:
3. 派生类的构造函数
由于派生类包含基类的原因,我们在创建一个派生类的时候,系统会先创建一个基类。需要注意的是,派生类会吸纳基类的全部成员,但并不包括构造函数及后面讲的析构函数,那么就意味着创建派生类在调用自己的构造函数之前,会先调用基类的构造函数。
通过代码来验证:
#include<iostream>
using namespace std;
class Clock
{
private:
int H;
int M;
int S;
public:
Clock()
{
cout<<"Clock's Constructor Called!"<<endl;
}
};
class AlarmClock:public Clock
{
private:
int AH;
int AM;
public:
AlarmClock()
{
cout<<"AlarmClock's Constructor Called!"<<endl;
}
};
int main()
{
AlarmClock A;
return 0;
}
以上大家看到的是最常见也最简单的调用方法,这仅仅是隐式的、也就是不用写出来、自动的调用。那么**当基类的构造函数是带参数的情况下如何调用呢?**这样还可以吗?如何传参呢?
答,那就需要我们显式的、也就是明确的写出来,并指定参数传递,来告诉编译器。
一般的写法格式为:
派生类构造函数名(总形参表列):基类构造函数(实参表列)
例如:
#include<iostream>
using namespace std;
class Clock
{
private:
int H;
int M;
int S;
public:
Clock()
{
cout<<"Clock's Constructor Called!"<<endl;
}
Clock(int h, int m, int s)
{
this->H = h;
this->M = m;
this->S = s;
cout << "Clock's Constructor with parameter Called!" << endl;
}
};
class AlarmClock:public Clock
{
private:
int AH;
int AM;
public:
AlarmClock()
{
cout<<"AlarmClock's Constructor Called!"<<endl;
}
AlarmClock(int h, int m, int s) : Clock(h, m, s) // 注意这一行
{
cout << "AlarmClock's Constructor with parameter Called!" << endl;
}
};
int main()
{
AlarmClock A(8,10,30);
AlarmClock B;
return 0;
}
需要注意的是,一旦基类中有带参数的构造函数,派生类中则必须有显式传参的派生类构造函数,来实现基类中参数的传递,完成初始化工作。
4. 派生类的析构函数
同上一节一样,在派生类中,析构函数也无法被派生类吸收。另外需要清楚,派生类和基类的析构函数的调用顺序与构造函数则完全相反。
#include<iostream>
using namespace std;
class Clock
{
private:
int H;
int M;
int S;
public:
Clock()
{
cout<<"Clock's Constructor Called!"<<endl;
}
~Clock()
{
cout<<"Clock's Destructor Called!"<<endl;
}
};
class AlarmClock:public Clock
{
private:
int AH;
int AM;
public:
AlarmClock()
{
cout<<"AlarmClock's Constructor Called!"<<endl;
}
~AlarmClock()
{
cout<<"AlarmClock's Destructor Called!"<<endl;
}
};
int main()
{
AlarmClock A;
return 0;
}
构造函数调用顺序:基类->派生类
析构函数调用顺序:派生类->基类
5. 虚基类及虚基类的定义使用
在前面学过的继承过程中,不知道大家有没有想到这种情况:在多继承关系中,如果一个派生类的从两个父类那里继承过来,并且这两个父类又恰恰是从一个基类那里继承而来。那这样就麻烦了,因为你可能继承了两份一样的成员!
这就出现了所谓二义性问题,下面我们展开学习:
先给大家举一个能说明这种多继承的例子吧:以前呢有一个大户人家,起初呢就两口子,男主人掌权,保管着大门钥匙,后来他们有两个儿子,若干年后长大成人,男主人也老了,就把钥匙又配了两把,分别交给他的两个儿子,交由他们主持家业,又过去好多年,两个儿子也都成了家,又分别有一男一女,也就是家里的孙子孙女,传宗接代。所谓人丁兴旺吖,后来他们也长大成人,又到了交接钥匙的时候,古代当然钥匙要传给男孩啦,那么这个时候,当然给孙子啦,这个时候呢孙子的爹和他的叔伯(也就是最初主人的俩儿子)都要配一个钥匙给这个孙子,他手上就有了两把。
小孙子看着手里两把钥匙,说:“一把就够!两把…这不浪费嘛!”
看完这个故事,大家就好理解多继承了,故事中最初的男主人,也就是爷爷,就是基类,他的两个孩子,是两个派生类,然后其中一个人的孩子,就是那个小孙子,就是最后的派生类,由于他继承自两个派生类。且这两个派生类都有一个共同的成员(钥匙,因为都继承同一个基类--爷爷)。最后的小孙子的派生类有两个重复的成员(钥匙),就是二义性的根源所在!
按照这个例子具体成代码:
#include <iostream>
using namespace std;
class Grandfather
{
public:
int key;
public:
};
class Father1:public Grandfather
{
};
class Father2:public Grandfather
{
};
class Grandson:public Father1,public Father2
{
};
int main()
{
Grandson A;
//A.key=9; // 注意这一行
return 0;
}
如上图所示,Grandson类继承两个father类,会有两个key成员,这个时候如果试图使用这个key,注意已经声明为public类型,在主函数中试图赋值时候,会有“不唯一、模棱两可”的错误提示,即所谓的二义性问题发生。
那么如何规避这个问题呢?即使用虚基类!
所谓虚基类就是在继承的时候在继承类型public之前用virtual修饰一下 。比如还是这个例子,只需要父亲类在继承爷爷类的时候多加一个virtual,那么这个时候,派生类和基类就只维护一份一个基类对象。避免多次拷贝,出现歧义。
定义方法即在两个父亲类的派生时增加virtual的声明:
class Father1:virtual public Grandfather {}
class Father2:virtual public Grandfather {}
其余不变。
#include <iostream>
using namespace std;
class Grandfather
{
public:
int key;
public:
};
class Father1:virtual public Grandfather
{
};
class Father2:virtual public Grandfather
{
};
class Grandson:public Father1,public Father2
{
};
int main()
{
Grandson A;
A.key=9;
return 0;
}
可以看出,现在能正常运行了。