关于C++虚函数与静态、动态绑定的问题

覆盖:如果派生类中的方法,和基类继承来的某个方法,返回值、函数名、参数列表都相同,而且基类的方法是virtual虚函数,那么派生类的这个方法,自动处理成虚函数,它们之间成为覆盖关系;也就是说派生类会在自己虚函数表中将从基类继承来的虚函数进行替换,替换成派生类自己的。

静态绑定:编译时期的多态,通过函数的重载以及模板来实现,也就是说调用函数的地址在编译时期我们就可以确定,在汇编代码层次,呈现的就是 call 函数名;

动态绑定:运行时期的多态,通过派生类重写基类的虚函数来实现。在汇编代码层次,呈现的就是 call 寄存器,寄存器的值只有运行起来我们才可以确定。

不存在虚函数

#include <iostream>
#include <typeinfo>
class Base
{
public:
  Base(int data = 10): ma(data) {}
  ~Base() {};
 
  void show() {
    std::cout << "Base::show()" << std::endl;
  }
 
  void show(int data) {
    std::cout << "Base::show()" << data << std::endl;
  }
 
protected:
  int ma;
 
};
 
class Derive :public Base 
{
public:
  Derive(int data) :Base(data), mb(data) {}
  ~Derive() {}
  void show() {
    std::cout << "Derive::show()" << std::endl;
  }
 
private:
  int mb;
};
 
int main() {
 
  Derive d(50);
  Base *pb = &d;
  pb->show();//静态(编译时期)绑定(函数调用) Base::show (06F12E4h)   
  pb->show(10);//Base::show (06F12BCh)
 
  std::cout << "Base size:" << sizeof(Base) << std::endl;//4
  std::cout << "Derive size:" << sizeof(Derive) << std::endl;//8
 
  std::cout << typeid(pb).name() << std::endl;//class Base *
  std::cout << typeid(*pb).name() << std::endl;//class Base 
 
  return 0;
 
 }

打断点,F5进入调试,点击反汇编

可以看到调用的都是基类的show(),在编译阶段已经生成指令调用Base下的show;

可以看到结果:
因为pb是Base类型的指针,所以调用的都是Base类的成员方法;
基类Base只有一个数据成员ma,所以大小只有4字节;
派生类Derive继承了ma,其次还有自己的mb,所以有8字节;
pb的类型是一个class Base *;
*pb的类型是一个class Base。
为了更好地理解上述过程,我们简单画图如下:

在这里插入图片描述

为什么Base *类型的指针,Derive类型的对象,调用方法的时候是Base而不是Derive呢?
原因如上图:
Derive类继承了Base类,导致了派生类的大小要比基类大,而pb的类型是基类的指针,所以通过pb调用方法时只能访问到Derive中从Base继承而来的方法,访问不到自己重写的方法(指针的类型限制了指针解引用的能力)

基类定义虚函数

#include <iostream>
#include <typeinfo>
class Base
{
public:
  Base(int data = 10): ma(data) {}
  ~Base() {};
 
  //虚函数
  virtual void show() {
    std::cout << "Base::show()" << std::endl;
  }
 
  void show(int data) {
    std::cout << "Base::show()" << data << std::endl;
  }
 
protected:
  int ma;
 
};
 
class Derive :public Base 
{
public:
  Derive(int data) :Base(data), mb(data) {}
  ~Derive() {}
  void show() {
    std::cout << "Derive::show()" << std::endl;
  }
 
private:
  int mb;
};
 
