11 C++的继承


一、继承的概念及定义

继承(inheritance)允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称为派生类。提高代码的重复利用率,是面向对象的重要一种手段。
以前我们接触的复用都是函数复用,而继承便是类设计层次的复用。

比如一个类为Student

继承后,父类Person的成员,包括成员函数和成员变量,都会变成子类的一部分,也就是说,子类Student复用了父类Person的成员。

1.1. 继承方式和访问限定符

在这里插入图片描述

基类当中被不同访问限定符修饰的成员,以不同的继承方式继承到派生类当中后,该成员最终在派生类当中的访问方式将会发生变化。

类成员/继承方式public继承protected继承private继承
基类的public成员派生类的public成员派生类的protected成员派生类的private成员
基类类的protected成员派生类的protected成员派生类的protected成员派生类的private成员
基类类的private成员在派生类中不可见在派生类中不可见在派生类中不可见
  1. 由上表可知,基类的成员变量和函数的限定修饰符=min(基类的访问方式,继承方式)[public>protected>private]
  2. 基类private成员,在派生类中都不可见(派生类继承了这个成员,但是在语法上无法访问),可以通过调用基类的成员函数来访问这个成员,但是派生类的函数无法访问。
  3. 使用关键字class时默认的继承方式为private,使用struct时默认的继承方式是public,建议显示的写出继承方式。
  4. 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。 可以看出保护成员限定符是因继承才出现的。
  5. 实际运用中,一般都是使用public继承,很少并且不提倡用protected和private继承,因为这两种继承下来的成员都只能在派生类的类里面使用,实际中扩展延伸性不强。

二、基类和派生类对象赋值转换

派生类对象可以赋值给基类的对象、基类的指针以及基类的引用,因为在这个过程中,会发生基类和派生类对象之间的赋值转换。
对于这种做法,有个形象的说法叫做切片/切割,寓意把派生类中基类那部分切来赋值过去。
在这里插入图片描述

对于指针和引用而言,基类的指针如果指向派生类,则只能访问基类的public成员和函数,而不能访问派生类的成员和函数:
在这里插入图片描述

基类对象不能赋值给派生类对象。基类的指针可以通过强制类型转换赋值给派生类的指针,但是此时基类的指针必须是指向派生类的对象才是安全的,如果是指向基类的对象,通过指针对派生类成员进行访问会发生越界问题:
在这里插入图片描述


三、继承中的作用域

派生类中的成员变量或函数难免会出现和基类中相同的情况,此时如果想访问基类中的变量或函数就要加作用域。

  1. 在继承体系中基类和派生类都有独立的作用域。
  2. 派生类和基类中有同名成员,派生类成员将屏蔽基类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在基类成员函数中,可以使用 基类::基类成员的方式进行显示访问)
  3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏
  4. 注意在实际中在继承体系里面最好不要定义同名的成员。
  5. 基类和派生类中相同的函数不构成函数重载,因为函数重载要求两个函数在同一作用域,基类和派生类的函数并不在同一作用域。为了避免混淆,派生类当中最好不要定义和基类同名的成员。

在这里插入图片描述


四、派生类的默认成员函数

派生类与普通类的默认成员函数的不同之处概括为以下几点:

  1. 派生类的构造函数被调用时,会自动调用基类的构造函数初始化基类的那一部分成员,如果基类当中没有默认的构造函数,则必须在派生类构造函数的初始化列表当中显示调用基类的构造函数。
    在这里插入图片描述
    在这里插入图片描述

  2. 派生类的拷贝构造函数必须调用基类的拷贝构造函数完成基类成员的拷贝构造
    在这里插入图片描述

  3. 派生类的赋值运算符重载函数必须调用基类的赋值运算符重载函数完成基类成员的赋值。
    在这里插入图片描述

  4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。编译器会保证先析构子类,再析构父类,因此不需要我们显示调用父类的析构函数。
    在这里插入图片描述

  5. 派生类对象初始化时,会先调用基类的构造函数再调用派生类的构造函数。这是因为:成员初始化的顺序,和定义的顺序没有关系,和声明的顺序有关系,所以是先初始化基类的构造函数。
    在这里插入图片描述

  6. 派生类对象在析构时,会先调用派生类的析构函数再调用基类的析构函数。
    在这里插入图片描述


五、继承与友元

友元关系不能继承,也就是说基类的友元可以访问基类的私有和保护成员,但是不能访问派生类的私有和保护成员。如果想让基类的友元函数也能访问派生类的私有和保护成员,只能将其声明为派生类的友元。
在这里插入图片描述

六、继承与静态成员

基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。
无论派生出对少个子类,都只有一个static成员实例。
在这里插入图片描述


