Version 17.11
仮想関数テーブルって?
「前回は仮想関数について説明しました」
『わかんねー! なんかいみふめーな感じだったんだけど』
「そこで、もうちょっと詳しく説明します。まず、シンプルにして継承を使
わない場合から説明します」
// Data.h
// CData クラス。
class CData
{
public:
virtual void Output1();
virtual void Output2();
};
// Data.cpp
#include <Windows.h>
#include <stdio.h>
#include "Data.h"
// 基本クラスの Output1() メンバ関数。
void CData::Output1()
{
OutputDebugString( "CData::Output1()\n" );
}
// 基本クラスの Output2() メンバ関数。
void CData::Output2()
{
OutputDebugString( "CData::Output2()\n" );
}
// Main.cpp
#include <Windows.h>
#include <stdio.h>
#include "Data.h"
int WINAPI WinMain
( HINSTANCE p_hInstance
, HINSTANCE p_hPrevInstance
, LPSTR p_pchCmdLine
, int p_iCmdShow
)
{
CData cData;
return 0; // ここにブレークポイントをセット。
}
「 CData クラスだけにして、仮想関数を2つにしました」
『あれ? 仮想関数って継承と関係あるんじゃないの?』
「実際には関係あるんだけど、まずは分かりやすくってことで。この時、
CData メンバ変数を見てみるとこうなっています」
- CData {...}
- __vfptr 0x0042da24 const CData::`vftable'
[0] 0x0040105a CData::Output1(void)
[1] 0x0040105f CData::Output2(void)
『あ、なんだか配列っぽくなった』
「図にするとこうなります」
┌────┐
│ cData │
│ │
│ __vfptr→→→
└────┘ ↓
↓
←←←←
↓
↓
vftable
┌──────────────┐
0│CData::Output1()のアドレス │
├──────────────┤
1│CData::Output2()のアドレス │
└──────────────┘
「cData 変数内に、 __vfptr というメンバ変数が自動的に作られます。こ
のメンバ変数はポインタで、外にある vftable という変数のアドレスを格納
しています」
『で、 vftable はメンバ関数の配列になっていると』
「そう。 vftable は【仮想関数テーブル】って言って、仮想関数、つまり
virtual の付いたメンバ関数の、アドレスの配列になっているんです」
『だから、さっきの Output1() と Output2() が書いてあるってことね』
「その前に書いてある 0x0040105a とかが実際のアドレスです」
『このアドレスって、関数ポインタの時に使った関数のアドレス?』
「そう、 Version 13.17 ( No.253 ) で説明した、関数ポインタが配列に
なったもの、って考えると分かりやすいかも」
『だからそれぞれ、メンバ関数のポインタが入ってるわけね』
「さて、ここまでが仮想関数テーブルの基本」
『ってゆーかここまでじゃメリット分からないし』
「メリットについてはもう少し先で……次に継承とオーバーライドを使用し
た場合の説明をします。さっきのプログラムで、 Output2() メンバ関数だけ
オーバーライドします」
// Data.h
// CData クラス。
class CData
{
public:
virtual void Output1();
virtual void Output2();
};
// CData クラスの派生クラス。
class CDerivedData : public CData
{
// オーバーライドします。
void Output2();
};
// Data.cpp
#include <Windows.h>
#include <stdio.h>
#include "Data.h"
// 基本クラスの Output1() メンバ関数。
void CData::Output1()
{
OutputDebugString( "CData::Output1()\n" );
}
// 基本クラスの Output2() メンバ関数。
void CData::Output2()
{
OutputDebugString( "CData::Output2()\n" );
}
// 派生クラスの Output2() メンバ関数。
// オーバーライドしています。
void CDerivedData::Output2()
{
OutputDebugString( "CDerivedData::Output2()\n" );
}
// Main.cpp
#include <Windows.h>
#include <stdio.h>
#include "Data.h"
int WINAPI WinMain
( HINSTANCE p_hInstance
, HINSTANCE p_hPrevInstance
, LPSTR p_pchCmdLine
, int p_iCmdShow
)
{
// CDerivedData クラスを使用します。
CDerivedData cDerivedData;
return 0; // ここにブレークポイントをセット。
}
『うん、 Output2() メンバ関数だけオーバーライドしてる』
「このとき、ブレークポイントで cDerivedData 変数の中身を見ると、以下
のようになっています」
- cDerivedData {...}
- CData {...}
- __vfptr 0x0042c06c const CDerivedData::`vftable'
[0] 0x0040100f CData::Output1(void)
[1] 0x00401005 CDerivedData::Output2(void)
『あれ……あれれ? Output2() メンバ関数が、 CDerivedData クラスで
オーバーライドした方になってる!』
「図にするとこうなります」
┌───────┐
│ cDerivedData │
│ ┌────┐ │
│ │ cData │ │
│ │ │ │
│ │ __vfptr→→→→→
│ └────┘ │ ↓
└───────┘ ↓
↓
←←←←←←←←
↓
↓
vftable
┌──────────────────┐
0│CData::Output1()のアドレス │
├──────────────────┤
1│CDerivedData::Output2()のアドレス │
└──────────────────┘
『なんで、なんでなんで?』
「なぜかというと、この仮想関数テーブルには、必ずオーバーライドした
メンバ関数のアドレスが格納されるようになってるんです」
『オーバーライドした方……つまり、 CData::Output2() は
CDerivedData::Output2() でオーバーライドしてるから、
CDerivedData::Output2() のアドレスだけが書かれてるってこと?』
「そういうこと。仮想関数テーブルには各仮想関数のアドレスが書かれるわ
けど、その際、オーバーライドしたメンバ関数があると、そのメンバ関数の
アドレスで上書きされるんです」
『ん、なんだかそれはオーバーライドらしいかも』
「というわけで、次回はいよいよポリモーフィズムについて説明します!」
/*
Preview Next Story!
*/
「次回はいよいよポリモーフィズムについてです」
『難しい? 簡単?』
「面白くて、不思議」
『意味わかんねー!』
「というわけで次回」
< Version 17.12 ポリモーフィズム! >
『につづく!』
「これが面白いって思うかどうかが」
『人としてどうかの瀬戸際ね』