【 聲明:版權所有,歡迎轉載,請勿用於商業用途。 聯系信箱:feixiaoxing @163.com】
很多朋友,包括我自己在內,對C++語言的很多特性不是很明白。特別是幾年前找工作的時候,為了應付來自工作單位的考試,我經常逼著自己的去記住一些復雜的試題和答案。可是常常時間已過,一切又回到了原點。原來沒有弄清楚的問題還是沒有弄明白,一切都沒有發生改變。直到若干年後,當我在編碼過程中不斷積累經驗,嘗試用匯編代碼和內存數據來解釋一些現象的時候,才明白有些東西其實並不復雜。也許有的朋友對匯編語言會有畏懼,其實沒有必要。只要你對C語言有一些基礎,對堆棧有一些印象,那麼你已經擁有匯編語言的基礎了。在接下來的數篇博客中,我們就會就x86匯編、數據類型、數據運行邏輯、指針、數據、類、重載運算符在匯編下是如何展開的做一些介紹,談一些個人的看法。下面,我們就進行一些小測試,同時用匯編語言來說明一下。大家可以一起做一下。
(1)char name[] 和char* name
1:
2: void process()
3: {
00401020 push ebp
00401021 mov ebp,esp
00401023 sub esp,4Ch
00401026 push ebx
00401027 push esi
00401028 push edi
00401029 lea edi,[ebp-4Ch]
0040102C mov ecx,13h
00401031 mov eax,0CCCCCCCCh
00401036 rep stos dword ptr [edi]
4: char name_tmp[] = {"hello"};
00401038 mov eax,[string "hello" (0042201c)]
0040103D mov dword ptr [ebp-8],eax
00401040 mov cx,word ptr [string "hello"+4 (00422020)]
00401047 mov word ptr [ebp-4],cx
5: char* name_glb = "hello";
0040104B mov dword ptr [ebp-0Ch],offset string "hello" (0042201c)
6: }
00401052 pop edi
00401053 pop esi
00401054 pop ebx
00401055 mov esp,ebp
00401057 pop ebp
00401058 ret
通過上面的代碼,我們可以清楚地看出兩者之間的差別。"hello"字符串是一個全局只讀變量,空間地址為0x0042201C。name_tmp是函數內的char數組,第4行語句下面四行表示全局數據“hello”是分兩次拷貝到name_tmp的,第一次是dword、四個字節,第二次是word、兩個字節。所以name_tmp共有6個字節。相比較而言,name_glb什麼也沒有,它只是把自己指向了全局變量而已,所以它只是一個指針而已。
(2)apple a()和apple b
假設class apple的定義為:
class apple
{
public:
apple() {}
~apple() {}
};
那麼apple a()和apple b是分別怎麼編譯的呢?
9: void process()
10: {
00401020 push ebp
00401021 mov ebp,esp
00401023 sub esp,44h
00401026 push ebx
00401027 push esi
00401028 push edi
00401029 lea edi,[ebp-44h]
0040102C mov ecx,11h
00401031 mov eax,0CCCCCCCCh
00401036 rep stos dword ptr [edi]
11: apple a();
12: apple b;
00401038 lea ecx,[ebp-4]
0040103B call @ILT+20(apple::apple) (00401019)
13: }
00401040 lea ecx,[ebp-4]
00401043 call @ILT+10(apple::~apple) (0040100f)
00401048 pop edi
00401049 pop esi
0040104A pop ebx
0040104B add esp,44h
0040104E cmp ebp,esp
00401050 call __chkesp (004010b0)
00401055 mov esp,ebp
00401057 pop ebp
00401058 ret
為什麼apple a()這邊什麼也沒有編譯呢?原因很簡單,因為編譯器把apple a()看成是一個extern的函數,返回值為apple。與此相對應的apple b才是函數中真正定義的臨時變量,因為在下面不遠處有apple的兩個函數——apple的構造函數和apple的析構函數哦。
(3)(apple*) (0) -> print()
其中class apple這樣定義:
class apple
{
int value;
public:
apple() {}
~apple() {}
void print() { return;}
};
如果0設置為apple*,那麼訪問函數print會有問題嗎?
10: void process()
11: {
00401030 push ebp
00401031 mov ebp,esp
00401033 sub esp,40h
00401036 push ebx
00401037 push esi
00401038 push edi
00401039 lea edi,[ebp-40h]
0040103C mov ecx,10h
00401041 mov eax,0CCCCCCCCh
00401046 rep stos dword ptr [edi]
12: ((apple*)(0))->print();
00401048 xor ecx,ecx
0040104A call @ILT+0(apple::print) (00401005)
13: }
0040104F pop edi
00401050 pop esi
00401051 pop ebx
00401052 add esp,40h
00401055 cmp ebp,esp
00401057 call __chkesp (004010e0)
0040105C mov esp,ebp
0040105E pop ebp
0040105F ret
通過運行函數,我們發現沒有任何異常產生,為什麼呢?因為我們發現ecx是作為0傳給print函數的,也就是我們熟悉的this指針為0。但是我們發現在print函數內部沒有用到this指針,因為我們根本沒有對this->value進行訪問,只是一個返回語句return。這說明指針作為class null指針並不可怕,可怕的是用null去訪問內存中的數據。
(4)int m = 1; int n = m++ + ++m; 那麼n是多少呢?
10: void process()
11: {
0040D4D0 push ebp
0040D4D1 mov ebp,esp
0040D4D3 sub esp,48h
0040D4D6 push ebx
0040D4D7 push esi
0040D4D8 push edi
0040D4D9 lea edi,[ebp-48h]
0040D4DC mov ecx,12h
0040D4E1 mov eax,0CCCCCCCCh
0040D4E6 rep stos dword ptr [edi]
12: int m = 1;
0040D4E8 mov dword ptr [ebp-4],1
13: int n = m++ + ++m;
0040D4EF mov eax,dword ptr [ebp-4]
0040D4F2 add eax,1
0040D4F5 mov dword ptr [ebp-4],eax
0040D4F8 mov ecx,dword ptr [ebp-4]
0040D4FB add ecx,dword ptr [ebp-4]
0040D4FE mov dword ptr [ebp-8],ecx
0040D501 mov edx,dword ptr [ebp-4]
0040D504 add edx,1
0040D507 mov dword ptr [ebp-4],edx
14: }
0040D50A pop edi
0040D50B pop esi
0040D50C pop ebx
0040D50D mov esp,ebp
0040D50F pop ebp
通過匯編代碼,我們看到【ebp-4】就是m在堆棧中的地址,【ebp-8】就是n在堆棧中的地址。int n = m++ + ++m下面總共有9句匯編。我們可以分析一下:前面三句表示m自己增加1,第四句表示ecx = m,即ecx = 2。第五句ecx和m相加,翻譯過來就是ecx = ecx + m。此時ecx = 4。第六句表示n = ecx。 第七句到第九句表示m自增加1。為什麼會出現這樣的情況呢,其實道理很簡單,主要是因為我們的表達式是從右向左運算的。如果大家這樣看就明白了,首先++m,然後n = m + m,最後m++。
(5)*p++和(*p)++區別是什麼
10: void process()
11: {
0040D4D0 push ebp
0040D4D1 mov ebp,esp
0040D4D3 sub esp,48h
0040D4D6 push ebx
0040D4D7 push esi
0040D4D8 push edi
0040D4D9 lea edi,[ebp-48h]
0040D4DC mov ecx,12h
0040D4E1 mov eax,0CCCCCCCCh
0040D4E6 rep stos dword ptr [edi]
12: char data = 'a';
0040D4E8 mov byte ptr [ebp-4],61h
13: char* p = & data;
0040D4EC lea eax,[ebp-4]
0040D4EF mov dword ptr [ebp-8],eax
14: *p++;
0040D4F2 mov ecx,dword ptr [ebp-8]
0040D4F5 add ecx,1
0040D4F8 mov dword ptr [ebp-8],ecx
15: (*p)++;
0040D4FB mov edx,dword ptr [ebp-8]
0040D4FE mov al,byte ptr [edx]
0040D500 add al,1
0040D502 mov ecx,dword ptr [ebp-8]
0040D505 mov byte ptr [ecx],al
16: }
0040D507 pop edi
0040D508 pop esi
0040D509 pop ebx
0040D50A mov esp,ebp
0040D50C pop ebp
0040D50D ret
我們首先創建局部變量data。然後把data的指針復制給p。從匯編代碼可以清楚的看出來:*p++就等於p++;(*p)++首先把指針復制給edx,然後獲取edx地址指向的char數據復制給al,al自增加1,同時p地址復制給ecx,al復制給ecx指向的地址,就是這麼簡單。
類似的問題還有很多,大家不妨自己試一試:
(1) 下面的union在內存是怎麼安排的?gcc和vc編譯的時候,分配的內存size是一樣的嗎?
typedef union
{
char m:3;
char n:7;
int data;
}value;
(2) 下面地址一致嗎?
char value1[] = {"hello"};
char value2[] = {"hello"};
char* pValue1 = “hello”;
char* pValue2 = "hello";
value1和value2地址一致嗎?pValue1和pValue2呢?
(3)下面一段話為什麼運行錯誤?為什麼內存洩露了?怎麼修改?
class apple
{
char* pName;
public:
apple() { pName = (char*)malloc(10);}
~apple() {if(NULL != pName) free(pName);}
};
void process()
{
apple a, b;
a = b;
}
(全文完)