『 C++类与对象 』多态之单继承与多继承的虚函数表

简介: 『 C++类与对象 』多态之单继承与多继承的虚函数表



🫧 前言

多态是一种基于继承关系的语法,既然涉及到继承,而继承的方式有多种:

  • 单继承
  • 多继承
  • 棱形继承
  • 棱形虚拟继承
    不同的继承方式其虚表的形式也不同;
以下操作均为在CentOS7_x64机器上的操作

🫧 查看虚表

已知虚表为一个void (*)()的函数指针数组,除了以内存的方式查看虚表以外还可以使用函数调用的方式来查看虚表的真实情况;

其思路即为将该指针数组的指针打印并调用;

根据函数调用可以知道哪个指针是哪个函数;

typedef void(*VFPTR)();
void PrintVT( VFPTR vTable[] ,size_t n/*虚函数个数*/){
  cout<<"ptr: "<< vTable <<endl;
  for(size_t i = 0;i<n;++i){
    printf(" 第%u地址:0x%x,->",i,vTable[i]);
    VFPTR f=vTable[i];
    f();
  }
  cout<<endl;
}
//函数的参数为函数指针数组(虚表)的首地址;
//由于是自定义类型的前4/8个字节(在该平台下为8个字节)
//应使用对应的方式取到前8个字节;
//通过该首地址向后进行遍历;

🫧 单继承下的虚函数表

存在一个单继承关系:

class A{//基类
  public:
    virtual void Func1(){//虚函数
      cout<<"A::Func1()"<<endl;
    }
    virtual void Func2(){//虚函数
      cout<<"A::Func2()"<<endl;
    }
    int _a = 10;
};
class B:public A{//派生类
  public:
    virtual void Func1(){//虚函数且完成重写
      cout<<"B::Func1()"<<endl;
    }
    virtual void Func3(){//虚函数
      cout<<"B::Func3()"<<endl;
    }
    int _b = 20;
};
void test1(){
  //分别实例化出两个对象
  A aa;
  B bb;
}

使用GDB打印出实例化出的aabb的内容;

(gdb) display aa
1: aa = {_vptr.A = 0x400ad8 <vtable for A+16>, _a = 10}
(gdb) display bb
2: bb = {<A> = {_vptr.A = 0x400ab0 <vtable for B+16>, _a = 10}, _b = 20}

由于子类对象和父类对象种都存在一张虚表,所以对应的子类对象的虚函数存储于子类的虚表当中,父类对象的虚函数存储于父类的虚表当中;

其中该段所出现的结果中的_vptr.A = 0x400ad8_vptr.A = 0x400ab0即为虚表指针,该地址不是两个对象的地址,而是该对象地址中首地址所存储的内容;

可以使用&将两个对象的地址取出并使用x/x进行解析从而验证;

(gdb) p &aa 
$10 = (A *) 0x7fffffffe430  #aa对象的首地址
(gdb) x/x 0x7fffffffe430 
0x7fffffffe430: 0x00400ad8  #其首地址所存储的数据
(gdb) p &bb
$11 = (B *) 0x7fffffffe420  #bb对象的首地址
(gdb) x/x 0x7fffffffe420
0x7fffffffe420: 0x00400ab0  #其首地址所存储的数据

其中上面的首地址所存储的数据即为一个指针,这个指针即为虚表(虚函数表)指针,也就是虚函数表的首地址位置;

在该示例中基类和派生类中各有两个虚函数,其中派生类的Func1()虚函数重写了基类的Func1()虚函数,所以在基类和派生类的虚表中都存在该函数,且该函数的地址不同;

  • A类虚表
