外观
15.C++ 对象模型分析、C++中的抽象类和接口、被遗弃的多重继承、关于动态内存分配、关于虚函数
约 2845 字大约 9 分钟
C++对象模型抽象类和接口多重继承
2022-06-16
五十、C++ 对象模型分析(上)
1、回归本质
class是一种特殊的struct
- 在内存中class依旧可以看作变量的集合
- class 与struct遵循相同的内存对齐规则
- class中的成员函数与成员变量是分开存放的
- 每个对象有独立的成员变量
- 所有对象共享类中的成员函数
值得思考的问题

2、编程实验:对象内存布局初探

3、C++对象模型分析
运行时的对象退化为结构体的形式
- 所有成员变量在内存中依次排布
- 成员变量间可能存在内存空隙
- 可以通过内存地址直接访问成员变量
- 访问权限关键字在运行时失效
类中的成员函数位于代码段中
调用成员函数时对象地址作为参数隐式传递
成员函数通过对象地址访问成员变量
C++语法规则隐藏了对象地址的传递过程
4、编程实验:对象本质分析


5、小结
- C++中的类对象在内存布局上与结构体相同
- 成员变量和成员函数在内存中分开存放
- 访问权限关键字在运行时失效
- 调用成员函数时对象地址作为参数隐式传递
五十一、C++ 对象模型分析(下)
1、继承对象模型
- 在C++编译器的内部类可以理解为结构体
- 子类是由父类成员叠加子类新成员得到的

2、编程实验:继承对象模型初探

3、多态对象模型
- C++多态的实现原理
- 当类中声明虚函数时,编译器会在类中生成一个虚函数表
- 虚函数表是一个存储成员函数地址的数据结构
- 虚函数表是由编译器自动生成与维护的
- virtual成员函数会被编译器放入虚函数表中
- 存在虚函数时,每个对象中都有一个指向虚函数表的指针



4、编程实验:多态本质分析


5、小结
- 继承的本质就是父子间成员变量的叠加
- c++中的多态是通过虚函数表实现的
- 虚函数表是由编译器自动生成与维护的
- 虚函数的调用效率低于普通成员函数
五十二、C++ 中的抽象类和接口
1、什么是抽象类?
- 面向对象中的抽象概念


2、问题:Shape类有必要存在吗?
3、什么是抽象类?
面向对象中的抽象类
- 可用于表示现实世界中的抽象概念
- 是一种只能定义类型,而不能产生对象的类
- 只能被继承并重写相关函数
- 直接特征是相关函数没有完整的实现
Shape是现实世界中各种图形的抽象概念
- 因此:
- 程序中必须能够反映抽象的图形
- 程序中通过抽象类表示图形的概念
- 抽象类不能创建对象,只能用于继承
- 因此:
4、抽象类与纯虚函数
- C++语言中没有抽象类的概念
- C++中通过纯虚函数实现抽象类
- 纯虚函数是指只定义原型的成员函数
- 一个C++类中存在纯虚函数就成为了抽象类
- 纯虚函数的语法规则

5、编程实验:抽象类初探

6、抽象类与纯虚函数
抽象类只能用作父类被继承
子类必须实现纯虚函数的具体功能
纯虚函数被实现后成为虚函数
如果子类没有实现纯虚函数,则子类成为抽象类
满足下面条件的C++类则称为接口
- 类中没有定义任何的成员变量
- 所有的成员函数都是公有的
- 所有的成员函数都是纯虚函数
- 接口是一种特殊的抽象类
7、编程实验:接口初探

8、小结
- 抽象类用于描述现实世界中的抽象概念
- 抽象类只能被继承不能创建对象
- C++中没有抽象类的概念
- C++中通过纯虚函数实现抽象类
- 类中只存在纯虚函数的时成为接口
- 接口是一种特殊的抽象类
五十三、被遗弃的多重继承(上)
1、问题:C++中是否允许一个类继承自多个父类?
2、C++中的多重继承
- C++支持编写多重继承的代码
- 一个子类可以拥有多个父类
- 子类拥有所有父类的成员变量
- 子类继承所有父类的成员函数
- 子类对象可以当作任意父类对象使用
3、多重继承的语法规则
- 多重继承的本质与单继承相同!

4、编程实验:多重继承问题一

5、多重继承的问题一
- 通过多重继承得到的对象可能拥有 “不同的地址” !!
- 解决方案:无

6、多重继承问题二
- 多重继承可能产生冗余的成员

