查看原文
其他

C++ 面试必问:深入理解虚函数表

字节流动 2022-09-25

深入理解C++ 虚函数表

C++中的虚函数的作用主要是实现了多态的机制。关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数

Derive d;
Base1 *b1 = &d;
Base2 *b2 = &d;
Base3 *b3 = &d;
b1->f(); //Derive::f()
b2->f(); //Derive::f()
b3->f(); //Derive::f()

b1->g(); //Base1::g()
b2->g(); //Base2::g()
b3->g(); //Base3::g()

这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法。


比如:模板技术,RTTI技术,虚函数技术,要么是试图做到在编译时决议,要么试图做到运行时决议。


本文将详细介绍虚函数表的实现及其内存布局。

虚函数表概述

虚函数表是指在每个包含虚函数的类中都存在着一个函数地址的数组。当我们用父类的指针来操作一个子类的时候,这张虚函数表指明了实际所应该调用的函数。


C++的编译器保证虚函数表的指针存在于对象实例中最前面的位置,这样通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。


按照上面的说法,来看一个实际的例子:

#include <iostream>

using namespace std;

class Base {
public:
virtual void f() { cout << "f()" << endl; }
virtual void g() { cout << "g()" << endl; }
virtual void h() { cout << "h()" << endl; }
};

int main()
{
Base t;
( ((void(*)())*((int*)(*((int*)&t)) + 0)) ) ();
( ((void(*)())*((int*)(*((int*)&t)) + 1)) ) ();
( ((void(*)())*((int*)(*((int*)&t)) + 2)) ) ();
return 0;
}

经过VS2017,x86测试:



我们成功地通过实例对象的地址,得到了对象所有的类函数。

main定义Base类对象t,把&b转成int *,取得虚函数表的地址vtptr就是:(int*)(&t),然后再解引用并强转成int * 得到第一个虚函数的地址,也就是Base::f()即(int*)(*((int*)&t)),那么,第二个虚函数g()的地址就是(int*)(*((int*)&t)) + 1,依次类推。

单继承下的虚函数表

派生类未覆盖基类虚函数


下面我们来看下派生类没有覆盖基类虚函数的情况,其中Base类延用上一节的定义。从图中可看出虚函数表中依照声明顺序先放基类的虚函数地址,再放派生类的虚函数地址。


可以看到下面几点:

1)虚函数按照其声明顺序放于表中。
2)父类的虚函数在子类的虚函数前面。

测试代码:

#include <iostream>

using namespace std;

class Base {
public:
virtual void f() { cout << "f()" << endl; }
virtual void g() { cout << "g()" << endl; }
virtual void h() { cout << "h()" << endl; }
};

class Devired :public Base{
public:
virtual void x() { cout << "x()" << endl; }
};

int main()
{
Devired t;
(((void(*)()) *((int*)(*((int*)&t))))) ();

(((void(*)())*((int*)(*((int*)&t)) + 1))) ();

(((void(*)())*((int*)(*((int*)&t)) + 2))) ();
//(((void(*)())*((int*)(*((int*)&t)) + 3))) ();

return 0;
}

测试效果:


派生类覆盖基类虚函数


再来看一下派生类覆盖了基类的虚函数的情形,可见:

  • 虚表中派生类覆盖的虚函数的地址被放在了基类相应的函数原来的位置 (显然的,不然虚函数失去意义)

  • 派生类没有覆盖的虚函数延用基类的

测试代码:

#include <iostream>

using namespace std;

class Base {
public:
virtual void f() { cout << "f()" << endl; }
virtual void g() { cout << "g()" << endl; }
virtual void h() { cout << "h()" << endl; }
};

class Derive :public Base{
public:
virtual void x() { cout << "x()" << endl; }
virtual void f() { cout << "Derive::f()" << endl; }
};

int main()
{
Derive t;
(((void(*)()) *((int*)(*((int*)&t))))) ();

(((void(*)())*((int*)(*((int*)&t)) + 1))) ();

(((void(*)())*((int*)(*((int*)&t)) + 2))) ();
//(((void(*)())*((int*)(*((int*)&t)) + 3))) ();

return 0;
}

