C++类和对象(三)

170 阅读8分钟

多态

  • 子类重写父类的成员函数,父类指针指向子类对象,利用父类指针调用重写的成员函数
  • C++中,默认情况下,编译器只会根据指针类型调用对应对应的函数,不存在多态
  • 同一操作作用于不同对象,可以有不同的解释,产生不同的执行结果
  • 在运行时可以识别出真正的对象类型,调用对应子类中的函数

image.png

image.png

如果代码来执行并未按照我们的设想实现多态的效果,查看汇编

image.png

通过汇编可以看到这里会根据指针类型来直接调用函数,调用的函数地址此时是写死的

虚函数

  • C++的多态是通过虚函数来实现的,需要加上关键字virtual
  • 虚函数就是被virtual修饰的成员函数
  • 只要在父类中声明为虚函数,子类中重写的函数也自动变为虚函数

image.png

image.png

可以看出通过virtual的确实现了多态,那么究竟virtual是怎么样实现多态的呢?

在不使用虚函数的情况下,Cat对象占用的空间应当是8个字节,分别是两个int型的成员变量各自占4个字节,总共8个字节,那么先来看看使用虚函数后Cat对象是否有变化

image.png image.png

可以看到Cat对象占有12个字节,而且不论virtual定义一个或者两个成员函数,它都占用12个字节,也就是说额外的又多了4个字节

image.png

根据上图可以看到成员变量从对象首地址偏移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对象的前四个字节存储的就是虚表的地址,然后根据虚表的地址去获取所需调用虚函数的地址

下图便是内存中关于虚表的一个图示

image.png

虚函数的一些细节

  • 如果子类未实现某一个成员函数

image.png

image.png

image.png

这时run函数调用的就是父类的run,但是并不像我们想的是子类没有就去父类查找,一层一层查找,并非是这样的,而是直接把父类run函数的地址放在了虚表中

  • 只要类中成员方法使用了virtual关键字则就会使用虚表

image.png

image.png

image.png

调用父类成员函数的实现

在别的编程语言中如果要调用父类成员函数的话有的是使用关键字super来调用父类的实现,但在C++中并没有super关键字

image.png

image.png

image.png

这里调用父类的speak函数时直接调用地址而非使用虚表

虚析构函数

如果存在父类指针指向子类对象的情况,则应将析构函数声明为虚函数(虚析构函数),这样在delete父类指针时才会调用子类的析构函数,才能确保完整性

image.png

image.png

image.png

可以看出构造函数调用正确,但是析构函数并未调用Cat的析构函数,因为在析构时是看指针类型而调用的Animal的析构函数,并没有使用虚表,所以要正确调用的话需要把析构函数声明为虚函数,父类的析构函数声明为虚函数后,子类的析构函数虽然与父类析构函数不同名,但是也会默认变为虚函数

image.png

image.png

纯虚函数

  • 纯虚函数:没有函数体且初始化为0的函数,用来定义接口规范

抽象类

  • 含有纯虚函数的类,不可以实例化
  • 抽象类也可以包含非纯虚函数、成员变量
  • 如果父类是纯虚函数,子类没有完全重写纯虚函数,则子类也是纯虚函数

image.png

如上图所示Animal就是一个抽象类,Dog也是一个抽象类,但是Hashiqi并不是抽象类

多继承

C++允许一个类有多个父类,但并不建议使用多继承,会加大程序设计的复杂度

多继承后类的结构

image.png

image.png

image.png

根据汇编代码以及内存中的数据可以看出整个Undergraduate对象占用12个字节,成员变量依次是m_scorem_salarym_grade,分别占用4个字节,顺序是按继承的顺序来排列

image.png

多继承体系下的构造函数调用

image.png

多继承下的虚函数

如果子类继承的多个父类都有虚函数,那么子类的对象就会产生对应的多张虚表

image.png

image.png

image.png

这里看到studywork的调用明显是采用虚表来调用的,但是二者并未在同一张表里,存放study的虚表地址即为对象的首地址开始取4个字节,但是存放work的虚表地址是对象首地址偏移8个字节,从这里可以看出多继承下多个父类都有虚函数,则子类对象会产生多张虚表

image.png

在内存中看的话,是按继承顺序来排列虚表和成员对象的,为了验证本次猜想,现在将study不再作为虚函数,如果猜想正确的话本次调试应该只有一张虚表

image.png

image.png

通过汇编来看,本次仅有一张虚表,这次虚表地址是对象首地址,而且成员变量的顺序有改变

image.png

多继承下的同名函数

多继承下的同名函数调用如下,从汇编来看的话就是直接调用各自eat函数的地址

image.png

image.png

image.png

多继承下同名的成员变量

与上面举例大致相同,调用同名成员变量时需加上作用域

image.png

image.png

C++允许子类与父类有同名的成员变量和函数,OC中则不允许,虽然是同名成员变量,但是从本质来看只是给不同的内存单元赋值而已

菱形继承

image.png

image.png

上述代码打印结果为20,就意味着Undergraduate对象占有20个字节,原因就是m_age成员变量存在两份,分别继承于StudentWorker

image.png

菱形继承带来的问题:

  • 最底下子类从基类继承的成员会出现冗余和重复
  • 最底下子类无法访问基类的成员,会产生二义性

虚继承-解决菱形继承带来的问题

image.png

使用虚继承后打印结果为24,还比之前占内存更多了

Student

那么现在先分开来看看,使用虚继承后究竟产生了什么影响,首先先实例化一个Student对象,可以看出占用了12个字节,如果不是虚继承的话应该占用8个字节,而且排列顺序也应当是m_age之后是m_score,但是现在完全不同

image.png

再看看最前面四个字节中前4个字节是0,后四个字节是8

image.png

用图表示则是以下

image.png

Worker

同样来看看Worker的实例对象内存分布,发现与Student基本相同

image.png

image.png

Worker的内存分布图示如下

image.png

Undergraduate

Undergraduate对象占用24个字节的内存分布如下

image.png

image.png

image.png

总结后如下,并且要注意Student中虚表指针中的20表示m_age距离Student首地址的偏移,Worker中虚表指针中的12表示m_age距离Worker首地址的偏移,而非距离Undergraduate的首地址

image.png

虚表指针总结:

  • 前四个字节表示虚表指针与本类起始的偏移量
  • 后四个字节表示虚基类第一个成员变量与本类起始的偏移量