C++のthisポインタを祥解する


class CNullPointCall
{
public:
    static void Test1();
    void Test2();
    void Test3(int iTest);
    void Test4();

private:
    static int m_iStatic;
    int m_iTest;
};

int CNullPointCall::m_iStatic = 0;

void CNullPointCall::Test1()
{
    cout << m_iStatic << endl;
}

void CNullPointCall::Test2()
{
    cout << "Very Cool!" << endl; 
}

void CNullPointCall::Test3(int iTest)
{
    cout << iTest << endl; 
}

void CNullPointCall::Test4()
{
    cout << m_iTest << endl; 
}

では、次のコードは正しいですか?何が出力されますか?
CNullPointCall *pNull = NULL; //   ,         
pNull->Test1(); // call 1
pNull->Test2(); // call 2
pNull->Test3(13); // call 3
pNull->Test4(); // call 4

私がどうしてそんなことを聞いたのか不思議に思うに違いない.NULLの値を持つポインタがクラスのメンバー関数を呼び出すのにどのように使用できますか?!しかし、実際には驚くべきことに、call 4の行コードを除いて、残りの3つのクラスメンバー関数の呼び出しは成功し、結果を正確に出力することができ、この3行コードを含むプログラムは非常によく実行することができます.注意深く比較した結果,call 4の行コードは他の3行コードと本質的に異なることが分かった:クラスCNullPointCallのメンバー関数にthisポインタが用いられた.クラスメンバー関数では、オブジェクトが個別のメンバー関数体に対応するのではなく、このようなすべてのオブジェクトがこのメンバー関数体を共有します.プログラムがコンパイルされると、このメンバー関数のアドレスが決定されます.メンバー関数がこのような各オブジェクトに属するデータを区別できるのは,このthisポインタによる.関数内のクラス・データ・メンバーへのすべてのアクセスは、this->データ・メンバーの方法に変換されます.オブジェクトのthisポインタはオブジェクト自体の一部ではありませんsizeofには影響しません(「オブジェクト」)の結果です.この役割ドメインはクラス内部にあり、クラスの非静的メンバー関数でクラスの非静的メンバーにアクセスすると、コンパイラはオブジェクト自体のアドレスを自動的に隠しパラメータとして関数に渡します.すなわち、thisポインタを書かなくても、コンパイラはコンパイル時にthisを加え、非静的メンバー関数の隠しパラメータとして、各メンバーへのアクセスはthisによって行われます.上記の例では、thisの値、すなわちpNullの値である.つまりthisの値はNULLです.Test 1()は静的関数であり、コンパイラはthisポインタを渡さないので、call 1の行コードは正しく呼び出すことができます(ここではCNullPointCall::Test 1()に相当します).Test 2()とTest 3()の2つのメンバー関数の場合、コンパイラはこの2つの関数にthisポインタを渡しますが、クラスのメンバー変数にthisポインタでアクセスしていないため、call 2とcall 3の2行のコードを正しく呼び出すことができます.メンバー関数Test 4()に対してクラスにアクセスするメンバー変数であるため、thisポインタを使用します.このとき、thisポインタの値がNULLであることが判明すると、プログラムがクラッシュします.
 
実際には、コンパイラがTest 4()を次のように変換することが想像できます.
void CNullPointCall::Test4(CNullPointCall *this)
{
    cout << this->m_iTest << endl; 
}

call 4の行コードを次の形式に変換しました.call 4の行コードを次の形式に変換しました.
CNullPointCall::Test4(pNull);

だからthisポインタでm_にアクセスしますiTestの時にプログラムがクラッシュした.次に、上記のコードがVC 2005でコンパイルされたアセンブリコードを参照して、不思議なthisポインタを詳しく説明します.上のC++コードコンパイルで生成されたアセンブリコードは次の形式です.したがって、thisポインタでm_にアクセスします.iTestの時にプログラムがクラッシュした.次に、上記のコードがVC 2005でコンパイルされたアセンブリコードを参照して、不思議なthisポインタを詳しく説明します.上のC++コードコンパイルで生成されたアセンブリコードは次の形式です.
    CNullPointCall *pNull = NULL;
0041171E  mov         dword ptr [pNull],0 
    pNull->Test1();
00411725  call        CNullPointCall::Test1 (411069h) 
    pNull->Test2();
0041172A  mov         ecx,dword ptr [pNull] 
0041172D  call        CNullPointCall::Test2 (4111E0h) 
    pNull->Test3(13);
00411732  push        0Dh  
00411734  mov         ecx,dword ptr [pNull] 
00411737  call        CNullPointCall::Test3 (41105Ah) 
    pNull->Test4();
0041173C  mov         ecx,dword ptr [pNull] 
0041173F  call        CNullPointCall::Test4 (411032h) 

静的関数Test 1()と他の3つの非静的関数呼び出しを比較して生成されたアセンブリコードは,非静的関数呼び出しの前にオブジェクト指向のポインタpNull(すなわちthisポインタ)をecxレジスタ(mov ecx,dword ptr[pNull]に格納することが分かる.).これがthisポインタの特殊な点です.call 3の行のC++コードのアセンブリコードを見て、thisポインタと一般的な関数パラメータの違いを見ることができます:一般的な関数パラメータは直接スタックに押し込まれます(push 0 Dh)、thisポインタはecxレジスタに格納されます.クラスの非メンバー関数でクラスのメンバー変数を使用する場合は、ecxレジスタにアクセスしてオブジェクトを指すthisポインタを取得し、thisポインタにメンバー変数のオフセット量を加えて対応するメンバー変数を見つけることができます.次に、別の例で説明します.メンバー関数にどのように渡されるか、thisを使用してメンバー変数にアクセスする方法.依然として簡単なクラスです.
class CTest
{
public:
    void SetValue();

private:
    int m_iValue1;
    int m_iValue2;
};

