對C++中的this指針的分析

一個示例

首先讓我們觀察如下代碼:

namespace ClassTest {
    class A {
    private:
        int m_int1;
        int m_int;
        static int st_int;
    public:
        void test1() { cout << "test1" << endl; }
        void test2() { cout << "test2" << endl; }
        static void test3() { cout << "test3 " << st_int << endl; }
        void test4() { m_int = 5; }
    };
    int A::st_int = 5;
    void test() {
        A* nullP = NULL;
        nullP->test1();
        nullP->test2();
        nullP->test3();
        nullP->test4();
    }
}
int main() {
    ClassTest::test();
    system("pause");
    return 0;
}

你認爲這些代碼都能成功執行嗎?
想必你肯定會奇怪我居然會問這種問題,一個已經指向了NULL的類指針,怎麼可能還能成功調用成員函數呢?
但是假如你對C++的類的實現機制有比較多的瞭解,就會思考出上述的代碼執行情況可能會是這樣的:

A* nullP = NULL;
nullP->test1();//執行
nullP->test2();//執行
nullP->test3();//執行
nullP->test4();//出錯,因爲傳入的this指針爲NULL,但是卻想訪問非靜態成員變量

why?

思考

因爲在C++中,類的成員函數的執行並不只是直接跳轉到函數體然後就直接進行執行了,而是會在調用成員函數之前,傳入一個this指針(比如上面的代碼,傳入的this指針的類型爲A* const,其值爲NULL )。
所以我們可以很容易的想到,當我們使用一個類指針去執行其對應的成員函數的時候,編譯器也許會幫我們做下面的事情:

  • 根據指針類型找到這個成員函數
  • this放在一個固定寄存器中傳入然後在所有參數壓棧後再進行壓棧
  • 執行成員函數的代碼,當使用到非靜態成員變量的時候在其前面加上this->

    所以上面的test4函數可能會被編譯器添添改改變成下面這種樣子:

void test4( A* const this){ 
     this->m_int = 5;
};

實踐驗證,深入剖析

我們可以通過VS生成的彙編代碼看看我說的對不對(通過VS的單步調試和反彙編我們可以很容易的做到)

執行以上的代碼,我們可以發現在執行test4函數之前,會先執行如下彙編代碼:

0133C5BA  mov         ecx,dword ptr [nullP]  
0133C5BD  call        ClassTest::A::test4 (013175C2h)  

不難看出,在成員函數調用之前,nullP的值被放在了ecx寄存器中,然後接着跟蹤,test4內部的彙編代碼如下:

    void test4(){ 
0133BC20  push        ebp  
0133BC21  mov         ebp,esp  
0133BC23  sub         esp,0CCh  
0133BC29  push        ebx  
0133BC2A  push        esi  
0133BC2B  push        edi  
0133BC2C  push        ecx  
0133BC2D  lea         edi,[ebp-0CCh]  
0133BC33  mov         ecx,33h  
0133BC38  mov         eax,0CCCCCCCCh  
0133BC3D  rep stos    dword ptr es:[edi]  
0133BC3F  pop         ecx  
0133BC40  mov         dword ptr [this],ecx  
            m_int = 5;
0133BC43  mov         eax,dword ptr [this]  
0133BC46  mov         dword ptr [eax+4],5  
        };

ecx最後被壓棧

注意下面這幾行彙編代碼:

00F0BC3F  pop         ecx  
00F0BC40  mov         dword ptr [this],ecx  
            m_int = 5;
0133BC43  mov         eax,dword ptr [this]  
0133BC46  mov         dword ptr [eax+4],5  

我們可以看到在訪問m_int的時候,編譯器先將 ecx出棧,然後將ecx的值放在this指針應該在的位置(這裏我不是太清楚,但是我想的是vs便編譯器會將this指針放在堆棧上的固定位置),然後將this的值放在eax寄存器上,然後加上偏移值就可以訪問到其成員變量,如果我們將test4的函數改成如下形式:

void test4(){
      m_int1=5;
}

然後彙編代碼變成了這樣:

000CBC40  mov         dword ptr [this],ecx  
            m_int1 = 5;
000CBC43  mov         eax,dword ptr [this]  
000CBC46  mov         dword ptr [eax],5  

我們可以推斷,第一個非靜態成員變量就放在this指針指向的位置(在沒有析構函數的時候),當我們需要訪問其餘非靜態成員變量時,就加上由其變量類型主導的偏移量。

我們再觀察一下上面所有成員函數執行之前的彙編代碼:

        nullP->test1();
000CC5A5  mov         ecx,dword ptr [nullP]  
000CC5A8  call        ClassTest::A::test1 (0A75BDh)  
        nullP->test2();
000CC5AD  mov         ecx,dword ptr [nullP]  
000CC5B0  call        ClassTest::A::test2 (0A75CCh)  
        nullP->test3();
000CC5B5  call        ClassTest::A::test3 (0A75C7h)  
        nullP->test4();
000CC5BA  mov         ecx,dword ptr [nullP]  
000CC5BD  call        ClassTest::A::test4 (0A75C2h)  

可以發現,我上面說的那些想法都是對的,在執行一個非靜態成員函數之前,this指針就會被傳入,在訪問成員變量的時候,this指針會被使用,所以前三個函數不會出錯,因爲成員變量沒被訪問,this指針就算爲NULL,也不會出錯,因爲this指針不會被使用。
我們還可以發現test3函數執行之前並沒有傳入this指針,爲什麼?
很簡單,我就不說了,留給自己思考。

this指針總結

this指針何時被創建?
在函數調用之前,實際上,成員函數默認第一個參數就爲T* const this,不同的編譯器實現方法有所不同。

this指針何時被銷燬?
在函數執行完成之後

this指針何時不會被當作參數傳入?
全局函數,靜態函數都不會使用this指針。

思考以下如下代碼:

    class B {
    publicvoid test()const {

        }
    };

這個後置const的標識符我們肯定經常會使用,但是想必沒有過多的深究,我們一般都會把這個當作一個簡單的給編譯器看的標識符,但是其實這個也可以用const進行解釋:
這個後置const是用來修飾this指針的,所以在編譯期間,在這個函數作用範圍中,對非靜態成員的改變都是不被允許的,因爲this指針指向的空間是不能被修改的

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章