关于c++:虚函数虚表深度剖析

2次阅读

共计 3557 个字符,预计需要花费 9 分钟才能阅读完成。

面向对象,从繁多的类开始说起。

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

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

不对的中央也请指出来,大家一起学习提高。

正文完
 0