7、编程实验:多重继承问题二

8、多重继承问题二
- 当多重继承关系出现闭合时将产生数据冗余的问题!!!
- 解决方案:虚继承

- 虚继承能够解决数据冗余问题
- 中间层父类不再关心顶层父类的初始化
- 最终子类必须直接调用顶层父类的构造函数
- 问题:当架构设计中需要继承时,无法确定使用直接继承还是虚继承!!
9、小结
- C++支持多重继承的编程方式
- 多重继承容易带来问题
- 可能出现 “同一个对象的地址不同” 的情况
- 虚继承可以解决数据冗余的问题
- 虚继承的使得架构设计可能出现问题
五十四、被遗弃的多重继承(下)
1、多重继承的问题三
- 多重继承可能产生多个虚函数表

2、编程实验:多重继承问题三

3、多重继承问题三
- 需要进行强制类型转换时,C++中推荐使用新式类型转换关键字!!
- 解决方案:dynamic_cast

4、正确的使用多重继承
- 工程开发中的“多重继承”方式:
单继承某个类+实现(多个)接口

5、编程实验:正确的多继承方式

6、正确的使用多重继承
- 一些有用的工程建议
- 先继承自一个父类,然后实现多个接口
- 父类中提供equal()成员函数
- equal()成员函数用于判断指针是否指向当前对象
- 与多重继承相关的强制类型转换用dynamic_cast完成
7、小结
- 多继承中可能出现多个虚函数表指针
- 与多重继承相关的强制类型转换用dynamic_cast完成
- 工程开发中采用 “单继承多接口” 的方式使用多继承
- 父类提供成员函数用于判断指针是否指向当前对象
五十五、经典问题解析四
1、关于动态内存分配
new和malloc的区别是什么?
delete和free的区别是什么?
2、关于动态内存分配
new关键字与malloc函数的区别
- new关键字是C++的一部分
- malloc是由C库提供的函数
- new以具体类型为单位进行内存分配
- malloc 以字节为单位进行内存分配
- new在申请内存空间时可进行初始化
- malloc仅根据需要申请定量的内存空间
下面的代码输出什么?为什么?

3、编程实验:new和malloc的区别

4、关于动态内存分配
new 和 malloc的区别
- new在所有C++编译器中都被支持
- malloc在某些系统开发中是不能调用
- new能够触发构造函数的调用
- malloc仅分配需要的内存空间
- 对象的创建只能使用new
- malloc不适合面向对象开发
下面的代码输出什么?为什么?

5、关于动态内存分配
- delete和free的区别
- delete在所有C++编译器中都被支持
- free在某些系统开发中是不能调用
- delete 能够触发析构函数的调用
- free仅归还之前分配的内存空间
- 对象的销毁只能使用delete
- free不适合面向对象开发
6、关于虚函数
构造函数是否可以成为虚函数?
析构函数是否可以成为虚函数?
7、关于虚函数
构造函数不可能成为虚函数
- 在构造函数执行结束后,虚函数表指针才会被正确的初始化
析构函数可以成为虚函数
- 建议在设计类时将析构函数声明为虚函数
8、编程实验:构造,析构,虚函数

9、关于虚函数
构造函数中是否可以发生多态?
析构函数中是否可以发生多态?
10、关于虚函数
构造函数中不可能发生多态行为
- 在构造函数执行时,虚函数表指针未被正确初始化
析构函数中不可能发生多态行为
- 在析构函数执行时,虚函数表指针已经被销毁
构造函数和析构函数中不能发生多态行为,只调用当前类中定义的函数版本!!
11、关于继承中的强制类型转换
继承中如何正确的使用强制类型转换?
dynamic_cast是与继承相关的类型转换关键字
dynamic_cast 要求相关的类中必须有虚函数
用于有直接或者间接继承关系的指针(引用)之间
指针:
- 转换成功:得到目标类型的指针
- 转换失败:得到一个空指针
引用:
- 转换成功:得到目标类型的引用
- 转换失败:得到一个异常操作信息
编译器会检查dynamic_cast的使用是否正确
类型转换的结果只可能在运行阶段才能得到
12、编程实验:dynamic_cast的使用

13、小结
- new / delete会触发构造函数或者析构函数的调用
- 构造函数不能成为虚函数
- 析构函数可以成为虚函数
- 构造函数和析构函数中都无法产生多态行为
- dynamic_cast是与继承相关的专用转换关键字
