【C++】深度解剖多态(下)

简介: 【C++】深度解剖多态(下)

【C++】深度解剖多态(上)         https://developer.aliyun.com/article/1565588



🌙多态的原理

💫虚函数表

问题探究:

下面代码运行结果:



我们定义一个Base类,里面有虚函数,还有一个变量int,按照我们之前学习到了,这里Base类的大小应该是4个字节,图中确是8个字节,为什么会发生这种现象呢?


问题分析:

除了_b成员,还多一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数 的地址要被放到虚函数表中,虚函数表也简称虚表。其实应该叫__vftptr(多个t代表table)



总结归纳:

我们多添加几个虚函数,看看这个表里面的内容是怎么样的,可以发现虚函数会放到虚函数表中,普通函数不会,并且表里面的内容是一个数组,是函数指针数组

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;
  }
private:
  int _d = 2;
};
int main()
{
  Base b;
  Derive d;
  return 0;
}



💫多态的原理

1.虚表指针里的内容

举例说明:

class Person {
public:
  virtual void BuyTicket() { cout << "买票-全价" << endl; }
  virtual void fun() {}
private:
  int a;
};
class Student : public Person {
public:
  virtual void BuyTicket() { cout << "买票-半价" << endl; }
private:
  int b;
};
void Func(Person* p)
{
  p->BuyTicket();
}
int main()
{
  Person p;
  Student s;
  Func(&p);
  Func(&s);
  return 0;
}


问题分析:

从图中我们可以看到,在内存1里面输入&p可以找到p的地址, 因为p的第一个内容就是__vfptr,因此p的地址也是__vfptr的地址,那么我们通过__vfptr的地址就可以找到虚函数表里面的内容,因此我们在内存2里面输入__vfptr的地址,我们便找到了两个虚函数的地址。  



问题拓展:

注意这里Student类和Teacher的类表里的第二个虚函数地址是一样的,因为B类没有重写第二个虚函数,因此继承下来了。为什么第一个虚函数不一样呢?因为子类重写后覆盖掉了(这也是为什么重写

被称作覆盖的由来)



2.引用和指针如何实现多态

可以分析,为什么多态可以实现指向父类调用父类函数 ,指向子类调用子类函数?传递父类,通过vftptr找到虚函数表的地址,再去找到虚函数的地址,有了虚函数的地址,便可以去call这个虚函数。父类再去接受这个数据了,一样会有vftptr(是子类的vftptr),再去找到虚函数的地址,有了虚函数的地址,便可以去call这个虚函数这样就完成了多态。



传递子类,首先会进行切割


💫虚函数表存放位置

举例说明:

我们通过代码来打印各个区地地址,可以判断虚函数表存放位置

class Base {
public:
  virtual void func1() { cout << "Base::func1" << endl; }
  virtual void func2() { cout << "Base::func2" << endl; }
private:
  int a;
};
 
void func()
{}
 
int main()
{
  Base b1;
  Base b2;
  static int a = 0;
  int b = 0;
  int* p1 = new int;
  const char* p2 = "hello world";
  printf("静态区:%p\n", &a);
  printf("栈:%p\n", &b);
  printf("堆:%p\n", p1);
  printf("代码段:%p\n", p2);
  printf("虚表:%p\n", *((int*)&b1));
  printf("虚函数地址:%p\n", &Base::func1);
  printf("普通函数:%p\n", func);
}


问题分析:

注意打印虚表这里,vs x86环境下的虚表的地址是存放在类对象的头4个字节上。因此我们可以通过强转来取得这头四个字节b1是类对象,取地址取出类对象的地址,强转为(int*)代表我们只取4个字节,再解引用,就可以取到第一个元素的地址,也就是虚函数表指针的地址



从图中可以发现代码段和虚表地址非常接近,存在代码段的常量区。虚函数和普通函数地址非常接近,存在代码段。


🌙单继承和多继承关系的虚函数表

💫单继承中的虚函数表

举例说明:

