C++ -- 多态(2)

简介: 1. 多态的概念通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。举个例子:比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人买票时是优先买票。2. 多态的定义和实现2.1 满足条件必须通过基类的指针或者引用调用虚函数被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

4. 多态的原理

4.1 引出

// 这里常考一道笔试题:sizeof(Base)是多少?
#include <iostream> 
using namespace std;
class Base
{
public:
  virtual void Func1()
  {
    cout << "Func1()" << endl;
  }
private:
  int _a = 0;
  int _b = 1;
  char _c;
};
int main()
{
  Base B;
  cout << sizeof(B) << endl;
  return 0;
}

考虑到内存对齐,这里不应该是12字节大小吗,为什么是16字节大小呢?通过监视窗口观察

微信图片_20230524025603.png

这里我们发现多了一个_vfptr的void**类型的指针。对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function,ptr代表pointer)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。

4.2 多态的原理

看下面代码思考一个问题:多态满足条件中有个条件是:必须通过基类的指针或者引用调用虚函数。那么这里怎么来实现的用引用或者指针就可以调用不同类的成员函数?为什么用普通类型就不能实现调用不用类的成员函数呢?

//构成多态
#include <iostream> 
using namespace std;
class Person {
public:
  virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
  virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
void Func(Person& p)
{
  p.BuyTicket();
}
int main()
{
  Person mike;
  Func(mike);
  Student johnson;
  Func(johnson);
  return 0;
}

微信图片_20230524025726.png

通过观察上述问题就可以得以解决,其实就是_vfptr指针来实现调用不同类的成员函数的

4.3 多态和非多态调用区别

4.3.1 非多态

微信图片_20230524025752.png

直接按照对应对象的类型进行调用成员函数。

4.3.2 多态

微信图片_20230524025820.png

上述观察到,进行执行p.BuyTicket()语句时,编译器是不知道调用哪个类的成员函数的,当完成p.BuyTicket()语句后, 编译器做出了处理工作,直接跳转到Person::BuyTicket()成员函数。怎么证明进行执行p.BuyTicket()语句时编译器是不知道调用哪个类的成员函数的?

微信图片_20230524025844.png

通过调试观察到,的确执行p.BuyTicket()语句时编译器是不知道调用哪个类的成员函数的。

4.4 虚函数表

微信图片_20230524025913.png

可以看出来_vfptr是一个指向虚函数表的指针,这个虚函数表里面就有很多对应的成员函数指针。虚函数表本质就是一个函数指针数组。前面我们把虚函数重写也称为覆盖,这里介绍完虚函数表再来看为什么叫做覆盖?

微信图片_20230524025934.png

上述我们观察到BuyTicket()成员函数进行了重写,但是travel()和ClaimCoupon()成员函数并没有重写,此时观察到Person类维护的有一个虚函数表,Student维护的也有一个虚函数表,BuyTicket()成员函数重写了也就覆盖了,可以这么理解,原本的子类是继承父类的成员函数的,那么当前子类的虚函数表也就是父类的虚函数表,但是当子类对父类的虚函数重写后,原本的父类的虚函数就被覆盖为新的重写的虚函数,这个例子的体现就在_vfptr[0]位置上的BuyTicket()虚函数指针。 _vfptr[1]和 _vfptr[2]两个虚函数并没有被重写,所以就是继承的父类的虚函数。

4.5 回望满足条件

4.5.1 虚函数重写

我们描述了虚函数表,那么如果父类中没有虚函数的话就不能构成多态,再来看这句话不难理解了,因为没有虚函数,就没有虚函数表,当使用父类的引用和指针来调用对应的函数时,就不会在虚函数表中查找,而是直接依据类型来进行调用。

4.5.2 父类指针或引用调用

为什么指针或者引用可以调用相对应的成员函数,但是对象不行呢?对象也能够完成切片啊?原因还是在虚函数表上,对象的话就要拷贝,这里也就要拷贝虚函数表,如果拷贝就会有很大的问题:就比如子类拷贝给了父类,此时就分不清了,父类的虚函数表也是子类的了。

4.6 打印虚函数表

4.6.1 引出

#include <iostream>
using namespace std;
class Base
{
public:
  virtual void Func1()
  {
    cout << "Base::Func1()" << endl;
  }
  virtual void Func2()
  {
    cout << "Base::Func2()" << endl;
  }
  void Func3()
  {
    cout << "Base::Func3()" << endl;
  }
private:
  int _b = 1;
};
class Derive : public Base
{
public:
  virtual void Func1()
  {
    cout << "Derive::Func1()" << endl;
  }
  virtual void Func4()
  {
    cout << "Derive::Func4()" << endl;
  }
private:
  int _d = 2;
};
int main()
{
  Base b;
  Derive d;
  return 0;
}

微信图片_20230524030054.png

上述观察到虚函数表中并没有对应的子类Derive中的Func4()函数,这里Func4()不是virtual修饰的虚函数吗,为什么子类中的虚函数表没有呢?通过内存窗口看(怀疑状态):

image-20230405193522234image-20230405193522234

这里类中的函数地址怎么打印出来呢?下面的工作就是打印虚函数表。

4.6.2 打印虚函数表

虚函数表是一个函数指针数组,所以打印这里的函数指针数组即可。虚函数表是以nullptr为结束标记的。虚函数表指针是对象前四个或者八个字节。

#include <iostream>
using namespace std;
class Base
{
public:
  virtual void Func1()
  {
    cout << "Base::Func1()" << endl;
  }
  virtual void Func2()
  {
    cout << "Base::Func2()" << endl;
  }
  void Func3()
  {
    cout << "Base::Func3()" << endl;
  }
private:
  int _b = 1;
};
class Derive : public Base
{
public:
  virtual void Func1()
  {
    cout << "Derive::Func1()" << endl;
  }
  virtual void Func4()
  {
    cout << "Derive::Func4()" << endl;
  }
private:
  int _d = 2;
};
typedef void(*_vfptr)(); //void(*_vfptr)() --> 函数指针 --> 起别名为_vfptr
void print_virtual_function(_vfptr table[]) //打印虚函数表 table -> 数组名 -> _vfptr*(类型)
{
  for (int i = 0; table[i]; ++i) //虚函数表是以nullptr为结束标记的
  {
    printf("[%d]:%p\n", i, table[i]);
  }
  cout << endl;
}
int main()
{
  Base b;
  Derive d;
  //只是支持32位机器下
  //&b -> Base* -> (int*)&b -> 强制转换为int* -> (*(int*)&b) -> 拿到b对象的前四个字节 -> (_vfptr*)(*(int*)&b) -> 强制转换为_vfptr*(函数指针的指针)
  print_virtual_function((_vfptr*)(*(int*)&b));  
  print_virtual_function((_vfptr*)(*(int*)&d)); 
  //支持32和64位机器下
  print_virtual_function((_vfptr*)(*(long long*)&b));
  print_virtual_function((_vfptr*)(*(long long*)&d));
  //支持32和64位机器下
  //&b -> Base* -> (_vfptr**)&b -> Base*强制转换为_vfptr**(函数指针的指针) -> (*(_vfptr**)&b) -> _vfptr*
  print_virtual_function((*(_vfptr**)&b)); 
  print_virtual_function((*(_vfptr**)&d));
  return 0;
}

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-37TauRBN-1681121198967)(https://jinyinhan.oss-cn-beijing.aliyuncs.com/QQ截图20230405195810.png)]


通过观察可以发现Func4()这个函数是在Derive这个子类的虚函数表中的,只不过这个监视窗口进行了修饰而已,其实是有的。对打印窗口优化:

typedef void(*_vfptr)(); //void(*_vfptr)() --> 函数指针 --> 起别名为_vfptr
void print_virtual_function(_vfptr table[]) //打印虚函数表 table -> 数组名 -> _vfptr*(类型)
{
  for (int i = 0; table[i]; ++i) //虚函数表是以nullptr为结束标记的
  {
    printf("[%d]:%p->", i, table[i]);
    _vfptr f = table[i]; //拿到指针
    f(); //调用类中的虚函数
  }
  cout << endl;
}

4.7 其他问题

  1. 虚函数表是什么阶段生成的? 编译期间已经完成,因为编译就有函数的地址了。
  2. 对象中的虚表指针什么时候初始化?构造函数的初始化列表阶段。
  3. 虚表存在虚拟内存地址的什么区域?visual studio 2019编译器是常量区(代码段)。

4.8 多继承中的虚函数表

class Base1 {
public:
  virtual void func1() { cout << "Base1::func1" << endl; }
  virtual void func2() { cout << "Base1::func2" << endl; }
private:
  int b1;
};
class Base2 {
public:
  virtual void func1() { cout << "Base2::func1" << endl; }
  virtual void func2() { cout << "Base2::func2" << endl; }
private:
  int b2;
};
class Derive : public Base1, public Base2 {
public:
  virtual void func1() { cout << "Derive::func1" << endl; }
  virtual void func3() { cout << "Derive::func3" << endl; }
private:
  int d1;
};
typedef void(*_vfptr)(); //void(*_vfptr)() --> 函数指针 --> 起别名为_vfptr
void print_virtual_function(_vfptr table[]) //打印虚函数表 table -> 数组名 -> _vfptr*(类型)
{
  for (int i = 0; table[i]; ++i) //虚函数表是以nullptr为结束标记的
  {
    printf("[%d]:%p->", i, table[i]);
    _vfptr f = table[i];
    f();
  }
  cout << endl;
}
int main()
{
  Derive d;
  print_virtual_function((*(_vfptr**)&d)); //打印Base1虚表
  //&d -> Derive* -> (char*)&d -> 强制转化为char* -> ((char*)&d + sizeof(Base1)) -> 通过sizeof(Base1)大小偏移量按照一个字节偏移到Base2虚表位置
  //(_vfptr**)((char*)&d + sizeof(Base1)) -> char*强制转换为_vfptr**(指针的指针) -> *(_vfptr**)((char*)&d + sizeof(Base1)) -> 解引用后_vfptr*
  print_virtual_function(*(_vfptr**)((char*)&d + sizeof(Base1))); //打印Base2虚表
  return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kEajyGbk-1681121198968)(https://jinyinhan.oss-cn-beijing.aliyuncs.com/QQ截图20230405204128.png)]


Derive子类有两个父类所以有两个虚函数表,这样就很清晰的看出来Derive子类中的Func3()函数是放在Base1这个父类的虚表中的。另外再次观察Base1和Base2中的func1()都玩成了重写但是这里的Base1和Base2父类中的func1()虚函数地址并不一样这是什么原因呢?


[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-trd514U5-1681121198968)(https://jinyinhan.oss-cn-beijing.aliyuncs.com/QQ截图20230405210905.png)]


这里根据汇编可以看的出来调用p1->func1()和p2->func1()最终都是调用Derive::func1()。需要注意的是p2->func1()函数中第一次jmp跳转到sub指令,这里的ecx寄存器就是存的this指针,通过减少8个字节找到Base1类的。

4.9 多继承例题

class Base1 { public:  int _b1; };
class Base2 { public:  int _b2; };
class Derive : public Base1, public Base2 { public: int _d; };
int main() {
  Derive d;
  Base1* p1 = &d;
  Base2* p2 = &d;
  Derive* p3 = &d;
  return 0;
}
//A:p1 == p2 == p3 B:p1 < p2 < p3 C:p1 == p3 != p2 D:p1 != p2 != p3

根据上面4.8的多继承虚函数表的说明可以看得出选择C,Base1和Base2是Derive父类,这里Base1先声明,先继承Base1。

4.10 动态绑定与静态绑定

  1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
  2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。

























相关文章
|
1月前
|
编译器 C++
C++入门12——详解多态1
C++入门12——详解多态1
40 2
C++入门12——详解多态1
|
6月前
|
C++
C++中的封装、继承与多态:深入理解与应用
C++中的封装、继承与多态:深入理解与应用
151 1
|
1月前
|
C++
C++入门13——详解多态2
C++入门13——详解多态2
81 1
|
3月前
|
存储 编译器 C++
|
4月前
|
存储 编译器 C++
【C++】深度解剖多态(下)
【C++】深度解剖多态(下)
54 1
【C++】深度解剖多态(下)
|
4月前
|
存储 编译器 C++
|
4月前
|
机器学习/深度学习 算法 C++
C++多态崩溃问题之为什么在计算梯度下降时需要除以批次大小(batch size)
C++多态崩溃问题之为什么在计算梯度下降时需要除以批次大小(batch size)
|
4月前
|
Java 编译器 C++
【C++】深度解剖多态(上)
【C++】深度解剖多态(上)
55 2
|
4月前
|
程序员 C++
【C++】揭开C++多态的神秘面纱
【C++】揭开C++多态的神秘面纱
|
4月前
|
机器学习/深度学习 PyTorch 算法框架/工具
C++多态崩溃问题之在PyTorch中,如何定义一个简单的线性回归模型
C++多态崩溃问题之在PyTorch中,如何定义一个简单的线性回归模型