# A类虚表
(gdb)  p aa
$12 = {_vptr.A = 0x400ad8 <vtable for A+16>, _a = 10}
#----------------------------------
(gdb) x/x 0x400ad8  
0x400ad8 <_ZTV1A+16>: 0x00400924  #虚表首地址所存储的数据(A::Func1()函数的地址)
(gdb) x/x 0x00400924
0x400924 <A::Func1()>:  0xe5894855  #将地址解析后得到函数
#----------------------------------
(gdb) x/x 0x400ae0
0x400ae0 <_ZTV1A+24>: 0x00400950  #虚表中第二个位置所存储的数据(由于是64位机器偏移量为8,A::Func2()函数的地址)
(gdb) x/x 0x00400950
0x400950 <A::Func2()>:  0xe5894855  #将地址解析后得到函数
#----------------------------------
  • B类虚表
    B类虚表与之不同的是,B类作为派生类,而派生类的虚表可以看成是基类虚表的拷贝,且若发生重写的话虚表中的那个被重写的函数将会被重写的函数进行覆盖;
(gdb) p bb
# B类虚表
$14 = {<A> = {_vptr.A = 0x400ab0 <vtable for B+16>, _a = 10}, _b = 20}
#----------------------------------
(gdb) x/x 0x400ab0
0x400ab0 <_ZTV1B+16>: 0x0040097c  #虚表首地址所存储的数据(B::Func1()函数的地址[已被重写所以地址不同])
(gdb) x/x 0x0040097c
0x40097c <B::Func1()>:  0xe5894855  #将地址解析后得到函数
#----------------------------------
(gdb) x/x 0x400ab8
0x400ab8 <_ZTV1B+24>: 0x00400950  #虚表中第二个位置所存储的数据(由于是64位机器偏移量为8,A::Func2()函数的地址[派生类的虚函数表可以看成是基类函数表的拷贝])
(gdb) x/x 0x00400950
0x400950 <A::Func2()>:  0xe5894855  #将地址解析后得到函数
#----------------------------------
(gdb) x/x 0x400ac0
0x400ac0 <_ZTV1B+32>: 0x004009a8  #虚表中第三个位置所存储的数据(由于是64位机器偏移量为8,B::Func3()函数的地址[这里存放的是B类中自身的函数])
(gdb) x/x 0x004009a8
0x4009a8 <B::Func3()>:  0xe5894855  #将地址解析后得到函数

使用函数查看:

typedef void(*VFPTR)();
void PrintVT( VFPTR vTable[] ,size_t n/*虚函数个数*/){
  cout<<"ptr: "<< vTable <<endl;
  for(size_t i = 0;i<n;++i){
    printf(" 第%u地址:0x%x,->",i,vTable[i]);
    VFPTR f=vTable[i];
    f();
  }
  cout<<endl;
}
void test1(){
  A aa;
  B bb;
  PrintVT(*(VFPTR**)&aa,2);
  PrintVT(*(VFPTR**)&bb,3);
}

结果为 (重新编译过所以导致最终结果不同,但结论相同):

ptr: 0x400c60
 第0地址:0x400a94,->A::Func1()
 第1地址:0x400ac0,->A::Func2()
ptr: 0x400c38
 第0地址:0x400aec,->B::Func1()
 第1地址:0x400ac0,->A::Func2()
 第2地址:0x400b18,->B::Func3()

🫧 多继承下的虚函数表

多继承下的虚函数表较于单继承来说会更加的复杂;

复杂的原因在于多继承为多个基类继承给一个派生类,那么假设两个基类都有同名虚函数,且派生类重写了这个虚函数应该如何判断?

class A{
  public:
    virtual void Func1(){
      cout<<"A::Func1()"<<endl;
    }
    virtual void Func2(){
      cout<<"A::Func2()"<<endl;
    }
};
class B{
  public:
    virtual void Func1(){
      cout<<"B::Func1()"<<endl;
    }
    virtual void Func2(){
      cout<<"B::Func2()"<<endl;
    }
};
class C : public A,public B{
  public:
    virtual void Func1(){
      cout<<"C::Func1()"<<endl;
    }
    virtual void Func3(){
      cout<<"C::Func3()"<<endl;
    }
};
void test2(){
  C cc;
}

存在以上的继承关系;

使用GDB调试该程序并打印cc的内容;

p cc
$9 = {<A> = {_vptr.A = 0x400cc0 <vtable for C+16>}, <B> = {
    _vptr.B = 0x400ce8 <vtable for C+56>}, <No data fields>}