class Base {
public:
  virtual void func1() { cout << "Base::func1" << endl; }
  virtual void func2() { cout << "Base::func2" << endl; }
private:
  int a;
};
class Derive :public Base {
public:
  virtual void func1() { cout << "Derive::func1" << endl; }
  virtual void func3() { cout << "Derive::func3" << endl; }
  virtual void func4() { cout << "Derive::func4" << endl; }
private:
  int b;
};
class X : Derive
{
public:
  virtual void fun3() { cout << "X::func3" << endl; }
};
 
int main()
{
  Base b;
  Derive d;
  X x;
  return 0;
}


代码分析:

通过输入__vfptr的地址,我们成功找到了里面虚函数的地址。并且我们还发现似乎下面那两个地址跟上面两个非常接近,我们可以合理的设想,下面两个地址也是虚函数指针。



问题分析:

下面我们typedef了虚函数表指针  typedef void(*VFTPTR)(); 可以通过这个函数指针数组来打印里面的虚函数,这个打印函数终止条件就是 !=0 ,传递的参数内容跟前面我们分析的差不多,只是躲了一个强转,PrintVFPtr((VFTPTR*)*(int*)&b)  ; 因为后面的  *(int*)&b 虽然内容是地址,但是表现形式是一个整形,需要强为  (VFTPTR*) 。



问题拓展:

在*((int*)&d) 就会取到vTableAddress指向的地址,就得到虚函数的地址了。

class Base {
public:
  virtual void func1() { cout << "Base::func1" << endl; }
  virtual void func2() { cout << "Base::func2" << endl; }
private:
  int a;
};
class Derive :public Base {
public:
  virtual void func1() { cout << "Derive::func1" << endl; }
  virtual void func3() { cout << "Derive::func3" << endl; }
  virtual void func4() { cout << "Derive::func4" << endl; }
private:
  int b;
};
class X : Derive
{
public:
  virtual void func3() { cout << "X::func3" << endl; }
};
 
typedef void(*VFTPTR)();
 
void PrintVFPtr(VFTPTR a[])
{
  for (size_t i = 0; a[i] != 0; i++)
  {
    printf("a[%d]:%p->", i, a[i]);
    VFTPTR p = a[i];
    p();
  }
  cout << endl;
}
 
int main()
{
  Base b;
  Derive d;
  X x;
  PrintVFPtr((VFTPTR*)*(int*)&b);
  PrintVFPtr((VFTPTR*)*(int*)&d);
  PrintVFPtr((VFTPTR*)*(int*)&x);
  return 0;
}


运行:



分析:可以确认虚函数都会放到虚表里面


💫多继承中的虚函数表

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 PrintVTable(VFPTR vTable[])
{
  // 依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数
  cout << " 虚表地址>" << vTable << endl;
  for (int i = 0; vTable[i] != nullptr; ++i)
  {
    printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
    VFPTR f = vTable[i];
    f();
  }
  cout << endl;
}
int main()
{
  printf("%p\n", &Derive::func1);
 
  Derive d;
  //PrintVTable((VFPTR*)(*(int*)&d));
  PrintVTable((VFPTR*)(*(int*)&d));    
  PrintVTable((VFPTR*)(*(int*)((char*)&d+sizeof(Base1))));
}

PrintVTable((VFPTR*)(*(int*)&d));


因为对象中存虚表指针,虚表指针中存的是虚表(一个指针数组),则需要先解引用访问到这个对象的前四个字节内容(存的就是虚表指针),此时的虚表指针 *((int*)&d)是一个int类型,再把虚表指针类型强转成指针数组类型才能传参


PrintVTable((VFPTR*)(*(int*)((char*)&d+sizeof(Base1)))); 是找到Base2的虚表地址后再解引用找到虚表(直接加2个int字节也能找到base2,考虑Base1可能不单单是2个int大小,这里建议用sizeof(Base1) )



结论: Derive对象Base2虚表中func1时,是Base2指针ptr2去调用。但是这时ptr2发生切片指针偏移,需要修正。中途就需要修正存储this指针ecx的值



💫菱形继承和菱形虚拟继承

