一個示例
首先讓我們觀察如下代碼:
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 {
public:
void test()const {
}
};
這個後置const
的標識符我們肯定經常會使用,但是想必沒有過多的深究,我們一般都會把這個當作一個簡單的給編譯器看的標識符,但是其實這個也可以用const
進行解釋:
這個後置const
是用來修飾this
指針的,所以在編譯期間,在這個函數作用範圍中,對非靜態成員的改變都是不被允許的,因爲this
指針指向的空間是不能被修改的