C++-带你走进多态(2)

简介: C++-带你走进多态(2)

4.多态的原理


4.1虚函数表

大家认为bb的大小是多少?如果按照内存对齐的概念,大小是否是8呢?正确的答案是12,因为有一个虚函数,所以这个类里面存在一个指针指向虚函数表,这个虚函数表就是一个函数指针数组,里面存放的是虚函数的指针。

class Base
{
public:
  virtual void Func2()
  {
  cout << "Base::Func2()" << endl;
  }
private:
  int _b = 1;
  char _ch = 'a';
};
int main()
{
  Base bb;
  cout << sizeof(bb) << endl;
  return 0;
}

f40fcf83c5364208903157098a13ad4d.png

这个虚函数表只存放虚函数的指针。

1709ff1b8dc34cd48497ea034fe92bbb.png

1b2ba2018ba4478e9e7b257143406f63.png

一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表,。那么派生类中这个表放了些什么呢?我们接着往下分析

class Base
{
public:
  void Func1()
  {
  cout << "Base::Func1()" << endl;
  }
  virtual void Func2()
  {
  cout << "Base::Func2()" << endl;
  }
  virtual void Func3()
  {
  cout << "Base::Func3()" << endl;
  }
private:
  int _b = 1;
  char _ch = 'a';
};
class Derive : public Base
{
public:
  virtual void Func3()
  {
  cout << "Derive::Func1()" << endl;
  }
private:
  int _d = 2;
};
int main()
{
  Base bb;
  cout << sizeof(bb) << endl;
  Derive dd;
  cout << sizeof(dd) << endl;
  return 0;
}

当基类Base的派生类Derive重写了Func3之后,虚表的地址就不一样了。,Func3的地址也不同了,因为已经覆盖掉了。


6f644df99997453d8b980a7654fdb3ff.png

73da338c8bd34e3b85090e7450999f82.png

完成多态的条件之后,这个f函数就就是多态调用,就会去找相对应的虚表。

86c525f187c0439f8ea0700431bc11dc.png

通过观察和测试,我们发现了以下几点问题:

1. 派生类对象dd中也有一个虚表指针,dd对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分是自己的成员。


2. 基类bb对象和派生类dd对象虚表是不一样的,这里我们发现Func3完成了重写,所以d的虚表中存的是重写的Derive::Func3,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。


3. 另外Func2继承下来后是虚函数,所以放进了虚表,Func1也继承下来了,但是不是虚函


数,所以不会放进虚表。

4. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。


5. 总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。


6. 这里还有一个童鞋们很容易混淆的问题:虚函数存在哪的?虚表存在哪的? 答:虚函数存在虚表,虚表存在对象中。注意上面的回答的错的。但是很多童鞋都是这样深以为然的。注意虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。


4.2多态的原理

上面分析了这个半天了那么多态的原理到底是什么?还记得这里Func函数传Person调用的Person::BuyTicket,传Student调用的是Student::BuyTicket 。

66d5e500dc5f49358e2fee858d27756f.png

p是指向mike对象时,p->BuyTicket在mike的虚表中找到虚函数是Person::BuyTicket

p是指向johnson对象时,p->BuyTicket在johson的虚表中找到虚函数是Student::BuyTicket


这样就实现出了不同对象去完成同一行为时,展现出不同的形态。

满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象的中取找的。不满足多态的函数调用时编译时确认好的。


4.3 动态绑定与静态绑定

1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载


2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。


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


5.1 单继承中的虚函数表

观察下图中的监视窗口中我们发现看不见func3和func4。这里是编译器的监视窗口故意隐藏了这两个函数,也可以认为是他的一个小bug。那么我们如何查看d的虚表呢?下面我们使用代码打印出虚表中的函数。

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

26ed3f78748944f8af12829a66f1b5fd.png

思路:取出b、d对象的头4bytes,就是虚表的指针,前面我们说了虚函数表本质是一个存虚函数

指针的指针数组,这个数组最后面放了一个nullptr

1.先取b的地址,强转成一个int*的指针

2.再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针

3.再强转成VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。

4.虚表指针传递给PrintVTable进行打印虚表

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;
}

然后我们就可以发现func1被重写之后,将新的地址覆盖了旧的地址存放在Derve的虚表里面,这就是多态的原理,可以使得多态调用能够完成,

b80593fbc9c4409f86661bf8ae12a8f6.png

5.2 多继承中的虚函数表

观察下图可以看出:多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中

6400f69db4fe4dc0be21f76c1e6b527f.png


今天的分享到这里就结束了,感谢大家的阅读!

相关文章
|
28天前
|
存储 编译器 数据安全/隐私保护
【C++】多态
多态是面向对象编程中的重要特性,允许通过基类引用调用派生类的具体方法,实现代码的灵活性和扩展性。其核心机制包括虚函数、动态绑定及继承。通过声明虚函数并让派生类重写这些函数,可以在运行时决定具体调用哪个版本的方法。此外,多态还涉及虚函数表(vtable)的使用,其中存储了虚函数的指针,确保调用正确的实现。为了防止资源泄露,基类的析构函数应声明为虚函数。多态的底层实现涉及对象内部的虚函数表指针,指向特定于类的虚函数表,支持动态方法解析。
32 1
|
2月前
|
编译器 C++
C++入门12——详解多态1
C++入门12——详解多态1
47 2
C++入门12——详解多态1
|
7月前
|
C++
C++中的封装、继承与多态:深入理解与应用
C++中的封装、继承与多态:深入理解与应用
171 1
|
2月前
|
C++
C++入门13——详解多态2
C++入门13——详解多态2
88 1
|
4月前
|
存储 编译器 C++
|
5月前
|
存储 编译器 C++
【C++】深度解剖多态(下)
【C++】深度解剖多态(下)
57 1
【C++】深度解剖多态(下)
|
5月前
|
存储 编译器 C++
|
5月前
|
机器学习/深度学习 算法 C++
C++多态崩溃问题之为什么在计算梯度下降时需要除以批次大小(batch size)
C++多态崩溃问题之为什么在计算梯度下降时需要除以批次大小(batch size)
|
5月前
|
Java 编译器 C++
【C++】深度解剖多态(上)
【C++】深度解剖多态(上)
58 2
|
5月前
|
程序员 C++
【C++】揭开C++多态的神秘面纱
【C++】揭开C++多态的神秘面纱