淺析C++中的this指標及彙編實現
阿新 • • 發佈:2019-01-05
有下面的一個簡單的類:
那麼下面的程式碼都正確嗎?都會輸出什麼?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進行。
對於上面的例子來說,this的值也就是pNull的值。也就是說this的值為NULL。而Test1()是靜態函式,編譯器不會給它傳遞this指標,所以call 1那行程式碼可以正確呼叫(這裡相當於CNullPointCall::Test1());對於Test2()和Test3()兩個成員函式,雖然編譯器會給這兩個函式傳遞this指標,但是它們並沒有通過this指標來訪問類的成員變數,因此call 2和call 3兩行程式碼可以正確呼叫;而對於成員函式Test4()要訪問類的成員變數,因此要使用this指標,這個時候發現this指標的值為NULL,就會造成程式的崩潰。
其實,我們可以想象編譯器把Test4()轉換成如下的形式:
而把call 4那行程式碼轉換成了下面的形式:void CNullPointCall::Test4(CNullPointCall *this) { cout << this->m_iTest << endl; }
CNullPointCall::Test4(pNull);
所以會在通過this指標訪問m_iTest的時候造成程式的崩潰。下面通過檢視上面程式碼用VC 2005編譯後的彙編程式碼來詳細解釋一下神奇的this指標。
上面的C++程式碼編譯生成的彙編程式碼是下面的形式:
通過比較靜態函式Test1()和其他3個非靜態函式呼叫所生成的的彙編程式碼可以看出:非靜態函式呼叫之前都會把指向物件的指標pNull(也就是this指標)放到ecx暫存器中(mov ecx,dword ptr [pNull])。這就是this指標的特殊之處。看call 3那行C++程式碼的彙編程式碼就可以看到this指標跟一般的函式引數的區別:一般的函式引數是直接壓入棧中(push 0Dh),而this指標卻被放到了ecx暫存器中。在類的非成員函式中如果要用到類的成員變數,就可以通過訪問ecx暫存器來得到指向物件的this指標,然後再通過this指標加上成員變數的偏移量來找到相應的成員變數。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)
下面再通過另外一個例子來說明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++程式碼的彙編程式碼為: CTest test;
test.SetValue();
004117DC lea ecx,[test]
004117DF call CTest::SetValue (4111CCh)
同樣的,首先把指向物件的指標放到ecx暫存器中;然後呼叫類CTest的成員函式SetValue()。地址4111CCh那裡存放的其實就是一個轉跳指令,轉跳到成員函式SetValue()內部。004111CC jmp CTest::SetValue (411750h)
而411750h才是類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型的成員變數,在test物件記憶體中連續存放,也就是說this指標目前指向m_iValue1。
5、給暫存器eax指向的地址賦值0Dh(十六進位制的13)。其實就是給成員變數m_iValue1賦值13。
6、同4。
7、給暫存器eax指向的地址加4的地址賦值。在4中已經說明,eax暫存器記憶體放的是this指標,而this指標指向連續存放的int型的成員變數m_iValue1。this指標加4(sizeof(int))也就是成員變數m_iValue2的地址。因此這一行就是給成員變數m_iValue2賦值。
通過上面的分析,我們可以從底層瞭解了C++中this指標的實現方法。雖然不同的編譯器會使用不同的處理方法,但是C++編譯器必須遵守C++標準,因此對於this指標的實現應該都是差不多的。