实际中我们不建议设计出菱形继承及菱形虚拟继承,一方面太复杂容易出问题,另一方面这样的 模型,访问基类成员有一定得性能损耗。所以菱形继承、菱形虚拟继承我们的虚表我们就不看 了,一般我们也不需要研究清楚,因为实际中很少用。


🌙继承和多态常见的面试问题

1.什么是多态?本篇已讲解了


2. 什么是重载、重写(覆盖)、重定义(隐藏)?这里在本篇已讲解了


3. 多态的实现原理?这里在本篇已讲解了


4. inline函数可以是虚函数吗?答:可以,不过编译器就忽略inline属性,这个函数就不再是 inline,因为虚函数要放到虚表中去。


5. 静态成员可以是虚函数吗?答:不能,因为静态成员函数没有this指针,使用类型::成员函数 的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。


6. 构造函数可以是虚函数吗?答:不能,因为对象中的虚函数表指针是在构造函数初始化列表 阶段才初始化的。


7. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?答:可以,并且最好把基类的析 构函数定义成虚函数。


8. 对象访问普通函数快还是虚函数更快?答:首先如果是普通对象,是一样快的。如果是指针 对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函 数表中去查找。


9. 虚函数表是在什么阶段生成的,存在哪的?答:虚函数表是在编译阶段就生成的,一般情况 下存在代码段(常量区)的。


10. C++菱形继承的问题?虚继承的原理?这里在本篇已讲解了


11. 什么是抽象类?抽象类的作用?抽象类强制重写了虚函数,另外抽 象类体现出了接口继承关系。


🌟结束语

      今天内容就到这里啦,时间过得很快,大家沉下心来好好学习,会有一定的收获的,大家多多坚持,嘻嘻,成功路上注定孤独,因为坚持的人不多。那请大家举起自己的小手给博主一键三连,有你们的支持是我最大的动力💞💞💞,回见。


目录
相关文章
|
2月前
|
存储 编译器 C++
|
2月前
|
存储 编译器 C++
C++多态实现的原理:深入探索与实战应用
【8月更文挑战第21天】在C++的浩瀚宇宙中,多态性(Polymorphism)无疑是一颗璀璨的星辰,它赋予了程序高度的灵活性和可扩展性。多态允许我们通过基类指针或引用来调用派生类的成员函数,而具体调用哪个函数则取决于指针或引用所指向的对象的实际类型。本文将深入探讨C++多态实现的原理,并结合工作学习中的实际案例,分享其技术干货。
54 0
|
3月前
|
机器学习/深度学习 算法 C++
C++多态崩溃问题之为什么在计算梯度下降时需要除以批次大小(batch size)
C++多态崩溃问题之为什么在计算梯度下降时需要除以批次大小(batch size)
|
3月前
|
Java 编译器 C++
【C++】深度解剖多态(上)
【C++】深度解剖多态(上)
44 2
|
3月前
|
存储 Java 程序员
【c++】继承深度解剖
【c++】继承深度解剖
29 1
|
3月前
|
机器学习/深度学习 PyTorch 算法框架/工具
C++多态崩溃问题之在PyTorch中,如何定义一个简单的线性回归模型
C++多态崩溃问题之在PyTorch中,如何定义一个简单的线性回归模型
|
3月前
|
存储 算法 编译器
【C++11】C++11深度解剖(下)
【C++11】C++11深度解剖(下)
30 0
|
3月前
|
存储 安全 程序员
【C++11】C++11深度解剖(上)
【C++11】C++11深度解剖(上)
29 0
|
3月前
|
编译器 程序员 C++
【C++高阶】掌握C++多态:探索代码的动态之美
【C++高阶】掌握C++多态:探索代码的动态之美
36 0
|
3月前
|
存储 编译器 C++
C++基础知识(七:多态)
多态是面向对象编程的四大基本原则之一,它让程序能够以统一的接口处理不同的对象类型,从而实现了接口与实现分离,提高了代码的灵活性和复用性。多态主要体现在两个层面:静态多态(编译时多态,如函数重载)和动态多态(运行时多态,主要通过虚函数实现)。