0. 背景知识

虚函数表不必多说,是每位C++学习者都要面对的问题。简单来说,这是C++面向对象的基础,保证子类指针转换为父类指针类型后,调用成员函数时仍然是子类的成员函数。听上去有点绕,但如果这一部分理解不了,还请在继续阅读前,先去学习一下虚函数相关内容。

虚函数表大家都知道,是存放每个对象所有虚函数的指针的一块内存区域,指针指向真正的虚函数代码。《深入理解C++对象模型》一书中讲到:

  1. 同一个Class下的所有对象共享同一个虚函数表;
  2. 子类会继承父类的虚函数表(并不是直接使用,而是复制一份),并在此基础上做拓展;
  3. 带有虚函数表的对象大小最小(空对象)为32 or 64 bit (4 or 8 Byte),也就是虚函数表指针的大小;与之相反的,无虚函数的空对象最小为1 Byte作为占位;
  4. 调用虚函数时,相比直接调用成员函数多了一步,首先是通过对象头的虚函数表地址找到对应虚函数地址,然后才是调用对应的虚函数。

有必要备注一下:第3条虽然书中说是编译器相关的,但经过测试发现,Clang/GCC/MSVC都是这么做的,因此可以视这为现行标准之一。

说了这么多,虚函数表有什么可以玩的东西呢?先别急,我们再看一下overridefinal关键词。

在成员函数声明中,override关键词表示覆写父类虚函数,且可以继续被后续子类覆写;而final关键词表示覆写父类虚函数,且禁止被后续子类覆写。这两个关键词存在时,均暗含了“此函数为virtual”的意思,因此额外声明一个“virtual”时IDE会提示冗余。

那么我们开始和虚函数表玩耍吧,玩什么呢?交换对象的虚函数表

1. 示例代码

示例代码可以在64位机型上直接运行,且尽可能简化以突出主题,希望读者如果有不清楚的地方,最好还是自己亲自尝试一下。

QQ20200419-134732@2x.png

#include <cstdio>
#include <cstdint> // 引入uint64_t

// 我们从定义三个类开始,分别为A,B,C,且B继承A,C继承B
class A
{
public:
    virtual void Print() { printf("p - A, "); }
    virtual void PrintWithOverride() { printf("po - A, "); }
    virtual void PrintWithFinal() { printf("pf - A\r\n"); }
};
class B : public A
{
public:
    virtual void Print() { printf("p - B, "); }
    virtual void PrintWithOverride() override { printf("po - B, "); }
    void PrintWithFinal() override { printf("pf - B\r\n"); }
};
class C : public B
{
public:
    void Print() { printf("p - C, "); }
    void PrintWithOverride() override { printf("po - C, "); }
    void PrintWithFinal() final { printf("pf - C\r\n"); }
};



int main()
{
    A *a = new A();
    B *b = new B();
    C *c = new C();

    // 获取虚函数表指针
    uint64_t avpt = *(uint64_t *) a;
    uint64_t bvpt = *(uint64_t *) b;
    uint64_t cvpt = *(uint64_t *) c;


    // 打印每个类里,虚函数表和三个虚函数的指针
    printf("VPT & VFuncs: \r\n");
    printf("A: %p - %p, %p, %p\r\n", avpt, *(uint64_t *)avpt,*((uint64_t *)avpt+1),*((uint64_t *)avpt+2));
    printf("B: %p - %p, %p, %p\r\n", bvpt, *(uint64_t *)bvpt,*((uint64_t *)bvpt+1),*((uint64_t *)bvpt+2));
    printf("C: %p - %p, %p, %p\r\n", cvpt, *(uint64_t *)cvpt,*((uint64_t *)cvpt+1),*((uint64_t *)cvpt+2));

    // 打印正常情况下的输出
    printf("\r\nBefore swap:\r\n");
    printf("A: "); a->Print(); a->PrintWithOverride(); a->PrintWithFinal();
    printf("B: "); b->Print(); b->PrintWithOverride(); b->PrintWithFinal();
    printf("C: "); c->Print(); c->PrintWithOverride(); c->PrintWithFinal();
    printf("(A)B: "); ((A*)b)->Print(); ((A*)b)->PrintWithOverride(); ((A*)b)->PrintWithFinal();
    printf("(A)C: "); ((A*)c)->Print(); ((A*)c)->PrintWithOverride(); ((A*)c)->PrintWithFinal();
    /*
     * 输出:
        Before swap:
        A: p - A, po - A, pf - A
        B: p - B, po - B, pf - B
        C: p - C, po - C, pf - C
        (A)B: p - B, po - B, pf - B
        (A)C: p - C, po - C, pf - C
     */



    /* Magic: 我们在这里交换三个对象的虚函数表
     * C -> A
     * A -> B
     * B -> C     */
    *(uint64_t *) a = cvpt;
    *(uint64_t *) b = avpt;
    *(uint64_t *) c = bvpt;
    printf("Swap:\r\nA <- C\r\nB <- A\r\nC <- B\r\n");

    // 打印并测试交换完成后的结果
    printf("After swap:\r\n");
    printf("A: "); a->Print(); a->PrintWithOverride(); a->PrintWithFinal();
    printf("B: "); b->Print(); b->PrintWithOverride(); b->PrintWithFinal();
    printf("C: "); c->Print(); c->PrintWithOverride(); c->PrintWithFinal();
    printf("(C)A: "); ((C*)a)->Print(); ((C*)a)->PrintWithOverride(); ((C*)a)->PrintWithFinal();
    printf("(B)C: "); ((B*)c)->Print(); ((B*)c)->PrintWithOverride(); ((B*)c)->PrintWithFinal();
    /*
     * 输出:
        After swap:
        A: p - C, po - C, pf - C
        B: p - A, po - A, pf - A
        C: p - B, po - B, pf - C
        (C)A: p - C, po - C, pf - C
        (B)C: p - B, po - B, pf - C
     * 这里会发现:B->C之后,C打印出来的pf仍然为C
     * 尽管交换虚表这一操作完全是未定义的行为,但我们可以注意到,
     * 编译器在处理final时使用了特殊的方式。
     */

    /*
     * 那我们使用直接从虚表中调用PF会怎么样呢?
     * 值得注意的是,这里如果打印虚函数的取地址,取到的实际是虚函数在表中的索引值
     */
    typedef void (C::*VFP)();

    VFP cpf=(VFP)&C::PrintWithFinal;
    printf("\r\n%d",&C::PrintWithFinal);
    printf("\r\nCall C::PF via VT reference: \r\n");
    (c->*cpf)();
    /*
     * 输出:
        17
        Call C::PF via VT reference:
        pf - B
     */

    return 0;
}

Last modification:April 19th, 2020 at 01:49 pm
本文作者:灰格猫

本文链接:如何使用C++与虚函数表愉快地玩耍并掉进坑里 - https://graueneko.com/archives/84/

版权声明:如无特别声明,本文即为原创文章,仅代表个人观点,版权归 灰格猫的编程日记 所有,未经允许不得转载。