最終更新:2026-10-12
C++ 仮想ポインタと仮想テーブル機構¶
著者について:¶
個人公式アカウント(WeChat):

1.基礎理論¶
仮想関数を実現するため、C++は仮想テーブルと呼ばれる特殊な形式の遅延バインディングを使用します。この仮想テーブルは、動的/遅延バインディング方式での関数呼び出しを解決するためのルックアップテーブルです。仮想テーブルは、「vtable」「仮想関数テーブル」「仮想メソッドテーブル」「ディスパッチテーブル」など、他の名前で呼ばれることもあります。
仮想テーブルは実際には非常にシンプルですが、文章で説明するのは少し複雑です。まず、仮想関数を使用する各クラス(または仮想関数を使用するクラスから派生したクラス)は、それぞれ独自の仮想テーブルを持ちます。このテーブルは、コンパイラがコンパイル時に設定する静的配列にすぎません。仮想テーブルには、そのクラスのオブジェクトが呼び出せる各仮想関数のエントリが1つずつ含まれます。このテーブルの各エントリは単なる関数ポインタであり、そのクラスがアクセス可能な派生関数を指しています。
次に、コンパイラは基底クラスを指す隠れたポインタも追加します。これをvptrと呼びます。vptrはクラスのインスタンスが作成されるとき、そのクラスの仮想テーブルを指すように自動的に設定されます。実際にはコンパイラが自己参照を解決するために使う関数引数であるthisポインタとは異なり、vptrは本物のポインタです。
そのため、各クラスオブジェクトの割り当てはポインタ1つ分大きくなります。また、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++那些事