服务粉丝

我们一直在努力
当前位置:首页 > 财经 >

面试官:连 C++ 虚函数表都不知道,不想跟你聊了

日期: 来源:字节流动收集编辑:Leo

一、概述

为了实现 C++ 的多态,C++ 使用了一种动态绑定的技术。这个技术的核心是虚函数表(下文简称虚表)。本文介绍虚函数表是如何实现动态绑定的。


二、类的虚表

每个包含了虚函数的类都包含一个虚表


我们知道,当一个类(A)继承另一个类(B)时,类 A 会继承类 B 的函数的调用权。


所以如果一个基类包含了虚函数,那么其继承类也可调用这些虚函数,换句话说,一个类继承了包含虚函数的基类,那么这个类也拥有自己的虚表。


我们来看以下的代码。类 A 包含虚函数 vfunc1,vfunc2,由于类 A 包含虚函数,故类 A 拥有一个虚表。


class A {
public:
    virtual void vfunc1();
    virtual void vfunc2();
    void func1();
    void func2();
private:
    int m_data1, m_data2;
};

类 A 的虚表如图 1 所示。

图 1:类 A 的虚表示意图


虚表是一个指针数组,其元素是虚函数的指针,每个元素对应一个虚函数的函数指针。需要指出的是,普通的函数即非虚函数,其调用并不需要经过虚表,所以虚表的元素并不包括普通函数的函数指针。


虚表内的条目,即虚函数指针的赋值发生在编译器的编译阶段,也就是说在代码的编译阶段,虚表就可以构造出来了。


三、虚表指针

虚表是属于类的,而不是属于某个具体的对象,一个类只需要一个虚表即可。同一个类的所有对象都使用同一个虚表。


为了指定对象的虚表,对象内部包含一个虚表的指针,来指向自己所使用的虚表。为了让每个包含虚表的类的对象都拥有一个虚表指针,编译器在类中添加了一个指针,*__vptr,用来指向虚表。


这样,当类的对象在创建时便拥有了这个指针,且这个指针的值会自动被设置为指向类的虚表。


图 2:对象与它的虚表

上面指出,一个继承类的基类如果包含虚函数,那个这个继承类也有拥有自己的虚表,故这个继承类的对象也包含一个虚表指针,用来指向它的虚表。


四、动态绑定

说到这里,大家一定会好奇 C++ 是如何利用虚表和虚表指针来实现动态绑定的。我们先看下面的代码。

class A {
public:
    virtual void vfunc1();
    virtual void vfunc2();
    void func1();
    void func2();
private:
    int m_data1, m_data2;
};

class B : public A {
public:
    virtual void vfunc1();
    void func1();
private:
    int m_data3;
};

class C: public B {
public:
    virtual void vfunc2();
    void func2();
private:
    int m_data1, m_data4;
};

类 A 是基类,类 B 继承类 A,类 C 又继承类 B。类 A,类 B,类 C,其对象模型如下图 3 所示。

图 3:类 A,类 B,类 C 的对象模型


由于这三个类都有虚函数,故编译器为每个类都创建了一个虚表,即类 A 的虚表(A vtbl),类 B 的虚表(B vtbl),类 C 的虚表(C vtbl)。类 A,类 B,类 C 的对象都拥有一个虚表指针,*__vptr,用来指向自己所属类的虚表。


类 A 包括两个虚函数,故 A vtbl 包含两个指针,分别指向A::vfunc1()和A::vfunc2()。


类 B 继承于类 A,故类 B 可以调用类 A 的函数,但由于类 B 重写了B::vfunc1()函数,故 B vtbl 的两个指针分别指向B::vfunc1()和A::vfunc2()。


类 C 继承于类 B,故类 C 可以调用类 B 的函数,但由于类 C 重写了C::vfunc2()函数,故 C vtbl 的两个指针分别指向B::vfunc1()(指向继承的最近的一个类的函数)和C::vfunc2()。


