继承
语法:
class 类:继承方式 基类,...{
};
继承方式:
public :公开继承
protected :保护继承
private :私有继承 默认
继承方式决定了基类中的属性继承到子类中的访问控制属性
子类拥有父类所有的属性和方法
1.子类的构造函数
缺省的构造函数会默认调用父类的无参构造函数
构造函数的执行顺序:
(1)按照继承顺序依次调用基类的构造函数 默认调用无参构造
(2)按照成员的声明顺序 依次调用 类类型成员的构造函数 默认调用无参构造
(3)执行构造函数体
如果手动实现 默认调用基类和成员的 无参构造 如需指定则需要在初始化列表中指定
构造函数(形参列表):基类(实参),成员名(实参){}
2.子类的析构函数
缺省析构默认调用父类和成员的析构函数
手动实现 默认的也会调用 基类和成员的析构函数
析构函数的执行顺序正好和构造函数相反
3.拷贝构造和拷贝赋值
缺省拷贝构造和拷贝赋值会调用基类和成员的拷贝构造和拷贝赋值函数
手动实现拷贝构造: 默认调用基类和成员无参构造 需要手动在初始化列表中指定
手动实现拷贝赋值函数: 不会默认实现拷贝基类子对象和成员对象 需要手动实现拷贝 或者 调用基类和成员的拷贝赋值函数
4.子类对象 isA 父类类型对象
可以用子类对象来拷贝构造父类类型对象
直接把子类对象赋值给父类类型对象
在子类中一定存在着父类子对象 (基类子对象)
子类对象的指针可以隐式转换成基类类型的指针
子类对象的引用可以隐式转换成基类类型的引用
在多继承中,会存在着多个基类子对象 这些基类子对象按照继承顺序依次存储在子对象的内存中
子对对象的指针转换成基类类型的指针时 地址值 是不一样的 实际上会转换成对应基类子对象的首地址
基类对象不能赋值给子类对象
基类指针 也不能转换成为 子类指针
基类引用 也不能转换成为 子类引用
可以用static_cast<>() 进行转换 无论目标是否一致 都会成员 能够确保转换之后 获得子类对象的首地址
但是reinterpret_cast<>() 无法确定转换之后的地址一定是 子类对象的首地址
5.子类对象拥有基类的属性和方法
子类拥有和基类同名的属性和方法
直接访问都是访问子类自己的 子类的属性和方法会隐藏掉基类同名的属性和方法
可以用 基类名::同名属性 方式来解隐藏
用基类的指针指向子类对象 只能访问 基类的属性和方法
用基类的引用引用子类对象 只能访问 基类的属性和方法
用父类的指针 指向 new 子类对象; 在delete这个对象时 只会调用父类的析构函数 造成内存泄露
6.钻石继承
最终子类继承自多个基类,而多个基类里面至少两个基类的拥有相同的基类部分
相同基类部分的成员会沿着不同的继承分支到最终子类中呈现多份
子类对象调用不同分支的方法得到的结果并不一致
可以用虚继承来解决钻石数据不统一的问题
如果按照虚继承 那么在最终的子类中 只会拥有一份共同基类的成员
7.C++ python中允许多继承 java中不允许多继承
所谓多继承,子类拥有所有的基类的特征和行为 继承所有基类的属性和方法
子类对象的指针和引用 可以转换为任一基类类型的指针和引用
8.虚函数
如果一个成员函数声明为virtual
那么该函数就为虚函数
在子类中可以重写(覆盖)该虚函数
用基类的指针指向子类对象 只能访问 基类的属性和方法
如果用基类的指针指向子类对象 调用 基类中的虚函数,调用的是子类对象的自己重写版本的虚函数
用基类的引用引用子类对象 只能访问 基类的属性和方法
9.重写(覆盖)
重载 重写 隐藏
重写: 子类重写父类的虚函数
1.父类的函数必须是虚函数
2.子类的函数名和父类的函数名必须一致
3.函数的参数及 常属性必须一致
普通类型的参数 常属性可以不一致
对于类类型的引用 和 指针 常属性必须一致
4.如果是基本数据类型 和 类类型(不是指针和引用) 返回值必须一致
如果返回值是类类型的引用或者指针 则子类重写的版本可以是父类版本返回值类型的子类类型的指针和引用
5.子类重写版本的异常说明不能比父类声明抛出更多的异常
6.子类重写版本的访问控制属性不受父类访问控制属性的影响
重载: 同一个作用域下,函数名相同,参数列表不同 与返回值类型无关 即构成重载
隐藏: 子类隐藏父类同名的属性和方法 方法名相同
如果有virtual关键字,如果不是重写 即构成隐藏
如果没有virtual关键字,如果不是重载,则隐藏
拥有虚函数的类 --虚函数表(属于类) 和 虚表指针(属于每一个对象)
如果一个类拥有虚函数(个数无关,只要有) 那么该类比没有虚函数时 会增加 sizeof(指针) 字节内存
这个内存用于存储一个指针 这个指针就是虚表指针 有虚函数类的对象 每一个对象都拥有一个指针
如果一个类拥有虚函数 那么在编译时将会给这个类 生成一个虚函数表( 虚函数指针数组)
虚函数表: 指针数组 数组中的每一项都是一个虚函数的地址
如果一个类继承的基类有虚函数,那么该子类也会继承基类的虚函数表生成新子类自己的
如果子类有重写父类的虚函数,那么子类虚函数表中对应的值(虚函数地址)将会被子类重写的虚函数覆盖
虚表指针: 指向本类虚函数表的指针 (同一个类的对象他们的虚表指针值一样)
10.多态 多态=虚函数+指针/引用
虚函数 重写 基类指针指向子类对象 基类引用引用子类对象
用父类的指针指向子类对象 或者 用父类的引用引用子类对象
去调用父类中的虚函数时, 并不是调用的父类的虚函数,而是调用到子类重写的版本,这个现象就称为多态
重写之后,调用的函数不再决定于指针 和 引用本身的类型,而是目标对象的类型
11.运行时绑定 动态绑定
静态绑定 调用重载方法时 在编译阶段根据实参类型和个数来绑定对应的方法
多态 动态绑定 在编译阶段无法确定调用哪个方法 只有当运行的时候才能够确定调用哪一个方法
在编译时,基类指针调用虚函数 or 基类引用调用虚函数时 并不急于生成调用代码的指令,
而是用一段指令来替换调用指令:
(1)能够通过指针获得目标对象 or 通过引用获取引用目标 目标对象中有虚表指针
(2)通过虚表指针 找到 虚函数表
(3)调用函数其实就是去虚函数表中找到对应的函数地址 去指定地址执行
多态的执行效率偏低
12.dynamic_cast<>
用于多态父子指针或者引用之间的转换
一定是多态,没有多态将编译报错
13.typeid
求类型 和 对象的 类型信息
对于普通的类型和对象 就是其本身的类型
如果有多态, 对父类的指针和引用 求 typeid(*父类指针) typeid(父类引用) 得到是目标真实的类型
#include <typeinfo>
typeid().name()
typeid() == != 支持
14.虚析构
把基类的析构函数设置为virtual
那么在用基类的指针 = new 子类对象时
delete 基类指针; 调用的是子类的析构函数 子类的析构函数默认会调用父类的析构函数
之前内存泄露的问题就解决了
在很多情况下,即使不使用多态,也会把析构函数声明为virtual
为了防止内存泄露
15.纯虚函数 和 抽象类
虚函数没有函数体 = 0 称为纯虚函数
拥有纯虚函数的类称为抽象类 抽象类不能实例化对象
继承抽象类,就会继承纯虚函数,如果子类不覆盖纯虚函数,那么子类也是抽象类
如果一个类只有纯虚函数,那么该类就是纯抽象类
抽象类的意义:
1.提供一个公共的类型
2.虚函数提供一个统一的接口,子类拥有不同的实现
3.基类的实现没有意义
关于虚函数的问题:
1.内联函数可以是虚函数吗? 不可以
2.全局函数可以是虚函数吗? 不可以
3.以成员方式重载的运算符可以是虚函数吗? 可以
4.构造函数可以是虚函数吗? 不可以
5.析构函数可以是虚函数吗? 可以 建议
6.静态成员函数可以是虚函数吗? 不可以