Skip to content

Last updated:2026-10-12

C++ Virtual Pointer and vtable Mechanism

About the Author:

Personal WeChat official account:

wechat

1. Basic Theory

To implement virtual functions, C++ uses a special form of late binding known as the virtual table. The virtual table is a lookup table used to resolve function calls in a dynamic/late-bound fashion. The virtual table goes by other names too, such as "vtable", "virtual function table", "virtual method table", or "dispatch table".

The virtual table is actually quite simple, even if it is a little complicated to describe in words. First, every class that uses virtual functions (or is derived from a class that uses virtual functions) has its own virtual table. This table is simply a static array the compiler sets up at compile time. The virtual table contains one entry for each virtual function that can be called by objects of the class. Each entry in this table is just a function pointer pointing to the most-derived function accessible to that class.

Second, the compiler also adds a hidden pointer to the base class, which we call vptr. The vptr is set automatically when an instance of the class is created, so that it points to the virtual table for that class. Unlike the this pointer, which is really a function parameter the compiler uses to resolve self-reference, the vptr is a real pointer.

Consequently, it makes every class object bigger by one pointer. It also means the vptr is inherited by derived classes, which is important.

2. Implementation and Internal Structure

Now let's look at automatically and manually manipulating the vptr to obtain addresses and invoke virtual functions!

Before looking at the code, here is a call diagram to make things easier to understand:

base

The code all follows the standard commenting style; you will understand it at a glance, and if not, feel free to leave a comment!

/**
 * @file vptr1.cpp
 * @brief C++虚函数vptr和vtable
 * 编译:g++ -g -o vptr vptr1.cpp -std=c++11
 * @author 光城
 * @version v1
 * @date 2019-07-20
 */

#include <iostream>
#include <stdio.h>
using namespace std;

/**
 * @brief 函数指针
 */
typedef void (*Fun)();

/**
 * @brief 基类
 */
class Base
{
    public:
        Base(){};
        virtual void fun1()
        {
            cout << "Base::fun1()" << endl;
        }
        virtual void fun2()
        {
            cout << "Base::fun2()" << endl;
        }
        virtual void fun3(){}
        ~Base(){};
};

/**
 * @brief 派生类
 */
class Derived: public Base
{
    public:
        Derived(){};
        void fun1()
        {
            cout << "Derived::fun1()" << endl;
        }
        void fun2()
        {
            cout << "DerivedClass::fun2()" << endl;
        }
        ~Derived(){};
};
/**
 * @brief 获取vptr地址与func地址,vptr指向的是一块内存,这块内存存放的是虚函数地址,这块内存就是我们所说的虚表
 *
 * @param obj
 * @param offset
 *
 * @return 
 */
Fun getAddr(void* obj,unsigned int offset)
{
    cout<<"======================="<<endl;
    void* vptr_addr = (void *)*(unsigned long *)obj;  //64位操作系统,占8字节,通过*(unsigned long *)obj取出前8字节,即vptr指针
    printf("vptr_addr:%p\n",vptr_addr);

    /**
     * @brief 通过vptr指针访问virtual table,因为虚表中每个元素(虚函数指针)在64位编译器下是8个字节,因此通过*(unsigned long *)vptr_addr取出前8字节,
     * 后面加上偏移量就是每个函数的地址!
     */
    void* func_addr = (void *)*((unsigned long *)vptr_addr+offset);
    printf("func_addr:%p\n",func_addr);
    return (Fun)func_addr;
}
int main(void)
{
    Base ptr;
    Derived d;
    Base *pt = new Derived(); // 基类指针指向派生类实例
    Base &pp = ptr; // 基类引用指向基类实例
    Base &p = d; // 基类引用指向派生类实例
    cout<<"基类对象直接调用"<<endl;
    ptr.fun1();
    cout<<"基类引用指向基类实例"<<endl;
    pp.fun1(); 
    cout<<"基类指针指向派生类实例并调用虚函数"<<endl;
    pt->fun1();
    cout<<"基类引用指向派生类实例并调用虚函数"<<endl;
    p.fun1();

    // 手动查找vptr 和 vtable
    Fun f1 = getAddr(pt, 0);
    (*f1)();
    Fun f2 = getAddr(pt, 1);
    (*f2)();
    delete pt;
    return 0;
}

Run result:

基类对象直接调用
Base::fun1()
基类引用指向基类实例
Base::fun1()
基类指针指向派生类实例并调用虚函数
Derived::fun1()
基类引用指向派生类实例并调用虚函数
Derived::fun1()
=======================
vptr_addr:0x401130
func_addr:0x400ea8
Derived::fun1()
=======================
vptr_addr:0x401130
func_addr:0x400ed4
DerivedClass::fun2()

We observe that C++'s dynamic polymorphism is achieved through virtual functions. Simply put, via the virtual mechanism, a base-class pointer pointing to a subclass can call the subclass's functions. For example, above, a base-class pointer points to a derived-class instance and calls a virtual function; let's simplify the code above to:

Base *pt = new Derived(); // 基类指针指向派生类实例
cout<<"基类指针指向派生类实例并调用虚函数"<<endl;
pt->fun1();

The process is as follows: first, the program recognizes that fun1() is a virtual function; second, it uses pt->vptr to fetch Derived's virtual table; third, it looks up which version of fun1() to call in Derived's virtual table. Here we can see that Derived::fun1() is the one called. Therefore pt->fun1() is resolved to Derived::fun1()!

Beyond that, as you can see in the code above, it also includes manually obtaining the vptr address and calling a function in the vtable. So let's verify together whether the address above matches the one really used when the vtable's virtual function is invoked automatically — for example, when pt->fun1() runs above!

Here we use gdb debugging; remember to add -g when compiling.

Enter the gdb debugging screen with gdb vptr, then type b Derived::fun1 to set a breakpoint on fun1, and then type r to run the program to the breakpoint. At this point we need to inspect the memory addresses in the call stack: disassemable fun1 shows the relevant assembly code for fun1, where we see 0x0000000000400ea8; comparing this with the result above, we find it matches the manually invoked fun1, and fun2 is similar, proving the code is correct!

The gdb debugging information is as follows:

(gdb) b Derived::fun1
Breakpoint 1 at 0x400eb4: file vptr1.cpp, line 23.
(gdb) r
Starting program: /home/light/Program/CPlusPlusThings/virtual/pure_virtualAndabstract_class/vptr 
基类对象直接调用
Base::fun1()
基类引用指向派生类实例
Base::fun1()
基类指针指向派生类实例并调用虚函数

Breakpoint 1, Derived::fun1 (this=0x614c20) at vptr1.cpp:23
23              cout << "Derived::fun1()" << endl;
(gdb) disassemble fun1
Dump of assembler code for function Derived::fun1():
   0x0000000000400ea8 <+0>: push   %rbp
   0x0000000000400ea9 <+1>: mov    %rsp,%rbp
   0x0000000000400eac <+4>: sub    $0x10,%rsp
   0x0000000000400eb0 <+8>: mov    %rdi,-0x8(%rbp)
=> 0x0000000000400eb4 <+12>:    mov    $0x401013,%esi
   0x0000000000400eb9 <+17>:    mov    $0x602100,%edi
   0x0000000000400ebe <+22>:    callq  0x4009d0 <_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@plt>
   0x0000000000400ec3 <+27>:    mov    $0x400a00,%esi
   0x0000000000400ec8 <+32>:    mov    %rax,%rdi
   0x0000000000400ecb <+35>:    callq  0x4009f0 <_ZNSolsEPFRSoS_E@plt>
   0x0000000000400ed0 <+40>:    nop
   0x0000000000400ed1 <+41>:    leaveq 
   0x0000000000400ed2 <+42>:    retq   
End of assembler dump.
(gdb) disassemble fun2
Dump of assembler code for function Derived::fun2():
   0x0000000000400ed4 <+0>: push   %rbp
   0x0000000000400ed5 <+1>: mov    %rsp,%rbp
   0x0000000000400ed8 <+4>: sub    $0x10,%rsp
   0x0000000000400edc <+8>: mov    %rdi,-0x8(%rbp)
   0x0000000000400ee0 <+12>:    mov    $0x401023,%esi
   0x0000000000400ee5 <+17>:    mov    $0x602100,%edi
   0x0000000000400eea <+22>:    callq  0x4009d0 <_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@plt>
   0x0000000000400eef <+27>:    mov    $0x400a00,%esi
   0x0000000000400ef4 <+32>:    mov    %rax,%rdi
   0x0000000000400ef7 <+35>:    callq  0x4009f0 <_ZNSolsEPFRSoS_E@plt>
   0x0000000000400efc <+40>:    nop
   0x0000000000400efd <+41>:    leaveq 
   0x0000000000400efe <+42>:    retq   
End of assembler dump.

Comments