int main() {
 
  Derive d(50);
  Base *pb = &d;
 
  /*
  pb->show();
  pb 指针是base类型,如果发现Base中的show是虚函数,就进行动态绑定
mov         ecx,dword ptr [pb]  
00292B01 8B 45 D4             mov         eax,dword ptr [pb]   //将pb指向的内存前4个字节放入ecx寄存器,pb指向derive对象,前四个字节即vfptr,将虚函数表地址加载到eax
00292B04 8B 10                mov         edx,dword ptr [eax]  //将eax 的前四个字节 即Derive::show 加载到edx中
00292B06 8B F4                mov         esi,esp
00292B08 8B 4D D4             mov         ecx,dword ptr [pb]
00292B0B 8B 02                mov         eax,dword ptr [edx]
00292B0D FF D0                call        eax   //虚函数的地址
00292B0F 3B F4                cmp         esi,esp
00292B11 E8 9C E7 FF FF       call        __RTC_CheckEsp (02912B2h)
我们可以看到这一次,汇编码call的就不是确切的函数地址了,而是寄存器eax;
那么就很好理解了:
eax寄存器里存放的是什么内容,编译阶段根本无从知晓,只能在运行的时候确定;
故,动态绑定。
  pb->show(10);  如果发现show是普通函数,就进行静态绑定 call Base::show
  
  */
  pb->show();//
  pb->show(10);//
 
  std::cout << "Base size:" << sizeof(Base) << std::endl;//8
  std::cout << "Derive size:" << sizeof(Derive) << std::endl;//12
 
  std::cout << typeid(pb).name() << std::endl;//class Base *
  /*
  pb的类型:Base类型,查看Base中有没有虚函数
  (1)Base中没有虚函数*pb识别的就是编译时期的类型 *pb 就是Base类型
  (2) Base中有虚函数,*pb识别的就是运行时期的类型 RTTI类型:Derive
  */
  std::cout << typeid(*pb).name() << std::endl;//class Derive 
 
  return 0;
 
 }

在我们添加了virtual关键字后,对应的函数就变成了虚函数;
那么,一个类添加了虚函数,对这个类有什么影响呢?

  • 首先,如果类里面定义了虚函数,那么编译阶段,编译器给这个类类型产生一个唯一的vftable虚函数表,虚函数表中主要存储的内容是:RTTI(Run-time Type Information)指针和虚函数的地址,当程序运行时,每一张虚函数表都会加载到内存的.rodata区;
  • 一个类里面定义了虚函数,那么这个类定义的对象,在运行时,内存中会多存储一个vfptr虚函数指针,指向了对应类型的虚函数表vftable;
  • 一个类型定义的n个对象,他们的vfptr指向的都是同一张虚函数表;
  • 一个类里面虚函数的个数,不影响对象内存的大小(vfptr),影响的是虚函数表的大小。
  • 如果派生类中的方法和从基类继承来的某个方法中返回值、函数名以及参数列表都相同,且基类的方法是virtual,那么派生类的这个方法,自动处理成虚函数

图示如下:(以Base为例)

在这里插入图片描述

虚函数表
1、RTTI,存放的是类型信息,也就是(Base或者Derive)
2、偏移地址:虚函数指针相对于对象内存空间的偏移,一般vfptr都在0偏移位置
3、下面的函数时虚函数入口地址

在Derive类中,由于重写了show(),因此在Derive的虚函数表中,是使用子类的show()方法代替了Base类的show()

VS的工具来查看虚函数表的有关信息

1 找到

2 在打开的窗口中切换到当前工程所在目录:

C:Program Files (x86)Microsoft Visual Studio2017Community>cd C:UsersAdminsource eposC++test

3 输入命令:cl XXX.cpp /d1reportSingleClassLayoutXX(第一个XXX表示源文件的名字,第二个代表你想查看的类类型,我这里就是Derive)

以看到class Derived的对象的内存布局,在派生类对象的开始包含了基类Base的对象,其中有一个虚表指针,指向的就是下面的Derived::$vftable@ (virtual function table),表中包含了Derived类中所有的虚函数

多重继承、多继承 的虚函数表 1 内存分布

假设有一个基类ClassA,一个继承了该基类的派生类ClassB,并且基类中有虚函数,派生类实现了基类的虚函数。
我们在代码中运用多态这个特性时,通常以两种方式起手:
(1) ClassA *a = new ClassB();
(2) ClassB b; ClassA *a = &b;
以上两种方式都是用基类指针去指向一个派生类实例,区别在于第1个用了new关键字而分配在堆上,第2个分配在栈上

这里写图片描述

请看上图,不同两种方式起手仅仅影响了派生类对象实例存在的位置。
以左图为例,ClassA *a是一个栈上的指针。
该指针指向一个在堆上实例化的子类对象。基类如果存在虚函数,那么在子类对象中,除了成员函数与成员变量外,编译器会自动生成一个指向**该类的虚函数表(这里是类ClassB)**的指针,叫作虚函数表指针。通过虚函数表指针,父类指针即可调用该虚函数表中所有的虚函数。

2 类的虚函数表与类实例的虚函数指针

