[C++] C++继承详细分析
面向对象的语言中, 面向对象的封装特性是什么?
在学习C++ 之前, 先学习了 C语言, 在接触了 C++ 的类之后, 可以明显感觉到面向对象和面向过程的某些方面的区别
就拿对某种数据结构的模拟实现而言, 比如 queue 队列:
- 使用C语言实现队列, 会发现 队列的结构 与 各种操作队列的功能: 入队列、出队列、取队列的头等, 是分离的。并且, C语言中实现队列 并没有对队列的数据等加以限制, 这就表示 每一个使用者都可以直接操作数据而不是通过数据结构提供的接口等。 但是 C++ 中无论是模拟实现 队列, 还是 库中提供的 queue, 容器的结构 与 数据的操作接口是整体在一个类中的, 并且, 还可以指定 类中的内容用 访问限定符限制类外内容对对象内容的访问。这是C语言无法做到的。通过访问限定符 合适地对 类成员进行限制, 使操作者能够更加 安全、规范的操作、整理、修改数据。进而可以禁止操作者对数据的非法行为等
- 不仅仅是 整合 和 访问限制, 封装更好地隐藏了 容器结构的底层。在我看来封装最好的诠释之一就是STL容器中的 迭代器设计, 即之前介绍的 iterator 和 reverse_iterstor 的结构、实现及其部分细节, 都是为了对容器结构的隐藏和保护。倘若没有 iterator 即迭代器的的设计, 那么使用者想要访问、修改数据 大概率只能通过直接操作底层来操作数据, 迭代器 杜绝了这种情况。可以不让操作者与底层直接建立联系, 进而可以保护底层
- 而且, 封装可以更加简单快捷的编写代码, 比如 库中实现 stack、queue、priority_queue 使用的一种适配器的模式, 将已经实现过的容器进行封装整理, 在以后实现其他容器时 加以使用, 能更加简单的得到想要的结果。
- 在互联网中, 在这个信息时代, 最重要的资源 就是数据。封装 某中程度上提升了 数据的安全, 将底层、细节等对大部分人隐藏起来, 只提供最简单、便捷的接口, 从而可以降低对数据操作的门槛, 但也进行了对底层访问的限制。总的来说, 封装一种可以让数据更安全、让编写者更方便快捷、让使用者更简单易懂的一种手段、方法
继承
继承的概念
继承一般用于什么?
举一个简单的例子:
比如在学校这种存在多种职务的场所:
学校: 校长、老师、学生等
学校的每种职务, 其实都具有 人 的共同的某些属性——姓名、性别、年龄、电话、住址…… 而不同的职务又有各自的独有的属性: 老师—评分—工号, 学生—学分—学号 等
所以, 在定义 老师、学生类之前, 可以先定义一个成员包括姓名、性别、年龄、电话等的 人 类, 然后再复用这个 人 类 定义老师、学生的类
这样可以有效减少代码量, 方便、快捷
继承的定义
public
、protected
、private
继承方式与基类成员访问限定符的关系
继承方式👉 类成员限定👇 | public 继承 | protected 继承 | private 继承 |
---|---|---|---|
父类的public成员 | 可看作 子类的public成员 | 可看作 子类的protected 成员 | 可看作 子类的private 成员 |
父类的protected 成员 | 可看作 子类的protected 成员 | 可看作 子类的protected 成员 | 可看作 子类的private 成员 |
父类的private成 员 | 在子类中不可见 | 在子类中不可见 | 在子类中不可 见 |
-
父类的 private成员 在子类中无论以什么方式继承都是不可见的。
不可见 是指父类的private成员 已经被继承到了子类对象中, 但是语法上限制 子类对象不管在类里面还是类外都不能去访问它
做个父类的 公有成员和私有成员 对比更能展现 父类私有成员的特点:
公有成员:
私有成员:
即, 父类的私有成员, 虽然对子类不可见、不可访问, 但是 是实实在在继承下来了的
当父类有私有成员时子类的大小 与 父类没有私有成员时子类的大小 相同
-
父类private成员在子类中是不能被访问, 如果父类成员不想在类外直接被访问, 但需要在派生类中能访问, 就定义为protected
在 刚学习类和对象时, 说 暂时将 protected 和 private 看为作用一样的访问限定符, 但是 到了继承这里, 他们的作用就不一样了。
父类的 protected 保护成员, 在子类内 是可访问的, 但是在子类外 是不可访问的 类似 没有继承关系的 private 私有成员
用一张图可以清晰的表现出来
父类的保护成员, 被子类继承之后, 在子类内可以访问, 但是在子类外不能访问
-
仔细分析 父、子类限定成员关系与继承方式关系 的表格, 可以发现
除 父类的私有成员, 无论哪种继承方式在子类中都不可见 之外
继承方式 与 父类成员种类, 在子类中的访问限定 是 两者取小 的方式, 比如: 父类中的 protected成员, 即使是 public继承 给子类, 在子类中依旧看作 protected成员
而 父类中的 public成员, 若是 protected继承给 子类, 在子类中则看作 protected成员
父类中的 public成员、protected成员, 若是 private继承给 子类, 在子类中就看作 private成员
-
而在实际运用中一般都使用 public继承
几乎很少使用 protected / private继承, 也不提倡使用 protected/private继承, 因为 protected/private继承下来的成员都只能在派生类的类里面使用, 实际中扩展维护性不强
父类 与 子类对象的赋值
stu
切割给 per
, 使 per
原数据改变, 即说明 子类对象 可切割给 父类对象子类对象 可以切割给 父类对象, 这个父类对象是一个独立的、新的父类对象
而 父类指针 和 父类引用 就没有那么简单了,
这两个, 一个是指针 应该指向父类对象的地址, 另一个是引用 应该是一个父类对象的别名 而 由子类对象赋值, 会发生什么呢?
说明, 其实 子类对象的地址 切割给父类指针;子类对象 切割给 父类引用 就表示 此父类指针是直接指向 子类中从父类继承部分的, 此父类引用 是子类对象中 从父类继承部分的别名
也就表示了, 修改 父类指针 和 父类引用, 是直接修改在 子类对象上的:
但是要注意, 此操作 仅限于 子类对象被切割给的 父类指针 和 父类引用上
父类指针 和 父类引用 是通过强制类型转换 可以赋值给 子类指针 和 子类引用的 , 但是 这个操作与 多态 有关, 就暂不赘述
注意: 子类对象 赋值给父类对象、父类指针、父类引用 , 这是 语法默认支持的, 中间没有任何的 类似类型转换的转换
继承作用域相关
-
子类内, 继承父类的部分 与 子类自己的部分 是 两个独立的作用域
既然是两个独立的作用域, 就不得不考虑一个问题: 两个独立的作用域 按理来说是可以存在 同名变量或同名函数的, 那么子类内可不可以存在 与 父类内 同名的变量 或 同名的函数(同名成员)呢?
答案是 可以
-
子类和父类中可以存在同名成员, 但是子类成员 将屏蔽 父类对同名成员的直接访问, 这种情况叫隐藏, 即 子类中的成员 将 父类中的同名成员隐藏了起来, 这种情况也叫 重定义。
什么是 屏蔽父类对同名成员的直接访问?
当这样的 子类实例化出来的对象, 直接访问
_age
, 会访问哪个_age
呢?是 继承父类部分中的 还是 自己的?答案 很明显:
直接访问
_age
会访问stu 自己的 _age
而不是继承父类的_age
其实这就是 子类成员 屏蔽父类对同名成员的直接访问, 就是 子类成员将 父类同名成员隐藏了起来
虽然, 父类同名成员被隐藏了起来, 但是 还是可以通过 指定类域 来实现对父类同名成员的访问的:
-
成员函数也可以构成隐藏, 且 仅函数名相同 就会构成隐藏
-
*在实际中在继承体系里面最好不要定义同名的成员 **
子类的默认成员函数
-
子类的构造函数 必须 先调用父类的构造函数初始化父类的那一部分成员
就像这样:
可以看到 子类实例化对象调用子类构造函数时, 会先去调用父类默认构造函数, 然后再继续执行子类的构造函数
如果 父类没有默认构造函数, 则必须在子类构造函数的初始化列表阶段显示调用
否则会出现:
并且, 不能直接对 父类成员变量进行初始化, 只能传参调用父类的构造函数
为实现, 实例化 子类对象时, 指定 姓名、性别、年龄, 可以 给子类对象的构造函数添加指定相应类型的形参:
对象实例化时 构造函数的调用 是这样的, 而 析构函数、拷贝构造函数、赋值重载函数 也类似
-
子类的拷贝构造的调用, 是 先调用父类的拷贝构造函数将父类的部分拷贝过来, 然后在调用子类的拷贝构造进行拷贝。 赋值重载函数也是一样的
子类 显式定义 拷贝构造函数, 是需要在 初始化列表手动传参调用父类的拷贝构造函数的:
示例:
当然, 当成员变量所属类型提供的有拷贝构造函数, 编译器自动生成的默认拷贝构造函数, 也是可以用的
显式定义 子类的赋值重载函数, 也是需要在内部 手动调用父类的赋值重载函数的:
并且, 需要注意的是, 子类内部调用 父类的赋值重载函数时, 需要指明类域, 否则将无限循环调用子类的赋值重载函数, 因为 父类的赋值重载函数被隐藏
Person::operator=(s);
这个语句, 存在的两个切片操作是哪两个?operator=(s)
在调用时, 编译器会将其 转换为operator=(this, s)
这样一看就能明白, 子类的
this
传给 父类operator=
的形参this
和 子类对象s
传给 父类operator=
的形参p
一共发生两个 切片/切割 操作
在此父子类中, 默认赋值重载函数也是可以用的
-
子类对象 调用析构函数时, 会 先调用子类的析构函数 析构子类部分的, 再 调用父类的析构函数析构父类部分的
因为先构造的后析构, 这个规则 在 类和对象的默认构造函数 时就已经说过了, 原因是因为 栈区是按照顺序向上使用的, 也需要按照顺序清理释放
按照之前的逻辑, 子类的构造函数应该这样写:
为什么会出现:
没有与这些操作数匹配的'~'运算符
没有与参数列表匹配的构造函数Person::Person
Person 没有合适地默认构造可用
这样的错误?出现这样的错误, 其实是因为 编译器把
~Person
中的Person
当作了构造函数, 把~
当作了一个操作符; 并没有把~Person
当作一个析构函数, 为什么?因为, 继承关系中, 父子类的析构函数构成隐藏
父子类的析构函数是特殊的, 虽然没有满足函数名相同这个构成隐藏的条件, 但是它们还是构成隐藏
这是因为 C++ 中多态的需要, 会把 继承关系的父子类的析构函数 统一处理为
~destructor()
, 所以才会构成隐藏PS: 多态中的需求, 所以会处理析构函数, 对构造函数没有什么特殊需求, 所以不处理
构成隐藏, 所以 指出类域应该可以调用:
知名类域确实可以 调用 Person的析构函数了, 但是 子类的析构过程 却调用了两次~Person, 这又是为什么?
原因是, 子类对象析构时, 为了保证析构顺序, 调用完子类的析构函数, 编译器会自动去调用父类的析构函数
所以, 子类析构函数的定义其实不需要显式调用父类的析构函数
可以看到 子类对象的析构是 先调用 子类的析构函数 后自动调用 父类的析构函数的 调用父类析构函数的操作, 是编译器自动执行的, 不需要手动在子类析构函数内写出来:
问题: 怎么定义一个不能被继承的类
继承与友元
TEST
作为 Person的友元函数
, 可以访问 Person
的成员:Student
作为 Person的子类
, TEST
作为Person的友元
若解开注释:继承 与 静态成员
作者: 哈米d1ch 发表日期:2022 年 7 月 21 日