Последнее обновление:2026-10-12
C++ механизм vptr и vtable¶
Об авторе:¶
Личный официальный аккаунт:

1.Базовая теория¶
Для реализации виртуальных функций C++ использует особую форму позднего связывания, называемую виртуальной таблицей. Эта виртуальная таблица — таблица поиска, служащая для разрешения вызовов функций в динамическом/позднем режиме связывания. Виртуальная таблица иногда носит и другие названия: «vtable», «таблица виртуальных функций», «таблица виртуальных методов» или «таблица диспетчеризации».
На самом деле виртуальная таблица очень проста, хотя описать её словами несколько сложнее. Во-первых, каждый класс, использующий виртуальные функции (или порождённый от класса, использующего виртуальные функции), имеет собственную виртуальную таблицу. Эта таблица — просто статический массив, который компилятор создаёт во время компиляции. Виртуальная таблица содержит по одному пункту на каждую виртуальную функцию, которую может вызвать объект класса. Каждый пункт этой таблицы — просто указатель на функцию, указывающий на доступную классу производную функцию.
Во-вторых, компилятор также добавляет скрытый указатель на базовый класс, который мы называем vptr. vptr устанавливается автоматически при создании экземпляра класса так, чтобы указывать на виртуальную таблицу этого класса. В отличие от указателя this, который фактически является параметром функции, используемым компилятором для разрешения ссылок на самого себя, vptr — настоящий указатель.
Поэтому каждый объект класса становится больше на размер одного указателя. Это также означает, что vptr наследуется производным классом, и это важно.
2.Реализация и внутреннее устройство¶
Теперь посмотрим, как автоматически и вручную манипулировать vptr, чтобы получать адреса и вызывать виртуальные функции!
Прежде чем смотреть код, для удобства понимания приведём схему вызовов:

Весь код следует стандартному стилю комментариев — уверен, посмотрев, вы поймёте; если нет, пишите в комментариях!
/**
* @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;
}
Результат выполнения:
基类对象直接调用
Base::fun1()
基类引用指向基类实例
Base::fun1()
基类指针指向派生类实例并调用虚函数
Derived::fun1()
基类引用指向派生类实例并调用虚函数
Derived::fun1()
=======================
vptr_addr:0x401130
func_addr:0x400ea8
Derived::fun1()
=======================
vptr_addr:0x401130
func_addr:0x400ed4
DerivedClass::fun2()
Мы обнаруживаем, что динамический полиморфизм C++ реализуется через виртуальные функции. Проще говоря, благодаря функции virtual базовый указатель, указывающий на подкласс, может вызывать функции подкласса. Например, выше базовый указатель указывает на экземпляр производного класса и вызывает виртуальную функцию; упростим этот код до:
Процесс таков: сначала программа распознаёт, что fun1() — виртуальная функция; затем программа использует pt->vptr для получения виртуальной таблицы Derived; в-третьих, она ищет в виртуальной таблице Derived, какая версия fun1() должна быть вызвана. Здесь видно, что вызывается Derived::fun1(). Поэтому pt->fun1() разрешается в Derived::fun1()!
Кроме того, в приведённом коде вы видите и ручное получение адреса vptr с вызовом функций из vtable; давайте вместе проверим, совпадают ли эти адреса с теми, что реально используются при автоматическом вызове виртуальных функций из vtable — например, при описанном выше pt->fun1()!
Здесь используется отладка gdb; при компиляции не забудьте добавить -g.
Введите gdb vptr, чтобы войти в отладчик gdb, затем введите b Derived::fun1, чтобы поставить точку останова на fun1, а затем запустите программу до точки останова командой r; теперь нам нужно посмотреть адреса памяти в стеке вызовов: с помощью disassemable fun1 можно просмотреть соответствующий ассемблерный код для fun1 — мы видим 0x0000000000400ea8; сравнив с результатом выше, обнаружим, что он совпадает с вручную вызванным fun1; с fun2 аналогично — это доказывает корректность кода!
Отладочная информация gdb:
(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.
Похожие темы¶
- C++ чисто виртуальные функции и абстрактные классы | C++那些事
- C++ утверждения assert | C++那些事
- C++ битовые поля | C++那些事
- Реализация полиморфизма C++ на языке C | C++那些事
- C++ константы const и его модификация | C++那些事
- C++ всё семейство const | C++那些事
- C++ оператор разыменования * | C++那些事
- C++ инициализация и присваивание | C++那些事
- C++ возврат нескольких значений | C++那些事
- C++ ключевое слово mutable | C++那些事