首先不考虑继承的情况。如果一个类中有虚函数,那么该类就有一个虚函数表。
这个虚函数表是属于类的,所有该类的实例化对象中都会有一个虚函数表指针去指向该类的虚函数表。
从第一部分的图中我们也能看到,一个类的实例要么在堆上,要么在栈上。也就是说一个类可以有很多很多个实例。但是!一个类只能有一个虚函数表。在编译时,一个类的虚函数表就确定了,这也是为什么它放在了只读数据段中。

这里写图片描述

3 多态代码及多重继承情况

在第二部分中,我们讨论了在没有继承的情况下,虚函数表的逻辑结构。
那么在有继承情况下,只要基类有虚函数,子类不论实现或没实现,都有虚函数表。

#include <iostream>
 
using namespace std;
 
class ClassA
{
public:
  ClassA() { cout << "ClassA::ClassA()" << endl; }
  virtual ~ClassA() { cout << "ClassA::~ClassA()" << endl; }
 
  void func1() { cout << "ClassA::func1()" << endl; }
  void func2() { cout << "ClassA::func2()" << endl; }
 
  virtual void vfunc1() { cout << "ClassA::vfunc1()" << endl; }
  virtual void vfunc2() { cout << "ClassA::vfunc2()" << endl; }
private:
  int aData;
};
 
class ClassB : public ClassA
{
public:
  ClassB() { cout << "ClassB::ClassB()" << endl; }
  virtual ~ClassB() { cout << "ClassB::~ClassB()" << endl; }
 
  void func1() { cout << "ClassB::func1()" << endl; }
  virtual void vfunc1() { cout << "ClassB::vfunc1()" << endl; }
private:
  int bData;
};
 
class ClassC : public ClassB
{
public:
  ClassC() { cout << "ClassC::ClassC()" << endl; }
  virtual ~ClassC() { cout << "ClassC::~ClassC()" << endl; }
 
  void func2() { cout << "ClassC::func2()" << endl; }
  virtual void vfunc2() { cout << "ClassC::vfunc2()" << endl; }
private:
  int cData;
};
 
 
int main()
{
  ClassC c;
 
  return 0;
}

请看上面代码
(1) ClassA是基类, 有普通函数: func1() func2() 。虚函数: vfunc1() vfunc2() ~ClassA()
(2) ClassB继承ClassA, 有普通函数: func1()。虚函数: vfunc1() ~ClassB()
(3) ClassC继承ClassB, 有普通函数: func2()。虚函数: vfunc2() ~ClassB()
基类的虚函数表和子类的虚函数表不是同一个表。下图是基类实例与多态情形下,数据逻辑结构。注意,虚函数表是在编译时确定的,属于类而不属于某个具体的实例。虚函数在代码段,仅有一份
ClassB继承与ClassA,其虚函数表是在ClassA虚函数表的基础上有所改动的,变化的仅仅是在子类中重写的虚函数。如果子类没有重写任何父类虚函数,那么子类的虚函数表和父类的虚函数表在内容上是一致的

ClassA *a = new ClassB();
a->func1();                    // "ClassA::func1()"   隐藏了ClassB的func1()
a->func2();                    // "ClassA::func2()"
a->vfunc1();                   // "ClassB::vfunc1()"  重写了ClassA的vfunc1()
a->vfunc2();                   // "ClassA::vfunc2()"

这个结果不难想象,看上图,ClassA类型的指针a能操作的范围只能是黑框中的范围,之所以实现了多态完全是因为子类的虚函数表指针与虚函数表的内容与基类不同
这个结果已经说明了C++的隐藏、重写(覆盖)特性。

同理,也就不难推导出ClassC的逻辑结构图了
类的继承情况是: ClassC继承ClassB,ClassB继承ClassA
这是一个多次单继承的情况。(多重继承)

这里写图片描述

4、多继承下的虚函数表 (同时继承多个基类)

多继承是指一个类同时继承了多个基类,假设这些基类都有虚函数,也就是说每个基类都有虚函数表,那么该子类的逻辑结果和虚函数表是什么样子呢?

#include <iostream>
 
using namespace std;
 
class ClassA1
{
public:
  ClassA1() { cout << "ClassA1::ClassA1()" << endl; }
  virtual ~ClassA1() { cout << "ClassA1::~ClassA1()" << endl; }
 