由第一点可以知道,派生类的虚表可以看作是基类虚表的拷贝,那么在该程序中由于存在两个基类(多继承),所以应当也有两个虚表;

那么在这个继承关系中,派生类自身所增加的虚函数处于哪个虚表?

实际上在多继承关系中,派生类自身所增加的虚函数都在第一个虚表中,且第一张虚表不仅只存在派生类自身的虚函数,还有一个较为关键的数据;

  • 第一张虚表
#-------64位机器偏移量为8---------
# C::Func1()  被重写
(gdb) x/x 0x400cc0
0x400cc0 <_ZTV1C+16>: 0x00400b56
(gdb) x/x 0x00400b56
0x400b56 <C::Func1()>:  0xe5894855
#-------------------------------
# A::Func2() 
(gdb) x/x 0x400cc8
0x400cc8 <_ZTV1C+24>: 0x00400ad2
(gdb) x/x 0x00400ad2
0x400ad2 <A::Func2()>:  0xe5894855
#-------------------------------
# C::Func3()  派生类自身
(gdb) x/x 0x400cd0 
0x400cd0 <_ZTV1C+32>: 0x00400b88
(gdb) x/x 0x00400b88
0x400b88 <C::Func3()>:  0xe5894855
#-------------------------------
(gdb) x/x 0x400cd8
0x400cd8 <_ZTV1C+40>: 0xfffffff8 #关键数据
#-------------------------------

从该结果可以观察到,派生类自身的虚函数位于第一张虚表当中;

且在最后一个位置存在一个0xfffffff8的数据;


  • 第二张虚表
#-------------------------------
# 所存数据并不为虚函数
(gdb) x/x 0x400ce8
0x400ce8 <_ZTV1C+56>: 0x00400b81
(gdb) x/x 0x00400b81
0x400b81 <_ZThn8_N1C5Func1Ev>:  0x08ef8348
(gdb) x/x 0x08ef8348
0x8ef8348:  Cannot access memory at address 0x8ef8348
#-------------------------------
# B类中未重写的虚函数
(gdb) x/x 0x400cf0
0x400cf0 <_ZTV1C+64>: 0x00400b2a
(gdb) x/x 0x00400b2a
0x400b2a <B::Func2()>:  0xe5894855
#-------------------------------
# NULL空
(gdb) x/x 0x400cf8
0x400cf8 <_ZTV1B>:  0x00000000
#-------------------------------
  • 从该虚表中能看到第二张虚表的第一个位置所存储的数据并不是函数指针;
    在这里就可以提到对应的0xfffffff8数据;
    已知0xffffffff的值为-1,对应的0xfffffff8即为-8;
    这里的值其实是一个偏移量,这个偏移量:

当走到该处时将该处的偏移量-8,即得到该处函数所在的位置;

根据这个点进行验证;

此时已经知道了位置为0x400ce8,且该位置所存储的数据为0x00400b81;

(gdb) x/x 0x400ce8
0x400ce8 <_ZTV1C+56>: 0x00400b81
(gdb) x/x 0x00400b81-8
0x400b79 <C::Func1()+35>: 0xfffcb2e8
  • 从这里就已经看出,这里通过了偏移量间接的找到了对应的函数;
    当编译器在处理这段代码时,将根据偏移量做出一些处理,使得最终能够通过该偏移量找到对应的函数;

结论为:若是出现多继承,其中两个基类都存在同名的虚函数且在派生类中对该虚函数已经完成了重写的条件时,其虚表构造为如下图:


相关文章
|
3月前
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
|
2月前
|
编译器 C++
类和对象(中 )C++
本文详细讲解了C++中的默认成员函数,包括构造函数、析构函数、拷贝构造函数、赋值运算符重载和取地址运算符重载等内容。重点分析了各函数的特点、使用场景及相互关系,如构造函数的主要任务是初始化对象,而非创建空间;析构函数用于清理资源;拷贝构造与赋值运算符的区别在于前者用于创建新对象,后者用于已存在的对象赋值。同时,文章还探讨了运算符重载的规则及其应用场景,并通过实例加深理解。最后强调,若类中存在资源管理,需显式定义拷贝构造和赋值运算符以避免浅拷贝问题。
|
2月前
|
存储 编译器 C++
类和对象(上)(C++)
本篇内容主要讲解了C++中类的相关知识,包括类的定义、实例化及this指针的作用。详细说明了类的定义格式、成员函数默认为inline、访问限定符(public、protected、private)的使用规则,以及class与struct的区别。同时分析了类实例化的概念,对象大小的计算规则和内存对齐原则。最后介绍了this指针的工作机制,解释了成员函数如何通过隐含的this指针区分不同对象的数据。这些知识点帮助我们更好地理解C++中类的封装性和对象的实现原理。
|
3月前
|
编译器 C语言 C++
类和对象的简述(c++篇)
类和对象的简述(c++篇)
|
2月前
|
安全 C++
【c++】继承(继承的定义格式、赋值兼容转换、多继承、派生类默认成员函数规则、继承与友元、继承与静态成员)
本文深入探讨了C++中的继承机制,作为面向对象编程(OOP)的核心特性之一。继承通过允许派生类扩展基类的属性和方法,极大促进了代码复用,增强了代码的可维护性和可扩展性。文章详细介绍了继承的基本概念、定义格式、继承方式(public、protected、private)、赋值兼容转换、作用域问题、默认成员函数规则、继承与友元、静态成员、多继承及菱形继承问题,并对比了继承与组合的优缺点。最后总结指出,虽然继承提高了代码灵活性和复用率,但也带来了耦合度高的问题,建议在“has-a”和“is-a”关系同时存在时优先使用组合。
158 6
|
3月前
|
编译器 C++
c++中的多态
c++中的多态
|
2月前
|
编译器 C++
类和对象(下)C++
本内容主要讲解C++中的初始化列表、类型转换、静态成员、友元、内部类、匿名对象及对象拷贝时的编译器优化。初始化列表用于成员变量定义初始化,尤其对引用、const及无默认构造函数的类类型变量至关重要。类型转换中,`explicit`可禁用隐式转换。静态成员属类而非对象,受访问限定符约束。内部类是独立类,可增强封装性。匿名对象生命周期短,常用于临时场景。编译器会优化对象拷贝以提高效率。最后,鼓励大家通过重复练习提升技能!
|
2月前
|
存储 编译器 C++
【c++】多态(多态的概念及实现、虚函数重写、纯虚函数和抽象类、虚函数表、多态的实现过程)
本文介绍了面向对象编程中的多态特性,涵盖其概念、实现条件及原理。多态指“一个接口,多种实现”,通过基类指针或引用来调用不同派生类的重写虚函数,实现运行时多态。文中详细解释了虚函数、虚函数表(vtable)、纯虚函数与抽象类的概念,并通过代码示例展示了多态的具体应用。此外,还讨论了动态绑定和静态绑定的区别,帮助读者深入理解多态机制。最后总结了多态在编程中的重要性和应用场景。 文章结构清晰,从基础到深入,适合初学者和有一定基础的开发者学习。如果你觉得内容有帮助,请点赞支持。 ❤❤❤
273 0
|
3月前
|
安全 编译器 C语言
【C++篇】深度解析类与对象(中)
在上一篇博客中,我们学习了C++类与对象的基础内容。这一次,我们将深入探讨C++类的关键特性,包括构造函数、析构函数、拷贝构造函数、赋值运算符重载、以及取地址运算符的重载。这些内容是理解面向对象编程的关键,也帮助我们更好地掌握C++内存管理的细节和编码的高级技巧。
|
3月前
|
存储 程序员 C语言
【C++篇】深度解析类与对象(上)
在C++中,类和对象是面向对象编程的基础组成部分。通过类,程序员可以对现实世界的实体进行模拟和抽象。类的基本概念包括成员变量、成员函数、访问控制等。本篇博客将介绍C++类与对象的基础知识,为后续学习打下良好的基础。