虽然图 3 看起来有点复杂,但是只要抓住“对象的虚表指针用来指向自己所属类的虚表,虚表中的指针会指向其继承的最近的一个类的虚函数”这个特点,便可以快速将这几个类的对象模型在自己的脑海中描绘出来。


非虚函数的调用不用经过虚表,故不需要虚表中的指针指向这些函数。


假设我们定义一个类 B 的对象。由于 bObject是类 B 的一个对象,故bObject包含一个虚表指针,指向类 B 的虚表。


int main() 
{
    B bObject;
}

现在,我们声明一个类 A 的指针p来指向对象bObject。


虽然p是基类的指针只能指向基类的部分,但是虚表指针亦属于基类部分,所以p可以访问到对象bObject的虚表指针。


bObject的虚表指针指向类 B 的虚表,所以p可以访问到 B vtbl。如图 3 所示。


int main() 
{
    B bObject;
    A *p = & bObject;
}

当我们使用 p 来调用 vfunc1() 函数时,会发生什么现象?


int main() 
{
    B bObject;
    A *p = & bObject;
    p->vfunc1();
}

程序在执行p->vfunc1()时,会发现p是个指针,且调用的函数是虚函数,接下来便会进行以下的步骤。


首先,根据虚表指针p->__vptr来访问对象bObject对应的虚表。


虽然指针p是基类A*类型,但是*__vptr也是基类的一部分,所以可以通过p->__vptr可以访问到对象对应的虚表。


然后,在虚表中查找所调用的函数对应的条目。由于虚表在编译阶段就可以构造出来了,所以可以根据所调用的函数定位到虚表中的对应条目。


对于 p->vfunc1()的调用,B vtbl 的第一项即是vfunc1对应的条目。


最后,根据虚表中找到的函数指针,调用函数。从图 3 可以看到,B vtbl 的第一项指向B::vfunc1(),所以 p->vfunc1()实质会调用B::vfunc1() 函数。


如果p指向类 A 的对象,情况又是怎么样?


int main() 
{
    A aObject;
    A *p = &aObject;
    p->vfunc1();
}

当aObject在创建时,它的虚表指针__vptr已设置为指向 A vtbl,这样p->__vptr就指向 A vtbl。vfunc1在 A vtbl 对应在条目指向了A::vfunc1()函数,所以 p->vfunc1()实质会调用A::vfunc1()函数。


可以把以上三个调用函数的步骤用以下表达式来表示:


(*(p->__vptr)[n])(p)

可以看到,通过使用这些虚函数表,即使使用的是基类的指针来调用函数,也可以达到正确调用运行中实际对象的虚函数。


我们把经过虚表调用虚函数的过程称为动态绑定,其表现出来的现象称为运行时多态。动态绑定区别于传统的函数调用,传统的函数调用我们称之为静态绑定,即函数的调用在编译阶段就可以确定下来了。


那么,什么时候会执行函数的动态绑定?这需要符合以下三个条件。


  • 通过指针来调用函数

  • 指针 upcast 向上转型(继承类向基类的转换称为 upcast,关于什么是 upcast,可以参考本文的参考资料)

  • 调用的是虚函数


如果一个函数调用符合以上三个条件,编译器就会把该函数调用编译成动态绑定,其函数的调用过程走的是上述通过虚表的机制。


五、总结

封装,继承,多态是面向对象设计的三个特征,而多态可以说是面向对象设计的关键。C++ 通过虚函数表,实现了虚函数与对象的动态绑定,从而构建了 C++ 面向对象程序设计的基石。


参考资料

  • 《C++ Primer》第三版,中文版,潘爱民等译

  • http://www.learncpp.com/cpp-tutorial/125-the-virtual-table/

  • 侯捷《C++最佳编程实践》视频,极客班,2015

  • Upcasting and Downcasting, http://www.bogotobogo.com/cplusplus/upcasting_downcasting.php


附录

https://github.com/haozlee/vtable/blob/master/main.cpp