测试效果:



多继承下的虚函数表

无虚函数覆盖

如果是多重继承的话,问题就变得稍微复杂一丢丢,主要有几点:

  • 每个基类都有自己的虚函数表

  • 派生类的虚函数地址存依照声明顺序放在第一个基类的虚表最后(这点和单继承无虚函数覆盖相同),具体见下图所示:


测试代码

#include <iostream>
class Base
{
public:
Base(int mem1 = 1, int mem2 = 2) : m_iMem1(mem1), m_iMem2(mem2) { ; }

virtual void vfunc1() { std::cout << "In vfunc1()" << std::endl; }
virtual void vfunc2() { std::cout << "In vfunc2()" << std::endl; }
virtual void vfunc3() { std::cout << "In vfunc3()" << std::endl; }

private:
int m_iMem1;
int m_iMem2;
};

class Base2
{
public:
Base2(int mem = 3) : m_iBase2Mem(mem) { ; }
virtual void vBase2func1() { std::cout << "In Base2 vfunc1()" << std::endl; }
virtual void vBase2func2() { std::cout << "In Base2 vfunc2()" << std::endl; }

private:
int m_iBase2Mem;
};

class Base3
{
public:
Base3(int mem = 4) : m_iBase3Mem(mem) { ; }
virtual void vBase3func1() { std::cout << "In Base3 vfunc1()" << std::endl; }
virtual void vBase3func2() { std::cout << "In Base3 vfunc2()" << std::endl; }

private:
int m_iBase3Mem;
};

class Devired : public Base, public Base2, public Base3
{
public:
Devired(int mem = 7) : m_iMem1(mem) { ; }
virtual void vdfunc1() { std::cout << "In Devired vdfunc1()" << std::endl; }

private:
int m_iMem1;
};

int main()
{
// Test_3
Devired d;
int *dAddress = (int*)&d;
typedef void(*FUNC)();

/* 1. 获取对象的内存布局信息 */
// 虚表地址一
int *vtptr1 = (int*)*(dAddress + 0);
int basemem1 = (int)*(dAddress + 1);
int basemem2 = (int)*(dAddress + 2);

int *vtpttr2 = (int*)*(dAddress + 3);
int base2mem = (int)*(dAddress + 4);

int *vtptr3 = (int*)*(dAddress + 5);
int base3mem = (int)*(dAddress + 6);

/* 2. 输出对象的内存布局信息 */
int *pBaseFunc1 = (int *)*(vtptr1 + 0);
int *pBaseFunc2 = (int *)*(vtptr1 + 1);
int *pBaseFunc3 = (int *)*(vtptr1 + 2);
int *pBaseFunc4 = (int *)*(vtptr1 + 3);

(FUNC(pBaseFunc1))();
(FUNC(pBaseFunc2))();
(FUNC(pBaseFunc3))();
(FUNC(pBaseFunc4))();
// .... 后面省略若干输出内容,可自行补充
return 0;
}

测试效果:


派生类覆盖基类虚函数


我们再来看一下派生类覆盖了基类的虚函数的情形,可见:

  • 虚表中派生类覆盖的虚函数的地址被放在了基类相应的函数原来的位置

  • 派生类没有覆盖的虚函数延用基类的


代码如下所示,注意这里只给出了类的定义,main函数的测试代码与上节一样:

class Devired : public Base, public Base2, public Base3
{
public:
Devired(int mem = 7) : m_iMem1(mem) { ; }
virtual void vdfunc1() { std::cout << "In Devired vdfunc1()" << std::endl; }
virtual void vfunc1() { std::cout << "In Devired vfunc1()" << std::endl; }
virtual void vBase2func1() { std::cout << "In Devired vfunc1()" << std::endl; }

private:
int m_iMem1;
};

测试效果

钻石型虚继承

该继承还是遵循上述的所有原则,我们直接来测试。


测试代码

// 测试四:钻石型虚继承

//虚基指针所指向的虚基表的内容:
// 1. 虚基指针的第一条内容表示的是该虚基指针距离所在的子对象的首地址的偏移
// 2. 虚基指针的第二条内容表示的是该虚基指针距离虚基类子对象的首地址的偏移

