六、C++的继承与多态——深入掌握OOP最强大的机制
这一部分内容可以直接看《C++ primer》第十五章,这里讲的基本上都是重复的。第十五章的最后一个小节还有一个综合性的代码案例,包含操作符重载、继承、多态等等。第十五章的笔记可以看我的另一篇随笔第十五章 面向对象程序设计
继承的基本意义
继承的本质(好处):
-
代码的复用;
-
在基类中给所有派生类提供统一的虚函数接口,让派生类进行重写,然后就能使用多态了。
类和类之间的关系:
- 组合 一部分的关系
- 继承 一种的关系
总结:1.外部只能访问对象public的成员,protected和private成员无法直接访问;2、在集成结构中,派生类从基类可以继承过来private的成员,但是派生类缺无法直接访问;3、protected和private的区别?在基类中定义的成员,想被派生类访问,但是不想被外部访问,那么就在基类中把相关成员定义为protected的;如果派生类和外部都不打算访问,那么在基类中就把相关成员定义为private的。
默认的继承方式是什么?要看派生类使用class定义的还是struct定义的。如果是class定义的派生类,默认继承方式是private的;如果是struct定义的派生类,默认继承方式是public的。
派生类的构造过程
派生类怎么初始化从基类继承来的成员变量呢?
- 派生类从基类可以继承来所有的成员(变量和方法),但是不包含构造函数和析构函数
- 通过调用基类相应的构造函数来初始化
重载、覆盖、隐藏
重载关系:
- 一组函数要重载必须处在同一个作用域当中,且函数名字相同,参数列表不同
在基类和派生类中的函数不能被重载,因为作用域不同。如果在基类中定义了重载函数,在派生类中可以直接调用,但是如果派生类中定义了与基类同名的函数,在调用派生类的时候只会产生派生类中的重载,不会与基类中的同名函数发生重载
隐藏关系:
- 在继承结构中,派生类的同名成员会把基类的同名成员隐藏起来,
基类和派生类的 类型转化
只能访问蓝色部分的内存,后面红色部分的内存是不存在的,访问基类指针会报内存非法访问
在继承结构中,进行上下的类型转换,默认只支持从下到上的类型转换。
覆盖关系:
基类和派生类的方法、返回值、函数名以及参数列表都相同,而且基类的方法是虚函数,那么派生类的方法就自动处理成虚函数,他们之间称为覆盖关系。
虚函数、静态绑定和动态绑定
虚函数virtual。RTTI(run-time type information)运行时的类型信息。
总结1.如果类里面定义了虚函数,编译阶段编译器给这个类类型产生一个唯一的vftable虚函数表,虚函数表中主要存储的内容就是RTTI指针和虚函数的地址。当程序运行时,每一张虚函数表都会加载到内存的.rodata区。
总结2.一个类里定义了虚函数,那么这个类定义的对象运行时,内存中的开始部分会多存储一个vfptr虚函数指针,指向相应类型的虚函数表vftable。一个的类型定义的n个对象,他们的vfptr指向的都是同一张虚函数表。
在这个例子里对象的大小是8个字节
总结3.一个类里面虚函数的个数不影响对象内存大小(Vfptr),影响的是虚函数表的大小
class Base{
public:
Base(int data=10): ma(data){}
//虚函数 virtual
virtual void show(){ cout<<"Base::show()"<<endl;}
virtual void show(int)(cout<<"Base::show(int)"<<endl;)
protected:
int ma;
};
总结4.如果派生类中的方法和基类继承来的某个方法,返回值、函数名、参数列表都相同,而且基类的方法是virtual虚函数,那么派生类的这个方法会自动处理成虚函数。
派生类中的虚函数表,如果派生类中有与基类中重复的函数,会在派生类的虚函数表中覆盖原来基类中的方法地址。
类中存在虚函数就会发生动态绑定。
静态绑定在汇编代码中会call具体的方法地址,而动态绑定call一个寄存器,寄存器会在运行时找到虚函数表中相应的方法的地址。
虚析构函数
问题1:那些函数不能实现成虚函数?
-
虚函数依赖:
- 虚函数能产生地址,存储在vftable中
- 对象必须存在(vfptr->vftable->虚函数地址)对象存在才有vfptr,才能找到vftable,才有虚函数地址
-
构造函数不能成为虚函数。构造函数中调用虚函数,不会发生动态绑定。构造函数调用的任何函数都是静态绑定的
-
静态成员方法也不能实现成虚函数
问题2:虚析构函数
- 析构函数调用的时候对象是存在的。
- 基类的析构函数是虚析构函数,派生类的析构函数会自动定义为虚析构函数
什么时候必须把基类的析构函数必须实现成虚函数?
基类的指针(引用)指向堆上new出来的派生类对象的时候,delete pb(基类的指针),他调用析构函数的时候,必须发生动态绑定,否则会导致派生类的析构函数无法调用
为什么要使用虚析构函数?
解释这个问题使用的代码:
//
// Created by 26685 on 2022-05-17 19:41.
// Description:ClassDerive.h 学习继承和多态
//
#ifndef C___CLASSDERIVE_H
#define C___CLASSDERIVE_H
#include <iostream>
using namespace std;
class Base{
public:
Base(int data=10):ma(data){
cout<<"Base()"<<endl;
}
~Base(){
cout<<"~Base()"<<endl;
}
virtual void show(){
cout<<"Base::show()"<<endl;
}
private:
int ma;
};
class Derive:public Base{
public:
Derive(int data=10): Base(data),mb(data),ptr(new int(data)){
cout<<"Derive()"<<endl;
}
~Derive(){
cout<<"~Derive()"<<endl;
}
void show() override{
cout<<"Derive::show()"<<endl;
}
private:
int mb;
int* ptr;//会指向额外的空间,必须由析构函数释放
};
#endif //C___CLASSDERIVE_H
如果在堆上开辟内存存放派生类会发生不调用派生类的析构函数的情况,代码如下:
int main(){
Base *pb=new Derive;
pb->show();
delete pb;
return 0;
}
输出的结果只有基类的析构函数:
这里的pb是Base类型,会去Base类中找析构函数,此时的绑定就是静态绑定。
基类的析构函数如果是虚函数,派生类中的析构函数会自动生成为虚析构函数。所以要将析构函数定义为虚析构函数。
virtual Base::~Base(){
cout<<"~Base()"<<endl;
}
Derive::~Derive(){
cout<<"~Derive()"<<endl;
delete ptr;
}
此时就会正确的析构:
再谈虚函数和动态绑定
问题:是不是虚函数的调用一定就是动态绑定? 答:不是的
在类的构造函数当中调用虚函数是静态绑定。
用对象本身调用虚函数是静态绑定
动态绑定发生在指针调用虚函数的情况下:
理解多态是什么
面试中常见的问题:如何解释多态
动态的多态:
静态的多态:
函数的重载
理解抽象类
拥有纯虚函数的类叫做抽象类,抽象类不能再实例化对象,但是可以定义指针和引用变量
virtual void 函数名()=0;
定义纯虚函数
一般把什么类设计为抽象类?
基类