探討C++ 變量生命週期、棧分配方式、類內存佈局、Debug和Release程序的區別(二)

 

看此文,務必須要先了解本文討論的背景,很少說,給出連接: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++語言的對象生命週期的概念很重要,要重視,另外,使用指針要注意空指針的問題。

有時候,能夠直接使用對象的方式,就不要使用太多指針,都是坑!

 

後記:

忽然以爲本身好無聊,之後仍是少分析這些問題,多作些實事!不過,偶爾分析一下,仍是能夠的。

相關文章
相關標籤/搜索