概念基础
定义
继承是类设计层次的复用,允许开发者在保持原有类特性的基础上进行拓展、增加功能,产生一个新的类,其定义格式也比较简单:
class 派生类类名 : 继承方式 基类类名
{
//...
}
其中继承方式有3种:public继承、protected继承、private继承。
需要注意的是,如果我们不显示写继承方式,使用关键字class的派生类默认的继承方式是private继承,使用关键字struct的派生类默认的继承方式是pubilc继承,不过我们建议显示写出继承方式。
继承方式与基类成员访问方式的关系
不可见是指基类private成员虽然被继承到了派生类中,但在语法上限制派生类对象无论是在类里面还是类外面都不能从访问基类继承来的private成员。
在实际运用中,我们一般推荐使用public继承方式,因为protected继承和private继承继承下来的基类成员只能在派生类的类里面使用,拓展维护性不强。
对于继承有以下4点需要注意:
1.基类和派生类对象的赋值转换:
派生类对象可以赋值给基类的对象、基类的指针、基类的引用,即派生类中基类的那部分可以切出来赋值给一个基类对象(中间不产生临时变量,直接切割赋值),这种现象叫做切片或者切割。但反过来,基类对象不能赋值给派生类对象,而基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用(应保证基类的指针或者引用原本就指向一个派生类对象,这样才能保证该赋值转换是安全的)。
2.继承中的作用域:
在派生类中,子类成员和父类成员存在于相互独立的作用域中,这就意味着父子类可以有同名成员。如果子类和父类有同名成员,子类成员将屏蔽父类同名成员的访问,优先使用子类同名成员,这种现象称为隐藏,也叫重定义(只要成员函数名相同就构成隐藏),在子类中,可以使用 基类名::基类成员 显示访问被隐藏的成员,但在实际中,我们不推荐在继承体系中定义重名成员。
3.继承与友元:
友元关系不能被继承,即基类友元不能访问子类的private成员和protected成员
4.继承与静态成员:
如果基类定义了static静态成员,那么整个继承体系中只存在唯一一个该静态成员,即无论派生出多少个子类,都只有一个static成员实例。
派生类的默认成员函数
1.构造函数
派生类的构造函数与普通类的构造函数的行为没有什么区别,其按照先父后子的顺序进行初始化,只不过派生类的构造函数必须调用基类的构造函数初始化基类那一部分成员。即如果基类有默认构造函数,则派生类自动调用该构造函数初始化基类继承来的成员,如果基类没有默认构造函数,需要在派生类的初始化列表显示调用基类的构造函数。
//基类
class Person
{
public:
Person(int age)
:_age=age
{
;
}
protected:
int _age;
}
//派生类
class Student
{
public:
Student(int age, int number)
:Person(age)//显示调用基类构造函数
,_number(number)
{
;
}
protected:
int _number;
}
2.拷贝构造函数
与构造函数一样,派生类的拷贝构造函数必须调用基类的拷贝构造函数完成派生类中基类那部分成员的拷贝构造。如果在派生类的拷贝构造函数中不显示调用基类的拷贝构造函数,编译器自动调用基类的默认拷贝构造函数。如果在派生类中不写拷贝构造函数,其生成的默认拷贝构造函数自动调用基类的默认拷贝构造函数完成基类那部分成员的拷贝构造,对非基类部分的成员的处理与普通类的处理一样。
//派生类
class Student:public Person
{
public:
Student(const Student& s)
:Person(s)//显示调用基类拷贝构造函数,会进行切片
,_number(number)
{
;
}
protected:
int _number;
}
3.赋值运算符重载函数
派生类的赋值运算符重载函数必须调用基类的赋值运算符重载函数完成派生类中基类那部分成员的赋值。如果在派生类的赋值运算符重载函数中不显示调用基类的赋值运算符重载函数,编译器自动调用基类的默认生成的赋值运算符重载函数。如果在派生类中不写赋值运算符重载函数,其默认生成的赋值运算符重载函数自动调用基类的默认赋值运算符重载函数完成基类那部分成员的赋值,对派生类的成员的处理与普通类的处理一样。
class Student:public Person
{
public:
Student& operator=(const Student& s)
{
Person::operator= (s);//显示调用基类赋值运算符重载函数,自动切片
_num=s._num;
}
protected:
int _number;
}
4.析构函数
为了保证在清理派生类对象时先清理派生类的成员在清理基类成员(这种清理顺序更加安全),派生类的析构函数会在最后自动调用基类的析构函数。如果不在派生类中显示写析构函数,默认生成的析构函数调用基类的构造函数,对派生类的成员的处理与普通类的处理一样。
另外2个默认成员函数可不必予以理会。
菱形继承和菱形虚拟继承
如果一个派生类只有一个直接父类,那么这种继承关系被称为单继承,如果一个派生类有多个直接父类,那么这种继承关系被称为多继承,由于多继承的存在,就产生了一种特殊的继承情况:菱形继承。
菱形继承造成了数据冗余和数据存在二义性的问题(如Person类的数据),存在空间浪费。我们可以使用虚拟继承来解决这个问题,即从最后的派生类开始往上找这个类的基类,如果某个基类是被多个类继承的,那么继承了这个基类的派生类都要采用虚拟继承。
虚拟继承的原理在不同编译器下可能会略有差异,但总体相似:
class A
{
public:
int _a;
};
class B:virtual public A
{
public:
int _b;
};
class C :virtual public A
{
public:
int _c;
};
class D :public B,public C
{
public:
int _d;
};
考虑到复杂性和性能问题,我们建议不要设计出菱形继承。
继承和组合
组合即直接让另一个类的对象成为这个类的成员,这是一种has-a关系,即每一个该类型对象都存在另一个类类型对象作为成员,而public继承则是is-a关系,即每一个派生类对象都是一个基类对象。我们建议优先使用组合,而不是类继承,原因如下:
1.在继承中,基类的内部细节对子类是可见的,是一种白箱复用,而组合则是一种黑箱复用,只关心接口,不必理会内部细节,便于维护。
2.继承一定程度上破坏了基类的封装,而组合有助于保持每个类的封装性。
3.组合类不能直接访问另一个类的成员,耦合度较低,而继承可以直接访问基类成员,耦合度较高。