C++学习笔记—OOP三大特性之继承

继承(inheritance)是面向对象设计(OOP)的三大特性之一,是类之间定义的一种重要的机制,通过这一机制可以实现C++中可重用性,因此是C++的一个重要组成部分。
##继承的概念
所谓继承,就是在一个已经存在的类的基础上建立一个新的类的过程。例如,现有一个类A,要在这个类的基础上建立一个新的类B,则称B继承于A。此时,类B就重用了类A的方法和成员,类B还可以添加新的方法和成员来定制新的类以应对需求。在上述的两个类中,类B是类A的子类(也叫派生类),类A是类B的父类(也叫基类)。

通过继承可以让子类的每个实例包括父类的大多数属性,且能够修改类的部分或者全部属性,因此,继承的两个类需要满足以下关系:父类更通用,更抽象;子类更特殊,更具体;子类与父类是is-a的关系。

使用继承机制具有以下的几个优点:(1)实现代码重用,让父类的字段和方法可以用于子类;(2)能够体现不同的抽象层次;(3)可以更加轻松地自定义子类;(4)从抽象到具体形成类的继承体系。

##继承方式

在C++中主要有以下三种继承方式:公有继承(public),私有继承(private)和保护继承(protected)。一般情况下只使用公有继承和私有继承,保护继承比较少见,所以不做说明。

这三种继承方式的不同之处主要体现在以下两个方面:(1)子类成员对父类成员的访问权限不同;(2)子类的对象对父类成员的访问权限不同。具体的差别如下:

在公有继承(public)中

  1. 父类的public和protected成员的访问属性在子类中保持不变,但父类的private成员不可直接访问。
  2. 子类的成员函数可以直接访问父类中的public和protected成员,但不能直接访问父类的private成员。
  3. 通过子类的对象只能访问父类的public成员。

在私有继承(private)中

  1. 父类的public和protected成员都以private身份出现在子类中,但父类的private成员不可直接访问。
  2. 子类的成员函数可以直接访问父类中的public和protected成员,但不能直接访问父类的private成员。
  3. 通过子类的对象不能直接访问父类中的任何成员。

