【C++】虚函数表、虚函数指针

虚函数表、虚指针

​ 当一个类在实现的时候,如果存在一个或以上的虚函数时,那么这个类便会包含一张虚函数表。而当一个子类继承并重载了基类的虚函数时,它也会有自己的一张虚函数表。当我们在设计类的时候,如果把某个函数设置成虚函数时,也就表明我们希望子类在继承的时候能够有自己的实现方式;如果我们明确这个类不会被继承,那么就不应该有虚函数的出现。

下面是某个基类A的实现:

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

​ 从下图中可以看到该类在内存中的存放形式,对于虚函数的调用是通过查虚函数表来进行的,每个虚函数在虚函数表中都存放着自己的一个地址,而如何在虚函数表中进行查找,则是通过虚指针来调用,在内存结构中它一般都会放在类最开始的地方,而对于普通函数则不需要通过查表操作。这张虚函数表是什么时候被创建的呢?它是在编译的时候产生,否则这个类的结构信息中也不会插入虚指针的地址信息。

【C++】虚函数表、虚函数指针_第1张图片

以下例子包含了继承关系:

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

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

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

以上三个类在内存中的排布关系如下图所示:
【C++】虚函数表、虚函数指针_第2张图片

  1. 对于非虚函数,三个类中虽然都有一个叫 func2 的函数,但他们彼此互不关联,因此都是各自独立的,不存在重载一说,在调用的时候也不需要进行查表的操作,直接调用即可。
  2. 由于子类B和子类C都是继承于基类A,因此他们都会存在一个虚指针用于指向虚函数表。注意,假如子类B和子类C中不存在虚函数,那么这时他们将共用基类A的一张虚函数表,在B和C中用虚指针指向该虚函数表即可。但是,上面的代码设计时子类B和子类C中都有一个虚函数 vfunc1,因此他们就需要各自产生一张虚函数表,并用各自的虚指针指向该表。由于子类B和子类C都对 vfunc1 作了重载,因此他们有三种不同的实现方式,函数地址也不尽相同,在使用的时候需要从各自类的虚函数表中去查找对应的 vfunc1 地址。
  3. 对于虚函数 vfunc2,两个子类都没有进行重载操作,所以基类A、子类B和子类C将共用一个 vfunc2,该虚函数的地址会分别保存在三个类的虚函数表中,但他们的地址是相同的。
  4. 从上图可以发现,在类对象的头部存放着一个虚指针,该虚指针指向了各自类所维护的虚函数表,再通过查找虚函数表中的地址来找到对应的虚函数。
  5. 对于类中的数据而言,子类中都会包含父类的信息。如上例中的子类C,它自己拥有一个变量 m_data1,似乎是和基类中的 m_data1 重名了,但其实他们并不存在联系,从存放的位置便可知晓。

关于动态绑定

在设计了以上三个类之后,我们就要开始对它们进行使用。

int main() 
{
    B b;
    b.vfunc1();

    A a = (A)b;
    a.vfunc1();
}

​ 假如在程序中分别创建两个对象 a 和 b,a的创建是通过将b强制转化为类A得来的。对于 b.vfunc1() 的调用,应该没有太的疑问,它所调用的就是类B中的 vfunc1。而对于 a.vfunc1() 的调用,它虽然是强制转化后的结果,但并不能改变它是一个类A对象的事实,因此这里调用的便是类A中的 vfunc1,也就是上图中显示绿色的函数。

int main() 
{
    A* pa = new B;
    pa->vfunc1();

    B b;
    pa = &b;
    pa->vfunc1();
}

​ 将程序改写成以上内容,pa 是一个类A的指针,但它指向的是一个类B的对象。在使用pa调用 vfunc1 的时候,程序发现pa是一个指针,并且现在正在调用一个虚函数叫做 vfunc1,这时通过 pa->vptr 这个虚指针到类B的虚函数中(上图的B vtbl)找对应的虚函数地址,找到该地址以后,就用相应的虚函数来进行调用,也就是调用上图所示的 B::vfunc1()。

pa是类A的指针,为什么查找的是类B的虚函数表?
只要某一个类X包含虚函数,无论是它的父类或者它本身拥有,那么这个类的对象都会包含一个虚指针vptr,至于vptr要指向哪张表,取决于类X它本身是否含有虚函数。此处,类B中存在虚函数,那么它就会拥有自己的一张虚函数表。pa指向的是一个类B的对象,因此
p-vptr 指代的是类B中虚指针,所以它查找的是类B的虚函数表

如何从虚函数表中查找到 vfunc1 的地址? 虚函数表中的内容是在编译的时候确定的,通过以下方式进行查找 (* p->vptr[n]
)§ 或者 (* (p->vptr)[n]
)§,它的解读是:通过类对象指针p找到虚指针vptr,再查找到虚函数表中的第n个内容,并将他作为函数指针进行调用,调用时的入参是p(式子中的第二个p),而这个p就是隐藏的this指针,这里的n也是在编译的时候确定的。

int main()
{
    A a;
    B b;

    A* p1 = &a;
    p1->vfunc1();

    A* p2 = &b;
    p2->vfunc1();
}

​ 再将程序修改成以上内容,对于 p2->vfunc1() 的调用和上文所述一致,它调用的是 B::vfunc1 函数。而对于 p1->vfunc1() 的调用,同样通过上面的方法可知, p1->vptr 它所指向的是类A的虚函数表,因此它调用的是 A::vfunc1 函数。

​ 通过以上内容,我们可以知道在使用基类指针调用虚函数的时候,它能够根据所指的类对象的不同来正确调用虚函数。而这些能够正常工作,得益于虚指针和虚函数表的引入,使得在程序运行期间能够动态调用函数。

动态绑定有以下三项条件要符合:

  1. 使用指针进行调用
  2. 指针属于up-cast后的
  3. 调用的是虚函数

与动态绑定相对应的是静态绑定,它属于编译的时候就确定下来的,如上文的非虚函数,他们是类对象直接可调用的,而不需要任何查表操作,因此调用的速度也快于虚函数。

https://zhuanlan.zhihu.com/p/98776075

你可能感兴趣的:(C++基础,c++)