C++ 继承篇


在这里插入图片描述
面向对象语言的三大特性:封装、继承和多态

封装的思想:

  1. 将数据和方法封装到一个类中,通过访问限定符的修饰,想让外界访问的定义成public,不想让外界访问的定义成protected/private,比如class的定义
  2. 将一个类型/类封装到另一个类中,自主定义该类型的行为,或者通过typedef重命名类型,暴露给给用户,让用户同一使用,不用关注底层,比如list/reverse_list的迭代器

1. 继承的概念和定义


1.1 继承的概念

继承是面向对象语言在类设计上的一种代码复用手段

有这样的场景,你要完成一个学生管理系统,必然需要描述很多的对象,于是构建很多类,每个类代表不同的群体,比如学生类、老师类、宿管类…定义出来后,发现每个类中都有某些属性是相同的,比如大家都有名字、年龄、性别这样的属性,在每个类中都定义了一遍,很显然代码冗余,于是就有了继承

将每个类的公共属性提取出来,单独作为一个类,称为父类/基类;每个群体中持有它们独有的属性,叫做子类/派生类,通过继承的方式,将父类继承给子类,这样子类就有父类的属性和自身独有的属性

在这里插入图片描述

1.2 继承的定义

在这里插入图片描述

2. 父类成员变量在子类中的访问限定符


父类的成员变量有三种访问限定符,继承方式也有三种,父类成员变量在子类中的访问限定符是怎样的?

父类成员变量/继承方式public继承protected继承private继承
父类public成员子类的public成员子类的protected成员子类的private成员
父类protected成员子类的protected成员子类的protected成员子类的private成员
父类private成员子类中不可见子类中不可见子类中不可见

对于这张表,有个规律可以帮助记忆

无论是何种继承方式,只要是父类的private成员变量,在子类中不可见;这里的不可见是指无法在子类中无法直接访问父类成员,但可以间接访问

在这里插入图片描述

其余情况,按照public > protected > private 的顺序,取继承方式和成员访问限定符中较小的那个,就是父类的成员在子类的访问限定符;比如如果是父类的protected成员,public继承方式,那么在子类中该成员就是protected的

在实践中,大部分情况使用的都是public继承,同时用private修饰成员也很少见

3. 父类和子类的赋值转换


C语言中,相关类型之间可以发生隐式类型转换,中间会产生临时变量,C++中延续了这种语法

在public继承中,可以认为每一个子类对象都是一个特殊的父类对象,子类对象可以赋值给父类,中间不产生临时变量,这是一种赋值兼容

如果将子类赋给父类的指针或引用,该指针/引用指向子类中的父类那部分,我们也称为切割/切片

但是不能将父类赋值给子类

在这里插入图片描述

4. 继承中的作用域


  • 父类和子类有着各自的作用域
  • 如果父类和子类有同名变量,则这两个变量构成隐藏(重定义);在子类中访问时默认访问子类的同名变量,可以通过域作用限定符访问父类中的同名变量
  • 如果父类和子类有同名成员函数,只要函数名相同,这两个函数就构成隐藏

在这里插入图片描述

在这里插入图片描述

5. 子类的默认成员函数


子类中,我们将成员分成两部分,父类的和自身的

子类的默认成员函数执行规则:

  1. 把父类的成员看成一个整体,去调用父类的默认成员函数
  2. 自身的成员不变,内置类型不做处理,自定义类型去调用自定义类型的默认成员函数

5.1 子类的构造函数

如果没有写子类的构造函数,默认构造函数会把父类的成员看成整体,去调用父类的默认构造,自身成员内置类型不做处理,自定义类型去调用自定义类型的构造函数

如果父类没有默认构造,则需要自己写子类的构造并在初始化列表中显示调用父类的构造

在这里插入图片描述

5.2 子类的拷贝构造

同理,如果没写子类的拷贝构造,父类成员去调用父类的拷贝构造,自身成员完成值拷贝

如果有深拷贝,需要自己写拷贝构造,则需要在子类的拷贝构造中显示调用父类的拷贝构造