void CTest::SetValue()
{
    m_iValue1 = 13;
    m_iValue2 = 13;
}

メンバー関数を次のコードで呼び出します.メンバー関数を次のコードで呼び出します.
CTest test;
test.SetValue();

上のC++コードのアセンブリコードは:上のC++コードのアセンブリコードは:
    CTest test;
    test.SetValue();
004117DC  lea         ecx,[test] 
004117DF  call        CTest::SetValue (4111CCh) 

同様に、まずオブジェクトを指すポインタをecxレジスタに格納する.次にクラスCTestのメンバー関数SetValue()を呼び出します.アドレス4111 CChに格納されているのは、メンバー関数SetValue()の内部にジャンプするジャンプ命令である.同様に、まずオブジェクトを指すポインタをecxレジスタに格納する.次にクラスCTestのメンバー関数SetValue()を呼び出します.アドレス4111 CChに格納されているのは、メンバー関数SetValue()の内部にジャンプするジャンプ命令である.
004111CC  jmp         CTest::SetValue (411750h

411750 hこそクラスCTestのメンバ関数SetValue()のアドレスである.
void CTest::SetValue()
{
00411750  push        ebp  
00411751  mov         ebp,esp 
00411753  sub         esp,0CCh 
00411759  push        ebx  
0041175A  push        esi  
0041175B  push        edi  
0041175C  push        ecx // 1   
0041175D  lea         edi,[ebp-0CCh] 
00411763  mov         ecx,33h 
00411768  mov         eax,0CCCCCCCCh 
0041176D  rep stos    dword ptr es:[edi] 
0041176F  pop         ecx // 2 
00411770  mov         dword ptr [ebp-8],ecx // 3
    m_iValue1 = 13;
00411773  mov         eax,dword ptr [this] // 4
00411776  mov         dword ptr [eax],0Dh // 5
    m_iValue2 = 13;
0041177C  mov         eax,dword ptr [this] // 6
0041177F  mov         dword ptr [eax+4],0Dh // 7
}
00411786  pop         edi  
00411787  pop         esi  
00411788  pop         ebx  
00411789  mov         esp,ebp 
0041178B  pop         ebp  
0041178C  ret

次に、上記のアセンブリコードの重点行を分析します.1、ecxレジスタの値をスタックに圧縮します.つまり、thisポインタをスタックに圧縮します.2、ecxレジスタ出スタック、すなわちthisポインタ出スタック.3、ecxの値を指定した場所、すなわちthisポインタを[ebp-8]内に置く.4、thisポインタの値をeaxレジスタに入れます.この場合、thisポインタはtestオブジェクトを指し、testオブジェクトはint型のメンバー変数が2つしかなく、testオブジェクトメモリに連続的に格納されます.つまり、thisポインタは現在m_を指しています.iValue1.     5、レジスタeaxが指すアドレスに0 Dh(16進数の13)を付与する.実はメンバ変数m_iValue 1に13を付与する.6、同4.7、レジスタeaxが指すアドレスに4を加算するアドレスに値を付与する.4で説明したように、eaxレジスタに格納されているのはthisポインタであり、thisポインタは連続的に格納されているint型のメンバ変数m_iValue 1を指す.thisポインタに4を加算する(sizeof(int))つまりメンバ変数m_iValue 2のアドレスです.したがって、この行はメンバー変数m_に与えられるiValue 2賦課.以上の解析により,C++におけるthisポインタの実現方法を底層から理解できた.異なるコンパイラでは異なる処理方法が使用されますが、C++コンパイラはC++の基準を守らなければならないので、thisポインタの実現には差が少ないはずです.次に、上記のアセンブリコードの重点行を分析します.1、ecxレジスタの値をスタックに圧縮します.つまり、thisポインタをスタックに圧縮します.2、ecxレジスタ出スタック、すなわちthisポインタ出スタック.3、ecxの値を指定した場所、すなわちthisポインタを[ebp-8]内に置く.4、thisポインタの値をeaxレジスタに入れます.この場合、thisポインタはtestオブジェクトを指し、testオブジェクトはint型のメンバー変数が2つしかなく、testオブジェクトメモリに連続的に格納されます.つまり、thisポインタは現在m_を指しています.iValue1.     5、レジスタeaxが指すアドレスに0 Dh(16進数の13)を付与する.実はメンバ変数m_iValue 1に13を付与する.6、同4.7、レジスタeaxが指すアドレスに4を加算するアドレスに値を付与する.4で説明したように、eaxレジスタに格納されているのはthisポインタであり、thisポインタは連続的に格納されているint型のメンバ変数m_iValue 1を指す.thisポインタに4を加算する(sizeof(int))つまりメンバ変数m_iValue 2のアドレスです.したがって、この行はメンバー変数m_に与えられるiValue 2賦課.以上の解析により,C++におけるthisポインタの実現方法を底層から理解できた.異なるコンパイラでは異なる処理方法が使用されますが、C++コンパイラはC++の基準を守らなければならないので、thisポインタの実現には差が少ないはずです.