  void func1() { cout << "ClassA1::func1()" << endl; }
 
  virtual void vfunc1() { cout << "ClassA1::vfunc1()" << endl; }
  virtual void vfunc2() { cout << "ClassA1::vfunc2()" << endl; }
private:
  int a1Data;
};
 
class ClassA2
{
public:
  ClassA2() { cout << "ClassA2::ClassA2()" << endl; }
  virtual ~ClassA2() { cout << "ClassA2::~ClassA2()" << endl; }
 
  void func1() { cout << "ClassA2::func1()" << endl; }
 
  virtual void vfunc1() { cout << "ClassA2::vfunc1()" << endl; }
  virtual void vfunc2() { cout << "ClassA2::vfunc2()" << endl; }
  virtual void vfunc4() { cout << "ClassA2::vfunc4()" << endl; }
private:
  int a2Data;
};
 
class ClassC : public ClassA1, public ClassA2
{
public:
  ClassC() { cout << "ClassC::ClassC()" << endl; }
  virtual ~ClassC() { cout << "ClassC::~ClassC()" << endl; }
 
  void func1() { cout << "ClassC::func1()" << endl; }
 
  virtual void vfunc1() { cout << "ClassC::vfunc1()" << endl; }
  virtual void vfunc2() { cout << "ClassC::vfunc2()" << endl; }
  virtual void vfunc3() { cout << "ClassC::vfunc3()" << endl; }
};
 
 
int main()
{
  ClassC c;
 
  return 0;
}

ClassA1是第一个基类,拥有普通函数func1(),虚函数vfunc1() vfunc2()。
ClassA2是第二个基类,拥有普通函数func1(),虚函数vfunc1() vfunc2(),vfunc4()。
ClassC依次继承ClassA1、ClassA2。普通函数func1(),虚函数vfunc1() vfunc2() vfunc3()。

在多继承情况下,有多少个基类就有多少个虚函数表指针,前提是基类要有虚函数才算上这个基类。
如图,虚函数表指针01指向的虚函数表是以ClassA1的虚函数表为基础的,子类的ClassC::vfunc1(),和vfunc2()的函数指针覆盖了虚函数表01中的虚函数指针01的位置、02位置。当子类有多出来的虚函数时,添加在第一个虚函数表中。注意:
1.子类虚函数会覆盖每一个父类的每一个同名虚函数。
2.父类中没有的虚函数而子类有,填入第一个虚函数表中,且用父类指针是不能调用。
3.父类中有的虚函数而子类没有,则不覆盖。仅子类和该父类指针能调用

虚基类和多重继承

什么是多重继承

多重继承,很好理解,一个派生类如果只继承一个基类,称作单继承;
一个派生类如果继承了多个基类,称作多继承。
如图所示:

在这里插入图片描述

多重继承的优点
这个很好理解:
多重继承可以做更多的代码复用!
派生类通过多重继承,可以得到多个基类的数据和方法,更大程度的实现了代码复用。

关于菱形继承的问题
凡事有利也有弊,对于多继承而言,也有自己的缺点。
我们先通过了解菱形继承来探究多重继承的缺点:
菱形继承是多继承的一种情况,继承方式如图所示:

在这里插入图片描述

从图中我们可以看到:
类B类C类A单继承而来;
类D类B类C多继承而来。
那么这样继承会产生什么问题呢?
我们来看代码:

#include <iostream>
 
using namespace std;
class A
{
public:
  A(int data) :ma(data) { cout << "A()" << endl; }
  ~A() { cout << "~A()" << endl; }
protected:
  int ma;
};
class B :public A
{
public:
  B(int data) :A(data), mb(data) { cout << "B()" << endl; }
  ~B() { cout << "~B()" << endl; }
protected:
  int mb;
};
class C :public A
{
public:
  C(int data) :A(data), mc(data) { cout << "C()" << endl; }
  ~C() { cout << "~C()" << endl; }
protected:
  int mc;
};
class D :public B, public C
{
public:
  D(int data) : B(data), C(data), md(data) { cout << "D()" << endl; }
  ~D() { cout << "~D()" << endl; }
protected:
  int md;
};
int main()
{
  D d(10);
 
  return 0;
}

通过运行结果,我们发现了问题:
对于基类A而言,构造了两次,析构了两次!
并且,通过分析各个派生类的内存布局我们可以看到:

在这里插入图片描述

对于派生类D来说,间接继承的基类A中的数据成员ma重复了!
这对资源来说是一种浪费与消耗。
(如果多继承的数量增加,那么派生类中重复的数据也会增加!)

查看D类的内存布局:

其他多重继承的情况

除了菱形继承外,还有其他多重继承的情况,也会出现相同的问题

在这里插入图片描述

比如说图中呈现的:半圆形继承。

如何解决多重继承的问题

通过分析我们知道了,多重继承的主要问题是,通过多重继承,有可能得到重复的基类数据,并且可能重复的构造和析构同一个基类对象。
那么如何能够避免重复现象的产生呢?
答案就是:=》虚基类。

什么是虚基类
要理解虚基类,我们首先需要认识virtual关键字的使用场景:

修饰成员方法时:产生虚函数;
修饰继承方式时:产生虚基类。
对于被虚继承的类,称作虚基类。
比如说:

class A
{
	XXXXXX;
};
class B : virtual public A
{
	XXXXXX;
};

对于这个示例而言,B虚继承了A,所以把A称作虚基类。

虚基类如何解决问题

那么虚基类如何解决上述多重继承产生的重复问题呢?
我们来看代码:

#include <iostream>
 
using namespace std;
class A
{
public:
  A(int data) :ma(data) { cout << "A()" << endl; }
  ~A() { cout << "~A()" << endl; }
protected:
  int ma;
};
class B :virtual public A
{
public:
  B(int data) :A(data), mb(data) { cout << "B()" << endl; }
  ~B() { cout << "~B()" << endl; }
protected:
  int mb;
};
class C :virtual public A
{
public:
  C(int data) :A(data), mc(data) { cout << "C()" << endl; }
  ~C() { cout << "~C()" << endl; }
protected:
  int mc;
};
class D :public B, public C
{
public:
  D(int data) : B(data), C(data), md(data) { cout << "D()" << endl; }
  ~D() { cout << "~D()" << endl; }
protected:
  int md;
};
 

提示说:"A::A" : 没有合适的默认构造函数可用
为什么会这样呢?
我们可以这么理解:

刚开始BC单继承A的时候,实例化对象时,会首先调用基类的构造函数,也就是A的构造函数,到了D,由于多继承了BC,所以在实例化D的对象时,会首先调用BC的构造函数,然后调用自己(D)的。

但是这样会出现A重复构造的问题,所以,采用虚继承,把有关重复的基类A改为虚基类,这样的话,对于A构造的任务就落到了最终派生类D的头上,但是我们的代码中,对于D的构造函数:D(int data) : B(data), C(data), md(data) { cout << "D()" << endl; }并没有对A进行构造。
所以会报错。
那么我们就给D的构造函数,调用A的构造函数:
D(int data) :A(data), B(data), C(data), md(data) { cout << "D()" << endl; }
这一次再运行

我们会发现,问题解决了。

查看虚基类的内存布局

我们可以看到当前B的内存空间:

当前B的内存空间里,前四个字节是vbptr(这个就代表里虚基类指针:virtual base ptr);
vfptr(虚函数指针)指向了vftable(虚函数表)一样,
vbptr(虚基类指针)指向了vbtable(虚基类表)。

vbtable(虚基类表)的布局也如图所示,
首先是偏移量0:表示了虚基类指针再内存布局中的偏移量;
接着是偏移量8:表示从虚基类中继承而来的数据成员在内存中的偏移量。

对比普通继承下的内存布局

我们可以对比没有虚继承下的B的内存布局来理解:

我们把他们放在一起对比可以看到:

继承虚基类的类(BC)会把自己从虚基类继承而来的数据ma放在自己内存的最末尾(偏移量最大),并在原来ma的位置填充一个vbptr(虚基类指针),这个指针指向了vbtable(虚基类表)。
理解了B,我们可以看看更为复杂的D

可以看到,将ma移动到了末尾处,并在含有ma的地方,都用vbptr进行填充。
这样一来,就只有一个ma了!解决了多重继承的重复问题。

到此这篇关于关于C++虚函数与静态、动态绑定的问题的文章就介绍到这了,更多相关C++虚函数与静态、动态绑定内容请搜索云海天教程以前的文章或继续浏览下面的相关文章希望大家以后多多支持云海天教程!