在这里插入图片描述

5.3 子类的赋值重载

赋值重载同拷贝构造类似,有深拷贝则需要自己写赋值重载,在子类的赋值重载中调用父类的赋值重载,需要注意的是,父子赋值重载构成隐藏,在调用父类赋值重载时需要指定作用域

在这里插入图片描述

5.4 子类的析构函数

析构函数能够直接调用,但如果在子类析构函数中调用父类系统,会发现报错

由于多态的需要,编译器会将父类和子类的析构函数同一命名为destructor(),于是父类和子类的析构函数构成隐藏,默认调用的是子类的析构

在这里插入图片描述

析构函数的析构顺序不同,在构造时,是先构造父类的部分,再构造子类的部分;而析构则相反,需要保证先析构子类的部分,再析构父类的部分

因此,子类析构在调用结束后会自动调用父类析构,这是为了防止在子类析构中使用父类成员后导致非法访问的问题

在这里插入图片描述

在这里插入图片描述

6. 继承与友元


父类的友元函数不是子类的友元函数

在这里插入图片描述

7. 继承与静态成员

父类的静态成员不仅属于当前父类,也属于所有子类,只有一份

在这里插入图片描述

利用该特性,可以计算当前父类一共有多少个子类(包括父类)

在这里插入图片描述

8. 多继承与菱形继承


  • 单继承:一个子类只有一个直接父类
  • 多继承:一个子类有两个及以上个直接父类

在这里插入图片描述

生活中,有些物品具有双重属性,比如西红柿,它既是水果,又是蔬菜,使用多继承来描述十分合理

多继承本身也没问题,但有多继承,就可能出现菱形继承

在这里插入图片描述

菱形继承导致最总的D对象中包含了两份/多份A对象,也就是数据冗余的问题

同时,访问D对象中的A类成员时,编译器不知道访问哪个,也就是二义性的问题
在这里插入图片描述
在这里插入图片描述

对于菱形继承所导致的问题,Bjarne Stroustrup给出的答案是使用虚继承,对继承最初父类的子类进行virtual修饰

在这里插入图片描述
在这里插入图片描述

在菱形虚拟继承中,将B和C的公共父类A放到了D对象模型中的最下面,我们把A类叫做虚基类;B和C中都多了一个指针,该指针中存放的值是当前类到虚基类的偏移量

在这里插入图片描述

为什么要存放到虚基类的偏移量?如果我们想要通过父类的指针/引用,去访问虚基类中的对象,指针中的偏移量就能找到虚基类
在这里插入图片描述
同时,被virtual修饰的类自身内存结构也发生变化,为什么这样做?拿B类举例,B对象的指针/引用可能是一个D对象,也可能是一个B对象,这样做就统一了访问虚基类的操作
在这里插入图片描述

在这里插入图片描述

9. 继承和组合


  • 继承是一种is-a的关系
  • 组合是一种has-a的关系

如果一个对象既能用继承描述,又能用组合描述,优先使用组合

继承是一种"白箱"复用,父类成员对子类可见,导致类和类,模块和模块之间耦合度高,相互影响大

组合是一种"黑箱"复用,被包含类的成员对包含类不可见,模块之间耦合度低,相互影响程度小

10.继承总结


关于多继承的面试题:

  1. C++有多继承,为什么java没有?

    C++比java先设计,在当时,多继承看起来十分合理,于是就设计了出来,但是没想到出现了菱形继承的问题,导致解决非常麻烦,而且生活中也很少使用;而java吸收了这个教训,在设计时就舍弃了多继承

  2. 多继承的问题是什么?

    多继承本身没有任何问题,但有多继承就可能会写出棱形继承

  3. 棱形继承的问题?如何解决?

    数据冗余,二义性;对第一级的子类使用虚拟继承

  4. 底层角度是如何解决数据冗余和二义性的?

    在内存中,虚基类放到整个对象的最下面,并在虚继承类中添加一个指针,用来记录虚基类的偏移量

  • 13
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值