虚函数表(图文详解)

简介: 虚函数表(图文详解)

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

需要注意的是在单继承和多继承关系中,下面我们去关注的是派生类对象的虚表模型,因为基类的虚表模型前面我们已经看过了,没什么需要特别研究的

1.1 单继承中的虚函数表

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

观察下图中的监视窗口中我们发现看不见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;
};
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()
{
  Base b;
  Derive d;
  // 思路:取出b、d对象的头4bytes,就是虚表的指针,前面我们说了虚函数表本质是一个存虚函数
  //指针的指针数组,这个数组最后面放了一个nullptr
    // 1.先取b的地址,强转成一个int*的指针
    // 2.再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针
    // 3.再强转成VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。
    // 4.虚表指针传递给PrintVTable进行打印虚表
    // 5.需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最
    //后面没有放nullptr,导致越界,这是编译器的问题。我们只需要点目录栏的 - 生成 - 清理解决方案,再
    //编译就好了。
  VFPTR * vTableb = (VFPTR*)(*(int*)&b);
  PrintVTable(vTableb);
  VFPTR* vTabled = (VFPTR*)(*(int*)&d);
  PrintVTable(vTabled);
  return 0;
}

思路:(32位机器下)取出b、d对象的头4bytes,就是虚表的指针,前面我们说了虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr。

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

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

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

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

5.需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最后面没有放nullptr,导致越界,这是编译器的问题。我们只需要点目录栏的 - 生成 - 清理解决方案,再编译就好了。

运行结果:

1.2 多继承中的虚函数表

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()
{
  Derive d;
  VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
  PrintVTable(vTableb1);
  VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));
  PrintVTable(vTableb2);
  return 0;
}

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

1.3 菱形继承、菱形虚拟继承

复杂的菱形继承及菱形虚拟继承(详解)

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

2.1 概念查考

  1. 下面哪种面向对象的方法可以让你变得富有( )
    A: 继承 B: 封装 C: 多态 D: 抽象

  1. ( )是面向对象程序设计语言中的一种机制。这种机制实现了方法的定义与具体的对象无关,而对方法的调用则可以关联于具体的对象。
    A: 继承 B: 模板 C: 对象的自身引用 D: 动态绑定

  1. 面向对象设计中的继承和组合,下面说法错误的是?()
    A:继承允许我们覆盖重写父类的实现细节,父类的实现对于子类是可见的,是一种静态复用,也称为白盒复用
    B:组合的对象不需要关心各自的实现细节,之间的关系是在运行时候才确定的,是一种动态复用,也称为黑盒复用
    C:优先使用继承,而不是组合,是面向对象设计的第二原则
    D:继承可以使子类能自动继承父类的接口,但在设计模式中认为这是一种破坏了父类的封装性的表现

  1. 以下关于纯虚函数的说法,正确的是( )
    A:声明纯虚函数的类不能实例化对象 B:声明纯虚函数的类是虚基类
    C:子类必须实现基类的纯虚函数 D:纯虚函数必须是空函数

  1. 关于虚函数的描述正确的是( )
    A:派生类的虚函数与基类的虚函数具有不同的参数个数和类型 B:内联函数不能是虚函数
    C:派生类必须重新定义基类的虚函数 D:虚函数可以是一个static型的函数

  1. 关于虚表说法正确的是( )
    A:一个类只能有一张虚表
    B:基类中有虚函数,如果子类中没有重写基类的虚函数,此时子类与基类共用同一张虚表
    C:虚表是在运行期间动态生成的
    D:一个类的不同对象共享该类的虚表

  1. 假设A类中有虚函数,B继承自A,B重写A中的虚函数,也没有定义任何虚函数,则( )
    A:A类对象的前4个字节存储虚表地址,B类对象前4个字节不是虚表地址
    B:A类对象和B类对象前4个字节存储的都是虚基表的地址
    C:A类对象和B类对象前4个字节存储的虚表地址相同
    D:A类和B类虚表中虚函数个数相同,但A类和B类使用的不是同一张虚表

  1. 下面程序输出结果是什么? ()
#include<iostream>
using namespace std;
class A {
public:
  A(const char* s) { cout << s << endl; }
  ~A() {}
};
class B :virtual public A
{
public:
  B(const char* s1, const char* s2) :A(s1) { cout << s2 << endl; }
};
class C :virtual public A
{
public:
  C(const char* s1, const char* s2) :A(s1) { cout << s2 << endl; }
};
class D :public B, public C
{
public:
  D(const char* s1, const char* s2, const char* s3, const char* s4) :B(s1, s2), C(s1, s3), A(s1)
  {
    cout << s4 << endl;
  }
};
int main() {
  D* p = new D("class A", "class B", "class C", "class D");
  delete p;
  return 0;
}

