看此文,務必須要先了解本文討論的背景,很少說,給出連接:html
探討C++ 變量生命週期、棧分配方式、類內存佈局、Debug和Release程序的區別(一)函數
本文會以此問題做爲討論的實例,來具體討論如下四個問題:佈局
(1) C++變量生命週期優化
(2) C++變量在棧中分配方式spa
(3) C++類的內存佈局指針
(4) Debug和Release程序的區別code
一、Debug版本輸出現象解析htm
先來講說Debug版本的輸出,前5次輸出,交替輸出,後5次輸出,交替輸出,可是,前5次和後5次的地址是不同的。對象
咱們來看看反彙編:blog
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仍然只有分配一個地址供這些變量使用。
理由很簡單,變量的生命週期結束了,它的地址天然就能夠給其餘變量用了,更況且這樣變量的大小仍是同樣的呢!
二、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],esi cout<<&r<<endl; }
Release版本作了進一步的優化,esp內的值在本程序運行的過程當中不曾改變,所以,儘管有十次循環,也只分配了兩個對象的空間,即兩個地址。
最後,咱們看到,前5次循環和後5次循環的交替輸出的地址是同樣的。
三、再提一點:最後的十次輸出現象解析:
for(int i=0;i<10;i++)
{
p[i]->showNum();
}
實際上是沒有意義的,由於這10個指針指向的對象的生命週期早就結束了。
那麼爲何還能輸出正確的值呢?由於,這些對象的生命週期雖然結束了,可是這些對象的內存沒有遭到破壞,仍還存在,而且數據未被改寫。
若是此程序後續還增長代碼,這些地址的內容是否會被其餘對象佔用都是不可知的,因此,請不要使用生命週期已經結束了的對象。
四、總結:
給你們建議,C++語言的對象生命週期的概念很重要,要重視,另外,使用指針要注意空指針的問題。
有時候,能夠直接使用對象的方式,就不要使用太多指針,都是坑!
後記:
忽然以爲本身好無聊,之後仍是少分析這些問題,多作些實事!不過,偶爾分析一下,仍是能夠的。