単一継承条件での虚関数のリスト


子クラスが親クラスの虚関数を実現した場合、親クラスのポインタで子クラスの虚関数にアクセスできる鍵は、コンパイル時に子クラスの虚関数ポインタの内容を書き換えることにある.
例をみる
まず2つのクラスを定義します
class A
{
private:
	int ma;
	int mb;
public:
	A()
	{
		ma = 1;
		mb = 2;
	}
	
	virtual void Name()
	{
		cout<<"this is A"<<endl;	
	}

	virtual void special()
	{
		cout<<"Hello kitty"<<endl;
	}

	void print()
	{
		int c;
		printf("the address of the function is %x,class A is output ma %d
",&c,ma); } }; class B:public A { private: int mc; public: B() { mc = 3; } virtual void Name() { cout<<"this is B"<<endl; } virtual void specialB() { cout<<"Hello Motor"<<endl; } void print() { int c; printf("the address of the function is %x,class B is output mc %d
",&c,mc); } };

関数呼び出し
int experimentClassUpDownPointer()
{
    B b;
    A *a;
    a = (A*)&b;

    a->Name();
    b.Name();
    a->print();

    return 1;
}

アセンブリ分析
初期化コード除去後、bの定義のアセンブリコードは
00961D3E  lea         ecx,[b]  
00961D41  call        B::B (9611DBh) 

ここでbのアドレスをecxに送ることに注意してください.
そしてBクラスのコンストラクタ体内に入ります
同様に初期化コードを無視した後、ここに到着します.このときecx,すなわちこのときecxの値はbのアドレスであり,コンパイラでもthisポインタの内容がbのアドレスであることが容易に得られる.
00961EAF  pop         ecx  
00961EB0  mov         dword ptr [ebp-8],ecx  
00961EB3  mov         ecx,dword ptr [this]  
00961EB6  call        A::A (9611E5h) 

その後Aのコンストラクション関数に入るので,サブクラスオブジェクトのコンストラクションは親より先に完了する.
同様に、コンストラクション関数に入る前にecxをスタックに入れます.
初期化後、Aのコンストラクタがここに来る
00961BDF  pop         ecx  
00961BE0  mov         dword ptr [ebp-8],ecx  
00961BE3  mov         eax,dword ptr [this]  
00961BE6  mov         dword ptr [eax],offset A::`vftable' (96A8D8h)  
	{
		ma = 1;
00961BEC  mov         eax,dword ptr [this]  
00961BEF  mov         dword ptr [eax+4],1  
		mb = 2;
00961BF6  mov         eax,dword ptr [this]  
00961BF9  mov         dword ptr [eax+8],2  
	}

まずecxが格納したbのアドレスを復元する.注意したいのは、00961 BE 3および00961 BE 6のコードです.まずthisポインタの内容をeaxに入れ、すなわちbのアドレスをeaxに送る.次に、Aクラスの虚関数リストのアドレスは、bアドレスから始まる4バイトに入る.なぜなら、最初の4バイトはいわゆる虚ポインタ、すなわち、オブジェクトbの虚ポインタがクラスAの虚関数リストを指しているからである.Aのダミー関数テーブルが存在するメモリの内容は
0x0096A8D8  ea 11 96 00 f8 12 96 00 00 00 00 00 74 68 69 73 20 69 73 20 41 00 00 00 48 65 6c 6c 6f 20 6b  ・E.・E.・E.・E.....this is A...HelloK
0x0096A8F7  69 74 74 79 00 00 00 00 00 74 68 65 20 61 64 64 72 65 73 73 20 6f 66 20 74 68 65 20 66 75 6e  itty.....the address of the fun

その後変数を初期化し,bの格納アドレスから連続的に格納されていることがわかり,&b+8に占有されていることを覚えておく.
Aのコンストラクタの実行が完了したら、Bのコンストラクタの実行を続行します.
00961EBB  mov         eax,dword ptr [this]  
00961EBE  mov         dword ptr [eax],offset B::`vftable' (96A94Ch)  
	{
		mc = 3;
00961EC4  mov         eax,dword ptr [this]  
00961EC7  mov         dword ptr [eax+0Ch],3  
	}
00961ECE  mov         eax,dword ptr [this]  
00961ED1  pop         edi  
00961ED2  pop         esi  
00961ED3  pop         ebx  
00961ED4  add         esp,0CCh  
00961EDA  cmp         ebp,esp  
00961EDC  call        @ILT+750(__RTC_CheckEsp) (9612F3h)  
00961EE1  mov         esp,ebp  
00961EE3  pop         ebp  
00961EE4  ret 

肝心なのはどこですか.00961 EBBと00961 EBEでは,先ほどのAのコンストラクション関数とほぼ同じである.
得られた効果も同様に,オブジェクトbのダミーポインタに値を付与し,先ほどの値(Aのダミー関数リストのアドレス)を上書きし,その後Bのダミー関数リストを指す.
なお,Bクラスのメンバ変数を初期化する際に占有するアドレスは&b+0 chである.したがって、継承後の親と子のメンバー変数は連続的に格納されます.
では、呼び出し方法を見てみましょう.
//experimentClassUpDownPointer ,b :
    A *a;
    a = (A*)&b;
00961D46  lea         eax,[b]  
00961D49  mov         dword ptr [a],eax  

    a->Name();
00961D4C  mov         eax,dword ptr [a]  
00961D4F  mov         edx,dword ptr [eax]  
00961D51  mov         esi,esp  
00961D53  mov         ecx,dword ptr [a]  
00961D56  mov         eax,dword ptr [edx]  
00961D58  call        eax  

まず、bのアドレスをeaxに送り、次いで、bのアドレスから始まる4バイトの内容をedx、すなわちbの虚関数リストポインタの値をedxに与える.このときの虚関数ポインタがBの虚関数リストを指していることを忘れないでください.calleaxの後に何が起こったのかよくわかります.
B::Name:
00961366  jmp         B::Name (961F00h) 
virtual void Name()
	{
01101F00  push        ebp  
01101F01  mov         ebp,esp  
01101F03  sub         esp,0CCh  
01101F09  push        ebx  
01101F0A  push        esi  
01101F0B  push        edi  
01101F0C  push        ecx  
01101F0D  lea         edi,[ebp-0CCh]  
01101F13  mov         ecx,33h  
01101F18  mov         eax,0CCCCCCCCh  
01101F1D  rep stos    dword ptr es:[edi]  
01101F1F  pop         ecx  
01101F20  mov         dword ptr [ebp-8],ecx  
		cout<<"this is B"<<endl;	
01101F23  mov         esi,esp  
01101F25  mov         eax,dword ptr [__imp_std::endl (110E38Ch)]  
01101F2A  push        eax  
01101F2B  push        offset string "this is B" (110A95Ch)  
01101F30  mov         ecx,dword ptr [__imp_std::cout (110E390h)]  
01101F36  push        ecx  
01101F37  call        std::operator<<<std::char_traits<char> > (110127Bh)  
01101F3C  add         esp,8  
01101F3F  mov         ecx,eax  
01101F41  call        dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (110E388h)]  
01101F47  cmp         esi,esp  
01101F49  call        @ILT+750(__RTC_CheckEsp) (11012F3h)  
	}

案の定BのName関数にジャンプしました.
c++のアセンブリコードをよく分析することはやはり収穫があり,少なくとも下層でもその理由を知ることができる.