C++之继承总结(继承的定义与格式,赋值转换,默认成员函数,菱形继承及菱形虚拟继承)
Posted _BitterSweet
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++之继承总结(继承的定义与格式,赋值转换,默认成员函数,菱形继承及菱形虚拟继承)相关的知识,希望对你有一定的参考价值。
C++中继承相关知识
1.继承的概念及定义
1.1继承的概念
- 继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用
- 我们看以下代码的运行结果
#include<iostream>
#include<string>
using namespace std;
class Person
{
public:
void Print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
protected:
string _name = "peter"; // 姓名
int _age = 18; // 年龄
};
class Student : public Person
{
protected:
int _stuid; // 学号
};
class Teacher : public Person
{
protected:
int _jobid; // 工号
};
int main()
{
Student s;
Teacher t;
s.Print();
t.Print();
return 0;
}
- 运行结果:
- 总结:继承后父类的Person的成员(成员函数+成员变量)都会变成子类的一部分。这里体现出了Student和Teacher复用了Person的成员。
1.2继承的定义
1.2.1定义格式
1.2.2继承关系和访问限定符
1.2.3继承基类成员访问方式的变化
- 总结:
- 基类private成员在派生类中无论以什么方式继承都是不可见的。
这里的不可见是指基类的私有成员还是 被继承到了派生类对象中,但是语法上限制生类对象不管在类里面还是类外面都不能去访问它
- 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能 访问,就定义为
protected
。可以看出保护成员限定符是因继承才出现的 - 实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式 =
Min(成员在基类的访问限定符,继承方式)
,public
>protected
>privat
- 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式
- 在实际运用中一般使用都是public继承,几乎很少用protetced/private继承,也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强
2.基类和派生类对象赋值转换(只存在于公有继承当中)
- 派生类对象可以赋值给基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫
切片
或者切割
。寓意把派生类中父类那部分切来赋值过去 - 基类对象不能赋值给派生类对象
- 基类的指针可以通过强制类型转换赋值给派生类的指针。但是必须是基类的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用 RTTI(Run-Time Type Information) 的
dynamic_cast
来进行识别后进行安全转换。
- 我们对应看三个例子:
- 赋值给基类的对象
-
赋值给基类的引用
-
赋值给基类的指针
-
总结:
3.继承中的作用域
-
在继承体系中
基类
和派生类
都有独立的作用域
(可以定义同名的变量) -
子类和父类中
有同名成员
,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义
-
需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏
-
注意在实际中在继承体系里面最好不要定义同名的成员
-
举例:
4.派生类的默认成员函数
-
6个默认成员函数,“默认”的意思就是指我们不写,编译器会变我们自动生成一个,那么在派生类中,这几个成员函数是如何生成的呢?
-
Ⅰ
.构造函数:派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用
-
Ⅱ
.拷贝构造: 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化
-
Ⅲ
. 赋值运算符重载:派生类的operator=
必须要调用基类的operator=
完成基类的赋值
-
Ⅳ
.派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序(如果自己显式调用,会存在父类先析构问题)
-
Ⅴ
.派生类对象初始化先调用基类构造再调派生类构造 -
Ⅵ
.派生类对象析构清理先调用派生类析构再调基类的析构
5.继承与友元
- 友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员
6.继承与静态成员
- 基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都
只有一个static成员实例
7.菱形继承及菱形虚拟继承
7.1单继承与多继承
- 单继承:一个子类只有一个直接父类时称这个继承关系为单继承
- 多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
- 菱形继承:菱形继承是多继承的一种特殊情况(算是多继承中的一种缺陷)
7.2菱形继承存在的问题及解决方案
- 这里
加入域限定符
只能起到治标不治本的作用 - 那么我们如何解决数据冗余的问题呢,这里我们引入虚拟继承,即可解决问题
7.3虚拟继承
- 虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在Student和Teacher的继承Person时使用虚拟继承,即可解决问题
7.4虚拟继承解决数据冗余和二义性的原理
- 先分析菱形继承
- 再分析菱形虚拟继承
- 总结:这里可以分析出D对象中将A放到的了对象组成的最下面,这个A
同时属于B和C,那么B和C如何去找到公共的A呢? 这里是通过了B和C的两个指针,指向的一张表。这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的偏移量。通过偏移量可以找到下面的A
8.总结与反思
- 多继承中存在菱形继承,菱形继承就存在数据冗余和二义性的缺陷,那么解决这两个缺陷的方法就是虚拟继承,所以一般不要设计出菱形继承,否在在复杂度及性能上都有问题
- 多继承是C++的缺陷之一
8.1继承和组合
-
继承就是
is-a
,继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高
-
组合是
has-a
,因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。 组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装
-
优先使用对象组合,而不是类继承
-
继承和组合都是类设计角度的复用
-
在软件工程中用组合更好,组合更符合
高类聚,低耦合
,更符合is-a
关系用继承,更符合has-a
用组合,都符合的情况用组合
以上是关于C++之继承总结(继承的定义与格式,赋值转换,默认成员函数,菱形继承及菱形虚拟继承)的主要内容,如果未能解决你的问题,请参考以下文章
C++进阶:继承C++为什么要引入继承 | 继承概念及定义 | 基类和派生类对象赋值转换 | 继承中的作用域 | 派生类的默认成员函数 | 继承与友元/静态成员 | 复杂的菱形继承及菱形虚拟继承
C++进阶:继承C++为什么要引入继承 | 继承概念及定义 | 基类和派生类对象赋值转换 | 继承中的作用域 | 派生类的默认成员函数 | 继承与友元/静态成员 | 复杂的菱形继承及菱形虚拟继承
C++进阶:继承C++为什么要引入继承 | 继承概念及定义 | 基类和派生类对象赋值转换 | 继承中的作用域 | 派生类的默认成员函数 | 继承与友元/静态成员 | 复杂的菱形继承及菱形虚拟继承