虚函数,虚表深度剖析

面向对象,从单一的类开始说起。

class A
{
private:
    int m_a;
    int m_b;
};

这个类中有两个成员变量,都是int类型,所以这个类在内存中占用多大的内存空间呢?

sizeof(A), 8个字节,一个int占用四个字节。下图验证:

 

这两个数据在内存中是怎样排列的呢?

 

原来是这样,我们根据debug出来的地址画出a对象在内存的结构图

 

如果 class A 中包含成员函数呢? A 的大小又是多少?

class A
{
public:
    void func1() {}    
private:
    int m_a;
    int m_b;
};

直接告诉你答案,类的成员函数多大? 没人能回答你,并且不是本文的重点,类的成员函数是放在代码区的,不算在类的大小内。

类的对象共享这一段代码,试想,如果每一个对象都有一段代码,光是存储这些代码得占用多少空间?所以同一个类的对象共用一段代码。

共用同一段代码怎么区分不同的对象呢?

实际上,你在调用成员函数时,a.func1() 会被编译器翻译为 A::func1(&a),也就是A* const this, this 就是 a 对象的地址。

所以根据this指针就能找到对应的数据,通过这同一段代码来处理不同的数据。

接下来我们讨论一下继承,子类继承父类,将会继承父类的数据,以及父类函数的调用权。

以下的测试可以验证这个情况。

class A
{
public:
    void func1() { cout << "A func1" << endl; }
private:
    int m_a;
    int m_b;
};

class B : public A
{
public:
    void func2() { cout << "B func2" << endl; }
private:
    int m_c;
};

int main(int argc, char const* argv[])
{
    B b;
    b.func1();
    b.func2();
    return 0;
}

输出:

// A func1
// B func2

那么对象b在内存中的结构是什么样的呢?

继承关系,先把a中的数据继承过来,再有一份自己的数据。

 

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

为了指定对象的虚表,对象内部包含指向一个虚表的指针,来指向自己所使用的虚表。为了让每个包含虚表的类的对象都拥有一个虚表指针,编译器在类中添加了一个指针,*__vptr,用来指向虚表。这样,当类的对象在创建时便拥有了这个指针,且这个指针的值会自动被设置为指向类的虚表。

class A
{
public:
    void func1() { cout << "A func1" << endl; }
    virtual void vfunc1() { cout << "A vfunc1" << endl; }
private:
    int m_a;
    int m_b;
};

cout << sizeof(A);, 输出12,A中包括两个int型的成员变量,一个虚指针,指针占4个字节。

a的内存结构如下:

虚表是一个函数指针数组,数组里存放的都是函数指针,指向虚函数所在的位置。

对象调用虚函数时,会根据虚指针找到虚表的位置,再根据虚函数声明的顺序找到虚函数在数组的哪个位置,找到虚函数的地址,从而调用虚函数。

调用普通函数则不像这样,普通函数在编译阶段就指定好了函数位置,直接调用即可。


class A
{
public:
    void func1() { cout << "A func1" << endl; }
    virtual void vfunc1() { cout << "A vfunc1" << endl; }
private:
    int m_a;
    int m_b;
};

class B : public A
{
public:
    void func1() { cout << "B func1" << endl; }
    virtual void vfunc2() { cout << "B vfunc2" << endl; }
private:
    int m_a;
};

像这样,B类继承自A类,B中又定义了一个虚函数vfunc2, 它的虚表又是怎么样的呢?

给出结论,虚表如下图所示:

我们来验证一下:

A a;
B b;
void(*avfunc1)() = (void(*)()) *(int*) (*(int*)&a);
void (*bvfunc1)() = (void(*)()) *(int*) *((int*)&b);
void (*bvfunc2)() = (void(*)()) * (int*)(*((int*)&b) + 4);
avfunc1();
bvfunc1();
bvfunc2();

来解释一下代码: void(*avfunc1)() 声明一个返回值为void, 无参数的函数指针 avfunc1, 变量名代表我们想要取A类的vfunc1这个虚函数。

右半部分的第一部分,(void(*)()) 代表我们最后要转换成对应上述类型的指针,右边需要给一个地址。

我们看 (*int(*)&a), 把a的地址强转成int*, 再解引用得到 虚指针的地址。

*(int*) (*(int*)&a) 再强转解引用得到虚表的地址,最后强转成函数指针。

同理得到 bvfunc1, bvfunc2, +4是因为一个指针占4个字节,+4得到虚表的第二项。


覆盖

class A
{
public:
    void func1() { cout << "A func1" << endl; }
    virtual void vfunc1() { cout << "A vfunc1" << endl; }
private:
    int m_a;
    int m_b;
};

class B : public A
{
public:
    void func1() { cout << "B func1" << endl; }
    virtual void vfunc1() { cout << "B vfunc1" << endl; }
private:
    int m_a;
};

子类重写父类的虚函数,需要函数签名保持一致,该种情况在内存中的结构为:

多态

父类指针指向子类对象的情况下,如果指针调用的是虚函数,则编译器会将会从虚指针所指的虚函数表中找到对应的地址执行相应的函数。

子类很多的话,每个子类都覆盖了对应的虚函数,则通过虚表找到的虚函数执行后不就执行了不同的代码嘛,表现出多态了嘛。

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

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

  • 通过指针来调用函数

  • 指针 upcast 向上转型(继承类向基类的转换称为 upcast)

  • 调用的是虚函数

为什么父类指针可以指向子类?

子类继承自父类,子类也属于A的类型。

最后通过一个例子来体会一下吧:

class Shape
{
public:
    virtual void draw() = 0;
};

class Rectangle : public Shape
{
    void draw() { cout << "rectangle" << endl; }
};

class Circle : public Shape
{
    void draw() { cout << "circle" << endl; }
};

class Triangle : public Shape
{
    void draw() { cout << "triangle" << endl; }
};


int main(int argc, char const *argv[])
{
    vector<Shape*> v;
    v.push_back(new Rectangle());
    v.push_back(new Circle());
    v.push_back(new Triangle());
    for (Shape* p : v) {
        p->draw();
    }
    return 0;
}

有些话是大白话,哈哈,如果这篇文章写的不错,解决了你的疑惑的话,点个赞再走吧!

不对的地方也请指出来,大家一起学习进步。

 

 
全部评论

相关推荐

10-28 11:04
已编辑
美团_后端实习生(实习员工)
一个2人:我说几个点吧,你的实习经历写的让人觉得毫无含金量,你没有挖掘你需求里的 亮点, 让人觉得你不仅打杂还摆烂。然后你的简历太长了🤣你这个实习经历看完,估计没几个人愿意接着看下去, sdk, 索引这种东西单拎出来说太顶真了兄弟,好好优化下简历吧
点赞 评论 收藏
分享
评论
点赞
收藏
分享
牛客网
牛客企业服务