2023年10月

C++虚指针和虚函数表

C++实现多态的方式是通过虚指针和虚函数表

具体来说,当访问一个虚函数时,通过访问vptr,再加上一个偏移获取函数的指针。

子类的vtable相当于父类vtable的拷贝,然后替换掉表中被重写的虚函数的地址为子类的该函数地址。

image-20231006224237967.png

代码

#include<iostream>
#include<vector>

class A
{
public:
    A(){
        Print();
    }
    virtual void FuncA(){}
    virtual void Print()
    {
        uint64_t* vptr = (uint64_t*)(*((uint64_t*)this)); // 取虚指针
        std::cout << "A::vptr:      " << vptr <<  std::endl; // 输出虚指针
        std::cout << "A::FuncA:    " << (void*)(&A::FuncA) << std::endl; // 输出A::FuncA地址
        std::cout << "A::Print:    " << (void*)(&A::Print) << std::endl; // 输出A::Print地址
        std::cout << "A::vtable[0]: " << (void*)(vptr[0]) << std::endl;  // 输出虚函数表第一个函数的地址
        std::cout << "A::vtable[1]: " << (void*)(vptr[1]) << std::endl;  // 输出虚函数表第二个函数的地址
    }
};

class B: public A
{
public:
    B():A(){
        Print();
    }
    virtual void Print()
    {
        uint64_t* vptr = (uint64_t*)(*((uint64_t*)this));
        std::cout << "B::vptr:      " << vptr <<  std::endl;
        std::cout << "B::FuncA:     " << (void*)(&B::FuncA) << std::endl;
        std::cout << "B::Print:     " << (void*)(&B::Print) << std::endl;
        std::cout << "B::vtable[0]: " << (void*)(vptr[0]) << std::endl;
        std::cout << "B::vtable[1]: " << (void*)(vptr[1]) << std::endl;
    }
};

int main()
{
    B b;
    return 0;
}

输出结果

A::vptr:      0x4020d8
A::FuncA:     0x40121c
A::Print:     0x401228
A::vtable[0]: 0x40121c
A::vtable[1]: 0x401228
B::vptr:      0x4020b8
B::FuncA:     0x40121c
B::Print:     0x40135a
B::vtable[0]: 0x40121c
B::vtable[1]: 0x40135a

子类执行父类构造函数时能正确调用父类的虚函数

A::vptr: 0x4020d8

B::vptr: 0x4020b8

先执行A的构造函数,此时虚指针会被赋值为A类的虚指针,调用的为父类的虚函数

再执行B的构造函数,此时虚指针会被赋值为B类的虚指针,调用的为子类重写的虚函数

即使重写了父类的虚函数,在执行父类构造函数时调用虚函数,调用的为父类的虚函数而不是子类。保证虚函数能够正确被调用

单继承情况下,vptr验证

A::vptr: 0x4020d8

B::vptr: 0x4020b8

头个8字节为虚指针,子类的虚指针不同于父类的虚指针

A::FuncA: 0x401228
A::Print: 0x401234
A::vtable[0]: 0x401228
A::vtable[1]: 0x401234

B::FuncA: 0x401228
B::Print: 0x401366
B::vtable[0]: 0x401228
B::vtable[1]: 0x401366

虚指针指向vtable,函数地址与定义顺序一致

从虚函数地址表可以看出,没被重写的方法函数地址没有变化,被重写的函数地址发生了变化