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ポインタの実現には差が少ないはずです.