如果觉得本文对您理解 C++ 虚函数如何实现多态有帮助,可以 star 一下~


  • 本文作者:Leo

  • 本文链接:https://leehao.me/C-虚函数表剖析/

  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!



-- END --


进技术交流群,扫码添加我的微信:Byte-Flow



获取相关资料和源码



推荐:

Android FFmpeg 实现带滤镜的微信小视频录制功能

全网最全的 Android 音视频和 OpenGL ES 干货,都在这了

一文掌握 YUV 图像的基本处理

抖音传送带特效是怎么实现的?

所有你想要的图片转场效果,都在这了

面试官:如何利用 Shader 实现 RGBA 到 NV21 图像格式转换?

我用 OpenGL ES 给小姐姐做了几个抖音滤镜

JNI 局部引用、全局引用和弱全局引用

Android JNI SO 加载原理

Android JNI 动态库逆向

NDK/JNI 开发之 Java 类和 C 结构体互转示例

建议收藏 | Android JNI 相关文章汇总

相关阅读

  • Android-Native 开发之利用 AAudio 播放音频

  • 前言谈到在Android C/C++层实现音频播放/录制功能的时候,大家可能首先会想到的是利用opensles去做,这确实是一直不错的实现方式,久经考验,并且适配比较广。但如果你的项目最低版
  • 使用 STL 实现一个 C++ 线程池

  • C++ 线程池参考链接:基于C++11实现线程池的工作原理;c++简单线程池实现https://www.cnblogs.com/ailumiyana/p/10016965.html https://www.cnblogs.com/yangang92/p/5485868
  • 浅析Pe2shellcode

  • 编者注:本文仅供学习研究,严禁从事非法活动,任何后果由使用者本人负责。前言众所周知,对shellcode免杀是很流行的技术,但是直接对exe的免杀方法相对稀缺,如果我们能将exe转为shell
  • 弈 - Codeql 自动运行和项目监控工具

  • 前言代码审计总是离不开一些神器,笔者常用 Codeql[1] 这款工具辅助挖洞。当我每写一个规则都需要对其它项目手动运行检查一遍,效率很低,再加上 lgtm[2] 的关闭,此项目诞生了 ---
  • constexpr

  • 前面介绍了模板这种编译期动作,关于编译期动作,有必要介绍下constexpr。在这之前有必要简单提一下constexpr与const的关系,两者字面上都表达常量的意思。主要的区别是:const修饰
  • 万字长文教你如何做出 ChatGPT

  • 作者:monychen,腾讯 IEG 应用研究员简单来说,ChatGPT 是自然语言处理(NLP)和强化学习(RL)的一次成功结合,考虑到读者可能只熟悉其中一个方向或者两个方向都不太熟悉,本文会将 ChatGPT
  • IcedID僵尸网络滥用谷歌 PPC服务分发恶意软件

  • 关键词IcedID僵尸网络、谷歌 PPC、恶意软件1. 概述在密切跟踪 IcedID 僵尸网络的活动后,趋势科技的研究人员发现其分发方法发生了一些重大变化。自 2022 年 12 月以来, 趋势科

热门文章

  • “复活”半年后 京东拍拍二手杀入公益事业

  • 京东拍拍二手“复活”半年后,杀入公益事业,试图让企业捐的赠品、家庭闲置品变成实实在在的“爱心”。 把“闲置品”变爱心 6月12日,“益心一益·守护梦想每一步”2018年四

最新文章

  • Android-Native 开发之利用 AAudio 播放音频

  • 前言谈到在Android C/C++层实现音频播放/录制功能的时候,大家可能首先会想到的是利用opensles去做,这确实是一直不错的实现方式,久经考验,并且适配比较广。但如果你的项目最低版
  • 使用 STL 实现一个 C++ 线程池

  • C++ 线程池参考链接:基于C++11实现线程池的工作原理;c++简单线程池实现https://www.cnblogs.com/ailumiyana/p/10016965.html https://www.cnblogs.com/yangang92/p/5485868