天天看點

淺析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進行。

    對于上面的例子來說,this的值也就是pNull的值。也就是說this的值為NULL。而Test1()是靜态函數,編譯器不會給它傳遞this指針,是以call 1那行代碼可以正确調用(這裡相當于CNullPointCall::Test1());對于Test2()和Test3()兩個成員函數,雖然編譯器會給這兩個函數傳遞this指針,但是它們并沒有通過this指針來通路類的成員變量,是以call 2和call 3兩行代碼可以正确調用;而對于成員函數Test4()要通路類的成員變量,是以要使用this指針,這個時候發現this指針的值為NULL,就會造成程式的崩潰。   

    其實,我們可以想象編譯器把Test4()轉換成如下的形式:

void  CNullPointCall::Test4(CNullPointCall  * this )

{

    cout  <<   this -> m_iTest  <<  endl; 

}

    而把call 4那行代碼轉換成了下面的形式:

CNullPointCall::Test4(pNull);

    是以會在通過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) 

    通過比較靜态函數Test1()和其他3個非靜态函數調用所生成的的彙編代碼可以看出:非靜态函數調用之前都會把指向對象的指針pNull(也就是this指針)放到ecx寄存器中(mov ecx,dword ptr [pNull])。這就是this指針的特殊之處。看call 3那行C++代碼的彙編代碼就可以看到this指針跟一般的函數參數的差別:一般的函數參數是直接壓入棧中(push 0Dh),而this指針卻被放到了ecx寄存器中。在類的非成員函數中如果要用到類的成員變量,就可以通過通路ecx寄存器來得到指向對象的this指針,然後再通過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++代碼的彙編代碼為:

    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指針的實作應該都是差不多的。