C++继承
继承是面向对象编程的重要特征,是对类设计层次的复用
文章目录
- C++继承
- 一.介绍
- 1.继承定义
- 2.继承方式
- 3.class与struct
- 二.作用域
- 1.成员变量
- 2.成员函数
- 三.赋值转换
- 1.给基类对象赋值
- 2.给基类对象指针赋值
- 四.派生类的默认函数
- 五. 其他
- 1.友元
- 2.静态
- 六.继承
- 1.单继承
- 2.多继承
- 3.菱形继承
- 4.虚拟继承
- 七.其他
- 1.如何定义一个不能被继承的类?
- 2. 菱形继承中如果合理使用虚拟继承?
一.介绍
1.继承定义
class Parent
{};
class Child :public Parent
{};
Parent:是父类,也称作基类(base class);Student:是子类,也称作派生类(derived class)
public:表示继承方式为公共继承
2.继承方式
子类继承父类,有3种继承方式
继承方式的作用是:
对于父类非private的成员,使其在子类中的访问权限为MIN(继承方式,父类成员访问限制符)
如:
private成员pc在子类中是不可见的,不能在子类中被访问。
-
public继承
最常用的继承方式,不改变父类成员在子类的访问权限
-
protected继承
可将父类的public成员,在子类中为protected
-
private继承
可将父类的public、protected成员,在子类中为private
这个时候可以让我们回顾一下,类中访问限制符的作用
public
(公有):其修饰的成员可以在类外直接访问protected
(保护)与private
(私有):其修饰的成员不能在类外直接访问
此时就可以发现protected与private的异同了:
如果成员不想在类外被直接访问,则可以用protected或private修饰。但如果需要在子类中被访问,则需要设置为protected。因为private修饰的成员在子类中是不可见(虽然被子类继承了,但是子类不能访问)。
3.class与struct
- 用class作为类声明的关键字
如果派生类是使用class
关键字,则默认继承(不显示表明)方式为private
- 用struct作为类声明的关键字
如果派生类是使用struct
关键字,则默认继承(不显示表明)方式为private
无论使用那种,最好显示的写出继承方式
二.作用域
不同类都有其自己的类域,因此基类和派生类都有独立的作用域
例如,上例中的
Children
类,其继承了Parent
类,并继承得到Parent类中的成员,但是这些成员却不在Children的作用域里。
这很好理解,毕竟有两个{},基类成员都在基类的类体中声明(定义)。其次,我们可以在派生类中,声明(定义)与基类同名的成员。(要知道在相同作用域中,定义同名的变量是会引起命名冲突的)
如果派生类和基类中有同名成员,派生类成员将屏蔽对基类同名成员的直接访问,即会优先访问派生类的成员,也称隐藏或重定义。
此时访问基类成员需要显示指定基类的作用域
1.成员变量
c.pa:访问的是Children中的成员pa c.Parent::pa:访问到Parent中的成员pa
2.成员函数
需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏
Parent中的show()和Children中的show(),有不同的参数列表,但是并不构成函数重载,因为函数重载的条件是需要在相同作用域中。这两个函数构成隐藏关系
tips:实际中最好不要在继承体系里定义同名成员
三.赋值转换
public继承方式下
派生类的对象可以赋值给基类的对象、基类的指针、基类的引用
这种赋值操作又被叫做切片或者切割,比喻将派生类对象中基类的那部分切给基类进行赋值。
但是基类对象不能赋值给派生类对象
1.给基类对象赋值
将对象c中Parent部分切片赋值给对象p
2.给基类对象指针赋值
pp指向对象c中Parent部分的切片
引用:底层也是指针。
四.派生类的默认函数
对于一个空类,经由编译器处理过后,会为它声明一个默认构造函数、一个拷贝构造函数、一个赋值运算符重载、一个析构函数,且这些函数都是public的。称之为默认成员函数。
默认构造函数:
会先(在初始化列表的位置)调用基类的默认构造函数,完成基类的创建。
构造函数:
如果基类没有默认构造函数,则需要在初始化列表显示调用基类的构造函数
拷贝构造函数:
如果基类和派生类的拷贝构造函数都是编译器生成的,会先(在初始化列表的位置)调用基类的拷贝构造函数。
如果基类显示定义了,则需要在派生类的拷贝构造函数中显示调用,否则不会完成对基类部分的值拷贝(如果不是在参数列表位置调用的,则会先自动调用基类的默认构造函数,如果基类无默认构造,则会报错)
析构函数:
析构函数基本上都是当对象的生命周期结束后,由编译器自动调用的。在继承体系中,当一个派生类对象需要释放,会先调用派生类的析构函数,再调用其基类的析构函数。
五. 其他
1.友元
友元的目的是打破封装,使protected或private的类成员也可以被类外访问
友元关系不能被继承
友元函数show()并非Parent类的成员函数,只是通过friend关键字同类外产生联系
友元类同理
2.静态
对于基类的静态成员,无论其派生类有多少个,都共用着同一个静态成员
静态函数同理
六.继承
1.单继承
一个派生类只有一个直接基类的关系称为单继承
class Grandparent
{};
class Parent :public Grandparent
{};
class Children :public Parent
{};
Greadparent–>Parent–>Children,单继承
2.多继承
一个派生类有两个或以上直接基类的关系称为多继承
class Father
{};
class Mother
{};
class Children :public Father, public Mother
{};
Children<—Father & Mother,多继承
对基类的初始化顺序,是根据继承的先后顺序
Children类的默认构造函数中,在其初始列表位置,即使先调用Mother(),后Father(),结果依旧是按继承的顺序先构造Father,后Mother。(初始化列表的错误使用示例,最好按照声明的次序条列)
多继承有可能会出现二义性的问题
3.菱形继承
菱形继承是多继承的一种特例
class Grandma
{};
class Father :public Grandma
{};
class Mother :public Grandma
{};
class Children :public Father, public Mother
{};
Father是继承于其Grandma,Mother是继承于其Grandma,
当Children继承Father和Mother后,就间接的继承了两个Grandma对象
二义性问题
数据冗余问题
对于派生类如果只是需要一份基类的成员即可,那么菱形继承也会造成数据冗余。例如上例的Children类间接的继承了两份Grandma。
4.虚拟继承
意义:为解决由菱形继承而导致的数据冗余和二义性问题
示例:
class Grandma
{
public:
int i;
};
class Father :virtual public Grandma
{};
class Mother :virtual public Grandma
{};
class Children :public Father, public Mother
{};
让Father和Mother,虚拟(virtual)继承Grandma
此时会产生什么变化呢?
可以看见当Father、Mother虚拟继承后,其首位多出来4个字节的空间,存放着某种数据
首位置是一个指针,指针内存放着地址,该地址指向一张表。指针是虚基表指针,表是虚基表
虚基表中存放的是偏移量,通过第二个偏移量可以找到基类成员变量的地址
可以看出Father、Mother在虚拟(virtual)继承后,就隐式的增加了一个虚基表指针的成员(属于派生类Father、Mother),且所占空间额外增加了4个字节。
下面来看Children类的变化
由于Father、Mother都是虚拟继承于Grandma,其各自的虚基表中第二个偏移量都是指向来着于Grandma的成员变量。因此在Children类c中唯有一份Grandma成员变量
解决了数据冗余和二义性问题
在实际中建议避免定义出菱形继承。
七.其他
1.如何定义一个不能被继承的类?
-
将构造函数私有化
通过无法实例化来间接使A类无法被继承
因此需要提供一个接口GetA,来返回A类型对象
-
将析构函数私有化
通过无法实例化来间接使A类无法被继承
-
final
c++11,不同于前两种,使用final来修饰类后,如果有继承操作就会报错
2. 菱形继承中如果合理使用虚拟继承?
在下图的菱形继承中,只对B和C类使用虚拟继承,就可以解决数据冗余和二义性问题
如果在菱形继承中,每个继承关系都使用虚拟继承,也可以解决问题,但是没有必要,而且还会使空间增大(虚基表指针),并且性能和复杂度都有问题。
🦀🦀观看~~