Última actualización:2026-10-12
Mecanismo de puntero virtual y tabla virtual en C++¶
Sobre el autor:¶
Cuenta pública personal:

1. Teoría básica¶
Para implementar las funciones virtuales, C++ usa una forma especial de enlace tardío denominada tabla virtual. Dicha tabla virtual es una tabla de búsqueda que sirve para resolver, mediante enlace dinámico/tardío, las llamadas a funciones. La tabla virtual recibe a veces otros nombres, como "vtable", "tabla de funciones virtuales", "tabla de métodos virtuales" o "tabla de despacho".
La tabla virtual es en realidad muy sencilla, aunque describirla con palabras sea un poco complicado. Primero, cada clase que usa funciones virtuales (o que deriva de una clase que usa funciones virtuales) tiene su propia tabla virtual. Esa tabla es simplemente un arreglo estático que el compilador construye en tiempo de compilación. La tabla virtual contiene una entrada por cada función virtual que puede ser invocada por los objetos de la clase. Cada entrada de esta tabla es solo un puntero a función, que apunta a la función derivada más accesible para esa clase.
Segundo, el compilador añade también un puntero oculto hacia la clase base, al que llamamos vptr. El vptr se establece automáticamente al crear la instancia de la clase, de modo que apunte a la tabla virtual de esa clase. A diferencia del puntero this, que en realidad es un parámetro de función que el compilador usa para resolver la autorreferencia, el vptr es un puntero real.
En consecuencia, cada objeto de la clase pasa a ocupar un tamaño mayor en un puntero. Esto significa también que el vptr se hereda en las clases derivadas, lo cual es muy importante.
2. Implementación y estructura interna¶
¡Veamos ahora cómo obtener direcciones e invocar funciones virtuales manipulando el vptr de forma automática y manual!
Antes de empezar a mirar el código, para facilitar la comprensión, se muestra el grafo de llamadas:

Todo el código sigue el estilo de comentarios estándar; seguro que al verlo se entiende, y si no, ¡puedes dejar un mensaje!
/**
* @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 de la ejecución:
基类对象直接调用
Base::fun1()
基类引用指向基类实例
Base::fun1()
基类指针指向派生类实例并调用虚函数
Derived::fun1()
基类引用指向派生类实例并调用虚函数
Derived::fun1()
=======================
vptr_addr:0x401130
func_addr:0x400ea8
Derived::fun1()
=======================
vptr_addr:0x401130
func_addr:0x400ed4
DerivedClass::fun2()
Comprobamos que el polimorfismo dinámico de C++ se implementa mediante funciones virtuales. Dicho simplemente: gracias a las funciones virtual, un puntero a la clase base que apunta a una subclase puede invocar funciones de la subclase. Por ejemplo, arriba un puntero a la clase base apunta a una instancia de la clase derivada e invoca a la función virtual; simplificando ese código:
El proceso es el siguiente: primero, el programa reconoce que fun1() es una función virtual; segundo, el programa usa pt->vptr para obtener la tabla virtual de Derived; tercero, busca en la tabla virtual de Derived qué versión de fun1() invocar. Aquí se comprueba que la invocada es Derived::fun1(). Por eso, pt->fun1() se resuelve como Derived::fun1()!
Aparte de esto, en el código anterior también se ve la obtención manual de la dirección del vptr y la invocación de las funciones de la vtable; verifiquemos juntos si esas direcciones coinciden con las usadas realmente en la invocación automática de las funciones virtuales de la vtable, por ejemplo en el pt->fun1() anterior!
Aquí se emplea la depuración con gdb; al compilar, recuerda añadir -g.
Con gdb vptr se entra en la pantalla de depuración de gdb; luego se escribe b Derived::fun1 para poner un punto de interrupción en fun1 y, ejecutando con r, el programa se detiene en el punto de interrupción; en ese momento necesitamos consultar las direcciones de memoria de la pila de llamadas: con disassemable fun1 se puede ver el código ensamblador relacionado con fun1, donde aparece 0x0000000000400ea8; comparándolo con el resultado anterior se descubre que coincide con la invocación manual de fun1, y lo mismo ocurre con fun2, lo que demuestra que el código es correcto!
La información de depuración de gdb es la siguiente:
(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.
Temas relacionados¶
- Funciones virtuales puras y clases abstractas en C++ | C++那些事
- Aserciones assert en C++ | C++那些事
- Campos de bits en C++ | C++那些事
- Implementar el polimorfismo de C++ en lenguaje C | C++那些事
- const en C++: constantes y modificación | C++那些事
- La familia completa de const en C++ | C++那些事
- El operador de desreferencia * en C++ | C++那些事
- Inicialización y asignación en C++ | C++那些事
- Formas de devolver varios valores en C++ | C++那些事
- La palabra clave mutable en C++ | C++那些事