A:class A class B class C class D B:class D class B class C class A
C:class D class C class B class A D:class A class C class B class D


  1. 多继承中指针偏移问题?下面说法正确的是( )
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


  1. 以下程序输出结果是什么()
class A
{
public:
    virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }
    virtual void test() { func(); }
};
class B : public A
{
public:
    void func(int val = 0) { std::cout << "B->" << val << std::endl; }
};
int main(int argc, char* argv[])
{
    B* p = new B;
    p->test();
    return 0;
}

A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确


参考答案:

1. A 2. D 3. C 4. A 5. B

6. D 7. D 8. A 9. C 10. B

2.2 问答题

  1. 什么是多态?
    答:多态的概念
  2. 什么是重载、重写(覆盖)、重定义(隐藏)?
    答:重载,重写,重定义的区别
  3. 多态的实现原理?
    答:多态原理
  4. inline函数可以是虚函数吗?
    答:可以,不过编译器就忽略inline属性,这个函数就不再是inline,因为虚函数要放到虚表中去。
  5. 静态成员可以是虚函数吗?答:不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。
  6. 构造函数可以是虚函数吗?答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。
  7. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?
    答:可以,并且最好把基类的析构函数定义成虚函数。
  8. 对象访问普通函数快还是虚函数更快?
    答:首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。
  9. 虚函数表是在什么阶段生成的,存在哪的?
    答:虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的。
  10. C++菱形继承的问题?虚继承的原理?
    答:菱形继承
  11. 什么是抽象类?抽象类的作用?
    答:抽象类
    (本章完)
相关文章
|
6月前
|
IDE Java Linux
Java基础阶段的常见错误和解决方案的文章
本文精选Java基础常见错误与解决方案的优质文章,涵盖环境配置、语法基础、面向对象、异常处理、集合IO等核心知识点,结合典型错误代码与原理分析,助力新手避坑提效,适合系统学习与实战参考。
252 0
|
存储 人工智能 程序员
通义灵码AI程序员实战:从零构建Python记账本应用的开发全解析
本文通过开发Python记账本应用的真实案例,展示通义灵码AI程序员2.0的代码生成能力。从需求分析到功能实现、界面升级及测试覆盖,AI程序员展现了需求转化、技术选型、测试驱动和代码可维护性等核心价值。文中详细解析了如何使用Python标准库和tkinter库实现命令行及图形化界面,并生成单元测试用例,确保应用的稳定性和可维护性。尽管AI工具显著提升开发效率,但用户仍需具备编程基础以进行调试和优化。
919 9
|
机器学习/深度学习 算法
深度学习中的优化算法及其应用
本文探讨了深度学习中常用的优化算法,包括梯度下降、随机梯度下降、动量方法和Adam方法。通过对比这些算法的优缺点及适用场景,帮助读者更好地理解和应用这些优化方法。
399 2
|
机器学习/深度学习 人工智能 自然语言处理
AI浪潮下,我们需要什么样的创新型人才?
本文探讨了AI浪潮下创新型人才的需求与培养路径。随着生成式人工智能(GAI)技术的突破,AI已渗透至各领域,重塑传统行业并带来机遇与挑战。创新型人才需具备跨学科知识融合、持续学习、创新实践能力及伦理责任感。GAI认证作为权威标准,助力人才技能提升和企业招聘。未来应加强跨学科教育、实践教学、持续学习与伦理教育,以培养更多适应AI时代的创新型人才。
|
虚拟化 数据安全/隐私保护 iOS开发
VMware——安装MacOS 系统教程(仅供学习交流)
VMware——安装MacOS 系统教程(仅供学习交流)
562 4
|
机器学习/深度学习 编解码 计算机视觉
GAN Step By Step -- Step5 ACGAN
GAN Step By Step -- Step5 ACGAN
GAN Step By Step -- Step5 ACGAN
|
存储 人工智能 程序员
技术心得记录:堆(heap)与栈(stack)的区别
技术心得记录:堆(heap)与栈(stack)的区别
1067 0
|
自动驾驶 安全 物联网
5G网络:速度之外的优势深度解析
【8月更文挑战第5天】综上所述,5G网络的优势远不止于其惊人的速度。其超低时延、大规模连接能力、高可靠性和稳定性、灵活频谱利用以及创新的网络切片技术等优势共同构建了一个全新的智能世界。在这个世界里,万物皆可互联、实时交互成为可能,关键业务应用得到可靠保障,频谱资源得到高效利用,网络资源实现高度定制化。5G网络正以前所未有的方式改变着我们的生活和工作方式,引领着未来的智能生活与社会创新。
1343 2
|
存储 程序员 C语言
堆和栈之间有什么区别
【9月更文挑战第1天】堆和栈之间有什么区别
3707 0