继承
1 什么是继承
当遇到问题,先看一下现有的类是否能够解决一部分问题,如果有则继承,并在此基础上进行扩展来解决所有问题,以此缩短解决问题的时间(代码复用)
当遇到一个大而复杂的问题时,可以把复杂问题拆分成若干个小问题,为每个小问题的解决设计一个类,最后通过继承的方式把这些类汇总到一个类中,从而达到解决大问题的目的,以此降低问题的规模,也可以让多个程序员一起解决该问题
子类继承父类 派生类继承基类
2 如何继承
2.1 继承表
class Son : 继承表[继承方式 父类名1,继承方式 父类名2...]
{
成员变量;
public:
成员函数;
}
2.2 继承方式
public protected private
3 C++的继承特点
① C++中的继承可以有多个父类
② 子类会继承父类的所有内容(能否使用另说)
③ 子类对象可以向它的父类类型自动转换(父类类型的指针或引用可以指向子类对象)(允许缩小),但是父类对象不可以向它的子类类型自动转换(子类类型的指针或引用不可以指向父类对象)(不允许扩大)
Base* b = new Son; true
Base& b = son; true
Son* s = new Base; false
Son& s = base; false
④ 子类必须以public方式继承父类,才可以让子类对象向它的父类类型自动转换
⑤ 子类会隐藏父类的同名成员,在子类中直接访问同名成员时访问到的是子类的成员
但可以通过 父类名::同名成员 的方式访问父类的同名成员
⑥ 子类与父类的同名成员函数不能构成重载,因为不在同一个作用域下,子类会隐藏父类的同名函数,只能通过 父类名::同名成员函数() 的方式访问
⑦ 在执行子类的构造函数时,都会先按照继承表顺序执行父类的构造函数,默认执行父类的无参构造,也可以在子类的构造函数的初始化列表中显示调用父类的有参构造,然后再执行类类型成员的构造函数,最后执行子类的构造函数
Son(int num){} // Base的无参构造
Son(int num):Base(num){} // Base的有参构造
⑧ 在执行子类的析构函数后,再执行类类型成员的析构函数,最后执行父类的析构函数,会按照继承表的逆序执行
⑨ 当子类执行拷贝构造时,默认会调用父类的无参构造,这是不合理的,理应调用父类的拷贝构造进行,因此需要在子类的拷贝构造函数的初始化列表中显式地调用父类的拷贝构造
Son(const Son& that):Base(that) //调用Base拷贝构造
⑩ 当子类执行赋值操作时,默认不会执行父类的赋值函数,如果需要调用父类的赋值函数,可以在子类赋值函数中以 父类名::operator=(that) 方式调用
Son& operator=(const Son& that)
{
Base::operator=(that); // 调用父类的赋值操作
}
4 继承方式与访问控制属性
4.1 访问控制属性:限制类成员的访问范围
public: 可以在类内外任意位置访问
private: 只能在类内访问
protected:只能在类内和子类中访问
4.2 继承方式的影响:
① 父类的成员是否能在子类中访问,是由父类设计时成员的访问控制属性决定的,不受子类继承方式的影响
② 如果子类被别的孙子类继承,子类的继承方式会影响父类的成员在孙子类中的访问情况,详情见表格
③ 只有以public继承父类后,父类的指针或引用才能指向子类对象(是多态的基础)
5 多重继承与菱形继承
5.1 多重继承
当一个子类继承多个父类时,就称为多重继承
子类中会按照继承表中的顺序排列父类成员,并会标记记录父类成员与首地址的偏移值,当用父类指针指向子类对象时,编译器会自动让父类指针加上对应的偏移值,指向子类中该父类成员的首地址
当多个不同的父类指针指向同一个子类首地址时,它们的地址值会不同
5.2 菱形继承
假如有一个类A,类B、类C都各自继承了类A,类D又同时继承类B和类C,此时就构成菱形继承
问题:
类B、类C中都有类A的内容,类D继承B、C后就有两份类A的内容,还不会报错
但当类D的对象访问类A的内容时,此时会冲突,因为这样的内容有两份,编译器因无法分辨使用哪份而编译报错
5.3 虚继承 virtual
当使用virtual关键字修饰继承时,子类中多增加一个虚指针,用于指向父类内容
当该子类被继承时,孙子类也会继承该虚指针,如果孙子类中有多个虚指针,则编译器会比较这些虚指针指向的父类内容是否是同一份,如果是,则只保留一份
总结:虚继承目的是为了解决孙子类中有多份相同的祖先类的问题
注意:有些人认为多继承是C++的缺陷之一,一般建议不使用多继承,一定不要设计成菱形继承,C++提供的解决方法是使用虚继承,而一些后面的编程语言没有多继承,例如JAVA
6 虚函数、虚函数表、虚指针、覆盖
6.1 虚函数
当成员函数被virtual修饰后,就称为虚函数
6.2 虚指针
有虚函数的类中会多一个虚指针(虚表指针)
6.2 虚函数表
虚指针中记录的是一张表格的首地址,该表格中记录的是该类中所有虚函数的地址
((void(*)(void))(*(int*)*(int*)b))();// 调用的是虚函数表中的第一个函数 void func(void)
6.3 覆盖:(重写)
当使用virtual修饰父类的成员函数时,父类中就会多一个虚表指针,子类会把父类中的虚表指针以及虚函数表一起继承过来,如果子类中有与父类虚函数同名的函数时,编译器会去比较这两个同名函数的格式,如果格式完全相同,那么会把子类中同名函数的地址覆盖子类中虚函数表中的同名父类虚函数的地址,这种情况就称为覆盖
此时使用父类指针或引用指向子类对象时,调用父类的虚函数则会调用被覆盖后的子类的同名且格式相同的成员函数
构成覆盖的必要条件:
① 子类public继承父类
② 父类中被覆盖的函数必须为虚函数
③ 子类中必须有与父类虚函数名字、返回值类型、参数列表、常属性都完全相同的成员函数
④ 返回值类型相同、或者如果子类成员函数的返回值可以向父类虚函数返回值类型进行隐式转换且具有继承关系,也可以构成覆盖
多态
1 什么是多态?
多态指的是同一个指令的多种形态,当调用同一个指令时,能根据参数、环境不同做出相应的操作
C++中根据确定执行哪个操作的时间点分成:编译时多态、运行时多态
1.1 编译时多态
当调用重载函数时,编译器会根据参数的类型、个数等,在编译期间就能确定指定哪个版本的重载函数,属于编译时多态,还有模板技术等
1.2 运行时多态
当子类覆盖了父类中的同名虚函数,然后用父类指针或引用访问虚函数时,它可能调用父类中的虚函数,也可能调用其它覆盖了虚函数的子类中的同名函数,具体调用哪个版本是根据父类指针或引用指向哪个对象而决定的,而这个父类指向哪个对象可以在运行时才能确定,因此这种就称为运行时多态
1.3 构成运行时多态的必要条件:
① 父子类中必须构成覆盖
② 运行时才能确定父类指针或引用访问的是哪个构成覆盖的子类或者是父类
1.4 虚构造与虚析构
1.4.1 虚构造
构造函数不能是虚函数
假设构造函数可以定义为虚函数,在实例化子类对象时,先执行子类构造函数的初始化列表,那么就会先根据继承表顺序执行父类的构造函数,此时父类构造函数为虚函数被子类构造函数覆盖,又变成执行子类的构造函数,导致进入死循环,因此编译器禁止把构造函数定义为虚函数
1.4.2 虚析构
析构函数可以定义为虚函数
当使用类多态时,通过父类指针或引用释放子类对象时,默认只会执行父类的析构函数,不会去执行子类的析构函数,但是在创建子类对象时,已经调用了子类的构造函数,如果不执行子类的析构函数,可能会导致内存泄漏。
只有把父类的析构函数定义为虚函数(子类析构会自动覆盖,不关注名字是否相同),通过父类指针或引用释放子类对象时,就会先调用覆盖后子类的析构函数,而子类的析构函数执行完后,会自动调用父类的析构函数(对象销毁规则),因此不会导致内存泄漏
注意:当使用了多态,且子类的构造函数中申请了资源时,则父类的析构函数一定要设置为虚析构
1.5 纯虚函数、纯抽象类
1.5.1 纯虚函数
class Base
{
public:
virtual 返回值 函数名(参数列表) = 0;
};
① 纯虚函数的本意就是为了让子类来覆盖,所以纯虚函数不要去实现它
② 有纯虚函数的类不能创建该类的对象
③ 父类中有纯虚函数,子类必须覆盖,否则连对象都无法创建
④ 纯虚函数就是为了强制子类去覆盖,为了强制实现某些功能
⑤ 含有纯虚函数的类称为抽象类
⑥ 析构函数不能设置为纯虚函数文章来源:https://www.toymoban.com/news/detail-476231.html
1.5.2 纯抽象类
所有的成员函数都是纯虚函数,这种类称为纯抽象类,一般用于设置功能接口函数,也叫做接口类,它自己不实现具体功能文章来源地址https://www.toymoban.com/news/detail-476231.html
到了这里,关于C++基础篇:07 继承与多态的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!