更加清楚的关系如下表所示。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZszNvoMd-1573700824011)(https://img-my.csdn.net/uploads/201605/15/1463295976_1653.jpg)]

##继承的声明方式
假设现在已有一个父类A,在此基础上建立一个子类B,则声明方式如下:

class B : public A{
public:
...            //类B新增的方法和属性
...
private:
...
...
};

通过更换public,private和protected三个关键字即可实现三种继承方式。更一般的声明形式为:

class 子类名 : 继承方式关键字 父类名{
public:
...            //子类中新增的方法和属性
...
private:
...
...
};

需要注意的是,如果不指出继承方式(继承方式关键字),默认的是私有继承(private)。

以上是单继承的声明方式,除了单继承以外,C++还支持多重继承,也称为多继承。

##多重继承
多重继承(也称为多继承)可以为一个子类指定多个父类。

多继承的声明形式如下:

class 子类名 : 继承方式关键字 父类1,继承方式关键字 父类2...{
public:
...            //子类中新增的方法和属性
...
private:
...
...
};

使用多重继承的优点是很明显的,简单、清晰、更有利于代码的复用,不会因为基类的改变而去重写大量的代码。

但多重继承也有缺点:多重继承可能存在二义性。如果继承的两个或多个父类中存在同名的方法时,那么子类在调用的时候需要为其指明这个方法来自哪个父类,无形中带给我们许多麻烦。

多重继承还有其他的一些问题,可能导致不可预料的结果,因此,许多编程语言都抛弃了多重继承机制,如JAVA等。

##虚继承
我们考虑一下这个问题:

若有四个类A、B、C和D,其中B、C继承于A,而D又继承于B和C,那么,他们的继承关系如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ka8u5dsH-1573700824012)(https://img-my.csdn.net/uploads/201605/15/1463302324_3743.jpg)]
图1 多重继承(非虚继承)的继承关系图

从图中我们可以看出,D类重复继承了A类,这显然是不合理的,这会让D类中出现相同的元素,从而会导致模糊调用(二义性)的现象。

我们也可以从代码中来看上述问题。A、B、C和D四个类的定义和继承代码如下:

class A{
public:
	int a;
};

class B:public A{
public:
	int b;
};

class C:public A{
public:
	int c;
};

class D:public B,public C{
public:
	int d;
};

测试代码如下:

int main(){
	D d;   
	d.a=1;  //这行代码有错误,错误如图2所示
	return 0;
}

D类既从B类中继承了a属性,又从C类中继承了a属性,即包含两个属性a,从而导致二义性的产生。

上述测试代码错误详情如下图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4DrT3z8d-1573700824013)(https://img-my.csdn.net/uploads/201605/15/1463299424_4174.jpg)]
图2 多重继承错误结果详情

解决上述错误的方法有两种:

第一种,直接为其指明来自哪个父类。例如指明来自父类B,代码如下:

	d.B::a=1;  //指明其来自父类B

第二种方法是使用虚继承来解决问题。虚继承是在继承时使用virtual关键字,其原理是让间接子类(D类)穿透了其父类(B类和C类),直接继承了虚基类(A类)。代码修改也很简单,只需要B类和C类在继承时加上virtual关键字即可,具体代码如下:

class B:virtual public A{
public:
	int b;
};

class C:virtual public A{
public:
	int c;
};

通过以上修改,此时它们之间的继承关系图如图3所示。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cHGDCbpj-1573700824014)(https://img-my.csdn.net/uploads/201605/15/1463298463_3595.jpg)]
图3 多重继承(虚继承)的继承关系图

从关系图中我们可以看出,其是通过让D类中包含的B类和C类共享一份A类对象的方法来解决二义性问题。

##构造函数与析构函数
我们通过下面的例子来说明父类和子类的构造函数和析构函数的调用过程。具体代码如下:

class A{
public:
	int a;
	A(){
		printf("A的构造函数\n");
	}
	~A(){
		printf("A的析构函数\n");
	}
};

class B:virtual public  A{
public:
	int b;
	B(){
		printf("B的构造函数\n");
	}
	~B(){
		printf("B的析构函数\n");
	}
};

class C:virtual public A{
public:
	int c;
	C(){
		printf("C的构造函数\n");
	}
	~C(){
		printf("C的析构函数\n");
	}
};

class D:public B,public C{
public:
	int d;
	D(){
		printf("D的构造函数\n");
	}
	~D(){
		printf("D的析构函数\n");
	}
};

以上代码是含有虚继承的代码,代码运行结果如图4所示。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8HmrlD80-1573700824014)(https://img-my.csdn.net/uploads/201605/15/1463304464_3999.jpg)]
图4 虚继承构造函数测试运行结果

如果不是用虚继承,则运行结果如图5所示。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AucOVsUW-1573700824015)(https://img-my.csdn.net/uploads/201605/15/1463304463_2929.jpg)]
图5 非虚继承构造函数测试运行结果

从上面的运行结果,我们可以进一步理解虚继承的实现过程:
从图4中我们可以看出,A类只进行了一次构造,因此只产生一个A类对象,B类和C类共享一个A类对象。
从图5中我们可以看出,A类进行了两次构造,因此D类中会产生相同的元素,从而导致二义性的产生。

从上面的运行结果我们可以看出,子类的构造函数的执行次序如下:

  1. 如果有虚继承,则虚基类构造函数先执行,且仅执行一次;
  2. 如果没有虚继承,则构造函数按照父类的声明顺序依次从左到右进行调用。
  3. 最后调用子类的构造函数。

析构函数的调用过程与构造函数调用过程相反。

以上是我了解的关于C++继承的相关知识,如有不足或错误,请批评指正,谢谢~

你可能感兴趣的:(C++学习笔记—OOP三大特性之继承)