七、菱形继承和菱形虚拟继承

单继承:一个子类只有一个直接父类时称这个继承关系为单继承:
在这里插入图片描述

多继承:一个子类有两个或两个以上直接父类时称这个继承关系为多继承:
在这里插入图片描述

菱形继承:菱形继承是多继承的一种特殊情况:
在这里插入图片描述

7.1. 菱形继承导致的问题

菱形继承有数据冗余二义性的问题:
在这里插入图片描述

由于_name这个成员变量在D中始终会存在两份,所以其数据冗余的问题没有解决:
在这里插入图片描述

7.2. 虚拟继承

虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在Student和Teacher的继承Person时使用虚拟继承,即可解决问题。需要注意的是,虚拟继承不要在其他地方去使用。虚继承让我们的对象模型复杂了很多。
在这里插入图片描述

虚继承以后它们的地址相同,说明它们都是同一份数据。

在这里插入图片描述

7.3. C++编译器如何通过虚继承解决数据冗余和二义性

为了研究虚拟继承原理,我们使用一个简化的菱形继承继承体系,由于VS的监视窗口是被编译器处理过的,看到的不一定是真是对象模型,所以要借助内存窗口观察对象成员的模型。

当没有使用虚继承时:

#include <iostream>
using namespace std;
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.B::_a = 1;
	d.C::_a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;
	return 0;
}

D的大小为20,说明其中有5个整形变量:
在这里插入图片描述

通过内存窗口,我们可以看到D类对象当中各个成员在内存当中的分布情况如下:
在这里插入图片描述

也就是说,D类对象当中各个成员在内存当中的分布情况如下:

在这里插入图片描述

当将其改成虚继承以后:

#include <iostream>
using namespace std;
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;
};
int main()
{
	D d;
	d.B::_a = 1;
	d.C::_a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;
	return 0;
}

在这里插入图片描述

虚继承以后,D类对象当中各个成员在内存当中的分布情况如下:

在这里插入图片描述

当虚继承以后,不光D的内存布局会发生改变,B和C的布局也会发生类似的变化:
在这里插入图片描述


八、组合和继承的比较

C++语法复杂,多继承就是一个体现,有了多继承就存在菱形继承,有了菱形继承就有了菱形虚拟继承,底层实现就很复杂。一般不建议设计出多继承(IO流文件就是多继承)。当然也不建议设计出菱形继承,否则在复杂度以及性能上都会有问题。

使用继承的目的主要是为了复用父类的代码,实际上组合也可以做到复用:
在这里插入图片描述

  • public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。
  • 组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。
  • 优先使用对象组合,而不是类继承 。
  • 继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响派生类和基类间的依赖关系很强,关联度、耦合度高
  • 对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对
    象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。 组合类之间没有很强的依赖关系,关联度、耦合度低。优先使用对象组合有助于你保持每个类被封装。
  • 实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适
    合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用继承,可以用组合,就用组合。

比如上述的B继承A,C组合A:
因为C里面只能用A公有的成员,A的私有成员和保护成员对C是不透明的,那么C和A关联度就低,A的改变对C的影响小。B采用公有继承的方式,B既可以用A的公有成员,也可以用A的保护成员,B和A的关联度高,A的改变对B的影响比较大。并且A的封装对B是不太起作用的,A对B来说是不透明的。

如果类之间更符合is-a的关系,建议用继承;如果更符合has-a的关系,建议用组合;如果不明显,既可以看做是is-a,也可以是has-a,建议用组合。
比如:狗是一种动物,描述狗的类,狗类和动物类就是is-a的关系。
比如:轮胎和车的关系就是has-a的关系。


总结

什么是菱形继承?菱形继承的问题是什么?
菱形继承是多继承的一种特殊情况,两个子类继承同一个父类,而又有子类同时继承这两个子类,我们称这种继承为菱形继承。
菱形继承因为子类对象当中会有两份父类的成员,因此会导致数据冗余和二义性的问题。

什么是菱形虚拟继承?如何解决数据冗余和二义性?
菱形虚拟继承是指在菱形继承的腰部使用虚拟继承(virtual)的继承方式,菱形虚拟继承对于D类对象当中重复的A类成员只存储一份,然后采用虚基表指针和虚基表使得D类对象当中继承的B类和C类可以找到自己继承的A类成员,从而解决了数据冗余和二义性的问题。

继承和组合的区别?什么时候用继承?什么时候用组合?
继承是一种is-a的关系,而组合是一种has-a的关系。如果两个类之间是is-a的关系,使用继承;如果两个类之间是has-a的关系,则使用组合;如果两个类之间的关系既可以看作is-a的关系,又可以看作has-a的关系,则优先使用组合。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

今天也要写bug、

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值