多态
- 子类重写父类的成员函数,父类指针指向子类对象,利用父类指针调用重写的成员函数
- 在
C++
中,默认情况下,编译器只会根据指针类型调用对应对应的函数,不存在多态 - 同一操作作用于不同对象,可以有不同的解释,产生不同的执行结果
- 在运行时可以识别出真正的对象类型,调用对应子类中的函数
如果代码来执行并未按照我们的设想实现多态的效果,查看汇编
通过汇编可以看到这里会根据指针类型来直接调用函数,调用的函数地址此时是写死的
虚函数
C++
的多态是通过虚函数来实现的,需要加上关键字virtual
- 虚函数就是被
virtual
修饰的成员函数 - 只要在父类中声明为虚函数,子类中重写的函数也自动变为虚函数
可以看出通过virtual
的确实现了多态,那么究竟virtual
是怎么样实现多态的呢?
在不使用虚函数的情况下,Cat
对象占用的空间应当是8
个字节,分别是两个int
型的成员变量各自占4
个字节,总共8
个字节,那么先来看看使用虚函数后Cat
对象是否有变化
可以看到Cat
对象占有12
个字节,而且不论virtual
定义一个或者两个成员函数,它都占用12
个字节,也就是说额外的又多了4
个字节
根据上图可以看到成员变量从对象首地址偏移4个字节开始存储
虚表
虚函数的实现原理是虚表,这个虚表中存储着最终需要调用的虚函数的地址,这个虚表也叫虚函数表
下面是调用Cat::speak
的汇编代码
ebp-8就是cat指针
取出cat指针的内容也就是Cat对象的地址放入eax寄存器
00ED668E mov eax,dword ptr [ebp-8]
从Cat对象地址开始读取4个字节放入edx寄存器
edx内存储的就是虚表的地址
00ED6691 mov edx,dword ptr [eax]
从虚表中取出前四个字节放入eax
eax存放的就是Cat::speak函数的地址
00ED6698 mov eax,dword ptr [edx]
00ED669A call eax
接下来是调用Cat::run
的汇编代码
ebp-8就是cat指针
取出cat指针的内容也就是Cat对象的地址放入eax寄存器
00ED66A3 mov eax,dword ptr [ebp-8]
从Cat对象地址开始读取4个字节放入edx寄存器
edx内存储的就是虚表的地址
00ED66A6 mov edx,dword ptr [eax]
从虚表首地址偏移四个字节作为开始出取出前四个字节放入eax
eax存放的就是Cat::run函数的地址
00ED66AD mov eax,dword ptr [edx+4]
00ED66B0 call eax
可以看出Cat
对象的前四个字节存储的就是虚表的地址,然后根据虚表的地址去获取所需调用虚函数的地址
下图便是内存中关于虚表的一个图示
虚函数的一些细节
- 如果子类未实现某一个成员函数
这时run
函数调用的就是父类的run
,但是并不像我们想的是子类没有就去父类查找,一层一层查找,并非是这样的,而是直接把父类run
函数的地址放在了虚表中
- 只要类中成员方法使用了
virtual
关键字则就会使用虚表
调用父类成员函数的实现
在别的编程语言中如果要调用父类成员函数的话有的是使用关键字super
来调用父类的实现,但在C++中并没有super
关键字
这里调用父类的speak
函数时直接调用地址而非使用虚表
虚析构函数
如果存在父类指针指向子类对象的情况,则应将析构函数声明为虚函数(虚析构函数),这样在delete
父类指针时才会调用子类的析构函数,才能确保完整性
可以看出构造函数调用正确,但是析构函数并未调用Cat
的析构函数,因为在析构时是看指针类型而调用的Animal
的析构函数,并没有使用虚表,所以要正确调用的话需要把析构函数声明为虚函数,父类的析构函数声明为虚函数后,子类的析构函数虽然与父类析构函数不同名,但是也会默认变为虚函数
纯虚函数
纯虚函数
:没有函数体且初始化为0的函数,用来定义接口规范
抽象类
- 含有纯虚函数的类,不可以实例化
- 抽象类也可以包含非纯虚函数、成员变量
- 如果父类是纯虚函数,子类没有完全重写纯虚函数,则子类也是纯虚函数
如上图所示Animal
就是一个抽象类,Dog
也是一个抽象类,但是Hashiqi
并不是抽象类
多继承
C++
允许一个类有多个父类,但并不建议使用多继承,会加大程序设计的复杂度
多继承后类的结构
根据汇编代码以及内存中的数据可以看出整个Undergraduate
对象占用12
个字节,成员变量依次是m_score
、m_salary
、m_grade
,分别占用4
个字节,顺序是按继承的顺序来排列
多继承体系下的构造函数调用
多继承下的虚函数
如果子类继承的多个父类都有虚函数,那么子类的对象就会产生对应的多张虚表
这里看到study
和work
的调用明显是采用虚表来调用的,但是二者并未在同一张表里,存放study
的虚表地址即为对象的首地址开始取4
个字节,但是存放work
的虚表地址是对象首地址偏移8
个字节,从这里可以看出多继承下多个父类都有虚函数,则子类对象会产生多张虚表
在内存中看的话,是按继承顺序来排列虚表和成员对象的,为了验证本次猜想,现在将study
不再作为虚函数,如果猜想正确的话本次调试应该只有一张虚表
通过汇编来看,本次仅有一张虚表,这次虚表地址是对象首地址,而且成员变量的顺序有改变
多继承下的同名函数
多继承下的同名函数调用如下,从汇编来看的话就是直接调用各自eat
函数的地址
多继承下同名的成员变量
与上面举例大致相同,调用同名成员变量时需加上作用域
C++
允许子类与父类有同名的成员变量和函数,OC
中则不允许,虽然是同名成员变量,但是从本质来看只是给不同的内存单元赋值而已
菱形继承
上述代码打印结果为20,就意味着Undergraduate
对象占有20个字节,原因就是m_age
成员变量存在两份,分别继承于Student
和Worker
菱形继承带来的问题:
- 最底下子类从基类继承的成员会出现冗余和重复
- 最底下子类无法访问基类的成员,会产生二义性
虚继承-解决菱形继承带来的问题
使用虚继承后打印结果为24
,还比之前占内存更多了
Student
那么现在先分开来看看,使用虚继承后究竟产生了什么影响,首先先实例化一个Student
对象,可以看出占用了12
个字节,如果不是虚继承的话应该占用8
个字节,而且排列顺序也应当是m_age
之后是m_score
,但是现在完全不同
再看看最前面四个字节中前4
个字节是0
,后四个字节是8
用图表示则是以下
Worker
同样来看看Worker
的实例对象内存分布,发现与Student
基本相同
Worker
的内存分布图示如下
Undergraduate
Undergraduate
对象占用24个字节的内存分布如下
总结后如下,并且要注意Student
中虚表指针中的20
表示m_age
距离Studen
t首地址的偏移,Worker
中虚表指针中的12
表示m_age
距离Worker
首地址的偏移,而非距离Undergraduate
的首地址
虚表指针总结:
- 前四个字节表示虚表指针与本类起始的偏移量
- 后四个字节表示虚基类第一个成员变量与本类起始的偏移量