#pragma vtordisp(off)
#include <iostream>
using std::cout;
using std::endl;

class B
{
public:
B() : _ib(10), _cb('B') {}

virtual void f()
{
cout << "B::f()" << endl;
}

virtual void Bf()
{
cout << "B::Bf()" << endl;
}

private:
int _ib;
char _cb;
};

class B1 : virtual public B
{
public:
B1() : _ib1(100), _cb1('1') {}

virtual void f()
{
cout << "B1::f()" << endl;
}

#if 1
virtual void f1()
{
cout << "B1::f1()" << endl;
}
virtual void Bf1()
{
cout << "B1::Bf1()" << endl;
}
#endif

private:
int _ib1;
char _cb1;
};

class B2 : virtual public B
{
public:
B2() : _ib2(1000), _cb2('2') {}

virtual void f()
{
cout << "B2::f()" << endl;
}
#if 1
virtual void f2()
{
cout << "B2::f2()" << endl;
}
virtual void Bf2()
{
cout << "B2::Bf2()" << endl;
}
#endif
private:
int _ib2;
char _cb2;
};

class D : public B1, public B2
{
public:
D() : _id(10000), _cd('3') {}

virtual void f()
{
cout << "D::f()" << endl;
}

#if 1
virtual void f1()
{
cout << "D::f1()" << endl;
}
virtual void f2()
{
cout << "D::f2()" << endl;
}

virtual void Df()
{
cout << "D::Df()" << endl;
}
#endif
private:
int _id;
char _cd;
};

int main(void)
{
D d;
cout << sizeof(d) << endl;
return 0;
}


测试效果

1>class D size(52):
1> +---
1> 0 | +--- (base class B1)
1> 0 | | {vfptr}
1> 4 | | {vbptr}
1> 8 | | _ib1
1>12 | | _cb1
1> | | <alignment member> (size=3)
1> | +---
1>16 | +--- (base class B2)
1>16 | | {vfptr}
1>20 | | {vbptr}
1>24 | | _ib2
1>28 | | _cb2
1> | | <alignment member> (size=3)
1> | +---
1>32 | _id
1>36 | _cd
1> | <alignment member> (size=3)
1> +---
1> +--- (virtual base B)
1>40 | {vfptr}
1>44 | _ib
1>48 | _cb
1> | <alignment member> (size=3)
1> +---
1>
1>D::$vftable@B1@:
1> | &D_meta
1> | 0
1> 0 | &D::f1
1> 1 | &B1::Bf1
1> 2 | &D::Df
1>
1>D::$vftable@B2@:
1> | -16
1> 0 | &D::f2
1> 1 | &B2::Bf2
1>
1>D::$vbtable@B1@:
1> 0 | -4
1> 1 | 36 (Dd(B1+4)B)
1>
1>D::$vbtable@B2@:
1> 0 | -4
1> 1 | 20 (Dd(B2+4)B)
1>
1>D::$vftable@B@:
1> | -40
1> 0 | &D::f
1> 1 | &B::Bf
1>

总结

几个原则


单继承

  • 虚表中派生类覆盖的虚函数的地址被放在了基类相应的函数原来的位置

  • 派生类没有覆盖的虚函数就延用基类的。同时,虚函数按照其声明顺序放于表中,父类的虚函数在子类的虚函数前面。

多继承

每个基类都有自己的虚函数表
派生类的虚函数地址存依照声明顺序放在第一个基类的虚表最后


安全性问题

当我们直接通过父类指针调用子类中的未覆盖父类的成员函数,编译器会报错,但通过实验,我们可以用对象的地址访问到各个子类的成员函数,就违背了C++语义,操作会有一定的隐患,当我们使用时要注意这些危险的东西!


参考:

https://coolshell.cn/articles/12165.html

https://jocent.me/2017/08/07/virtual-table.html

https://blog.csdn.net/lihao21/article/details/50688337


来源:https://www.cnblogs.com/Mered1th/p/10924545.html


-- END --


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



获取视频教程和源码



推荐:

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

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

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

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

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

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

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

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存