?
看此文,務必需要先了解本文討論的背景,不多說,給出鏈接:
探討C++ 變量生命周期、棧分配方式、類內存布局、Debug和Release程序的區別(一)
?
本文會以此問題作為討論的實例,來具體討論以下四個問題:
(1)?????? C++變量生命周期
(2)?????? C++變量在棧中分配方式
(3)?????? C++類的內存布局
(4)?????? Debug和Release程序的區別
?
1、Debug版本輸出現象解析
先來說說Debug版本的輸出,前5次輸出,交替輸出,后5次輸出,交替輸出,但是,前5次和后5次的地址是不一樣的。
我們來看看反匯編:
T1 r(2); 01363A0D push 2 01363A0F lea ecx,[r] 01363A12 call T1::T1 (1361172h)p[i]=&r; 01363A17 mov eax,dword ptr [i] 01363A1A lea ecx,[r] 01363A1D mov dword ptr p[eax*4],ecx
?
關鍵是看對象r的地址是如何分配的,但是,反匯編中似乎沒有明顯的信息,只有一句:lea ?ecx,[r],這條語句是什么意思呢?將對象r的地址取到通用寄存器ecx中。
我們知道,程序在編譯鏈接的時候,變量相對于棧頂的位置就確定了,稱為相對地址確定。所以,此時程序在運行了,根據所在環境,變量的絕對地址也就確定了。
通過lea指令取得對象地址,調用對象的構造函數來進行構造,即語句call ?T1::T1 (1361172h). 構造完之后,對象所在地址的值才被正確填充。
?
好了,我們知道了這些局部變量相對于棧的相對地址,其實在編譯鏈接的時候就確定了,那么,這個策略是什么樣的呢?就是說,編譯器是如何來決定這些局部變量的地址的呢?
一般來說,對于不同的變量,編譯器都會分配不同的地址,一般是按照順序分配的。但是,對于那些局部變量,而且非常明顯的生命周期已經結束了,同一個地址,也會分配給不同的變量。
舉個例子,地址0X00001110,被編譯器用來存放變量A,同時也可能被編譯器用來存放變量B,如果A和B的大小相等,并且肯定不會同時存在。
?
編譯器在判斷一個地址是否能夠被多個變量同時使用的時候,這個判斷策略取決于編譯器本身,不同的編譯器判斷策略不同。
微軟的編譯器,就是根據代碼的自身邏輯來判斷的。當編譯器檢測到以下代碼的時候:
for(int i=0;i<5;i++){if(i%2==0){T1 r(2);p[i]=&r;cout<<&r<<endl;}else{T2 r(3);p[i]=&r;cout<<&r<<endl;}}
微軟的編譯器認為,只需要分配兩個地址則可,分別用來保存兩個對象,循環執行的話,因為前一次生成對象的生命周期已經結束,直接使用原來的地址則可。
因此,我們在用VS編譯這段程序時,就出現了地址交替輸出的情況。
?
當微軟的編譯器接著又看到以下代碼的時候,
for(int i=5;i<10;i++){if(i%2==0){T1 r(4);p[i]=&r;cout<<&r<<endl;}else{T2 r(5);p[i]=&r;cout<<&r<<endl;}}
微軟的編譯器認為,需要再分配兩個地址,分別用來保存這兩個新的對象,
于是,我們再次看到了地址交替輸出的情況,只是這一次交替輸出的地址與前一次交替輸出的地址不同。
?
延伸1:稍微修改代碼再試試
我們已經能夠理解VS下Debug版本為什么會輸出這樣的結果了,再延伸一下,我們把代碼進行修改:
修改前的代碼:for(int i=0;i<5;i++){if(i%2==0){T1 r(2);p[i]=&r;cout<<&r<<endl;}else{T2 r(3);p[i]=&r;cout<<&r<<endl;}}
修改后的代碼為:if (0 == i){T1 r(2);p[i]=&r;cout << &r << endl;}else if (1 == i){T2 r(3);p[i]=&r;cout << &r << endl;}else if (2 == i){T1 r(2);p[i]=&r;cout << &r << endl;}else if (3 == i){T2 r(3);p[i]=&r;cout << &r << endl;}else if (4 == i){T1 r(2);p[i]=&r;cout << &r << endl;} )
代碼修改之后,功能完全一樣,那么前五次循環的輸出會有什么不同嗎?
也許你猜到了,修改完代碼之后,前5次地址輸出,是5個不同的地址,按規律遞增或者遞減。
很明顯,代碼的改動,編譯器的認知也改變了,分配了5個地址來給這5個對象使用。
?
延伸2:GCC編譯器是如何編譯這段代碼的呢?
我們再延伸一下,不同的編譯器,對代碼的編譯是不同的,GCC編譯器是如何編譯這段代碼的呢?默認編譯之后,運行結果如下:
?
不用我說,大家也知道了,GCC編譯器檢測到這些變量生命周期結束了,盡管有十次循環,盡管代碼有改動,但是GCC仍然只有分配一個地址供這些變量使用。
理由很簡單,變量的生命周期結束了,它的地址自然就可以給其他變量用了,更何況這樣變量的大小還是一樣的呢!
?
?2、VS下Release版本輸出現象解析:
不再延伸,回到正題,VS下Release版本的表現為什么和Debug版本不一樣呢?
同樣,我們來看原始代碼的反匯編:
if(i%2==0){T1 r(2);p[i]=&r;cout<<&r<<endl; 00C11020 mov ecx,dword ptr [__imp_std::endl (0C12044h)] 00C11026 push ecx 00C11027 mov ecx,dword ptr [__imp_std::cout (0C12048h)] 00C1102D test bl,1 00C11030 jne main+42h (0C11042h) 00C11032 lea eax,[esp+14h] 00C11036 mov dword ptr [esp+14h],ebp 00C1103A mov dword ptr [esp+18h],ebp 00C1103E mov edx,eax}else 00C11040 jmp main+50h (0C11050h){T2 r(3);p[i]=&r; 00C11042 lea eax,[esp+1Ch] 00C11046 mov dword ptr [esp+1Ch],edi 00C1104A mov dword ptr [esp+20h],esicout<<&r<<endl;}
Release版本做了進一步的優化,esp內的值在本程序運行的過程中未曾改變,因此,盡管有十次循環,也只分配了兩個對象的空間,即兩個地址。
最后,我們看到,前5次循環和后5次循環的交替輸出的地址是一樣的。
?
3、再提一點:最后的十次輸出現象解析:
??? for(int i=0;i<10;i++)
??? {
??????? p[i]->showNum();
??? }
其實是沒有意義的,因為這10個指針指向的對象的生命周期早就結束了。
那么為什么還能輸出正確的值呢?因為,這些對象的生命周期雖然結束了,但是這些對象的內存沒有遭到破壞,仍還存在,并且數據未被改寫。
如果此程序后續還增加代碼,這些地址的內容是否會被其他對象占用都是不可知的,所以,請不要使用生命周期已經結束了的對象。
?
4、總結:
給大家建議,C++語言的對象生命周期的概念很重要,要重視,另外,使用指針要注意空指針的問題。
有時候,可以直接使用對象的方式,就不要使用太多指針,都是坑!
?
后記:
突然覺得自己好無聊,以后還是少分析這些問題,多做些實事!不過,偶爾分析一下,還是可以的。