Ir para o conteúdo

Última atualização:2026-10-12

O mecanismo de vptr e vtable em C++

Sobre o autor:

Conta oficial do WeChat:

wechat

1.Teoria básica

Para implementar funções virtuais, o C++ usa uma forma especial de ligação tardia chamada tabela virtual. Essa tabela virtual é uma tabela de consulta usada para resolver, de forma dinâmica/tardia, as chamadas de funções. A tabela virtual às vezes recebe outros nomes, como "vtable", "tabela de funções virtuais", "tabela de métodos virtuais" ou "tabela de despacho".

A tabela virtual, na verdade, é bastante simples, embora descrevê-la em palavras seja um pouco complexo. Primeiro, cada classe que usa funções virtuais (ou que deriva de uma classe que usa funções virtuais) tem a sua própria tabela virtual. Essa tabela é apenas um array estático que o compilador configura em tempo de compilação. A tabela virtual contém uma entrada para cada função virtual que pode ser chamada pelos objetos da classe. Cada entrada dessa tabela é apenas um ponteiro de função, apontando para a função derivada acessível por essa classe.

Em segundo lugar, o compilador também adiciona um ponteiro oculto para a tabela da classe, que chamamos de vptr. O vptr é configurado automaticamente quando a instância da classe é criada, de modo a apontar para a tabela virtual dessa classe. Diferente do ponteiro this — que, na verdade, é um parâmetro de função que o compilador usa para resolver autorreferências —, o vptr é um ponteiro de verdade.

Assim, cada objeto da classe passa a ter uma alocação maior em um tamanho de ponteiro. Isso também significa que o vptr é herdado pela classe derivada, o que é muito importante.

2.Implementação e estrutura interna

Agora vejamos como obter endereços e chamar funções virtuais manipulando o vptr automática e manualmente!

Antes de olhar o código, para facilitar a compreensão, apresentamos o diagrama de chamadas:

base

Todo o código segue o padrão de comentários; acredito que, ao ler, vocês entenderão; caso contrário, deixem um comentário!

/**
 * @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;
}

Resultado da execução:

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

Descobrimos que o polimorfismo dinâmico do C++ é implementado por meio de funções virtuais. Em suma: com funções virtual, um ponteiro da classe base apontando para a subclasse consegue chamar as funções da subclasse. Por exemplo, no caso acima, o ponteiro da classe base aponta para a instância da classe derivada e chama a função virtual; simplificando o código, temos:

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

O processo é o seguinte: primeiro, o programa reconhece que fun1() é uma função virtual; em seguida, usa pt->vptr para obter a tabela virtual de Derived; em terceiro lugar, procura na tabela virtual de Derived qual versão de fun1() chamar. Aqui se descobre que a chamada é Derived::fun1(). Portanto, pt->fun1() é resolvido como Derived::fun1()!

Além disso, vocês notarão no código acima que ele também inclui a obtenção manual do endereço do vptr e a chamada das funções da vtable; vamos então verificar, juntos, se esses endereços coincidem com os usados de fato na chamada automática das funções virtuais da vtable — por exemplo, no pt->fun1() acima!

Aqui usamos a depuração com gdb; lembre-se de adicionar -g ao compilar.

Entre na tela de depuração com gdb vptr, digite b Derived::fun1 para colocar um breakpoint em fun1 e, em seguida, digite r para executar o programa até o breakpoint; agora precisamos consultar o endereço de memória na pilha de chamadas; com disassemable fun1 é possível ver o código assembly relacionado a fun1; vemos 0x0000000000400ea8 e, comparando com o resultado acima, descobrimos que ele é igual ao da chamada manual de fun1; o mesmo vale para fun2, provando assim a corretude do código!

As informações de depuração do gdb são as seguintes:

(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.

Tópicos relacionados

Comentários