為什麼通過null 指標(NULL)可以正確調用一些類的成員函數?

來源:互聯網
上載者:User

原文:http://blog.csdn.net/starlee/article/details/2062586

有下面的一個簡單的類:

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

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.