C/C++記憶體分布與變數初始化順序__C++

來源:互聯網
上載者:User


關於入棧、出棧,棧頂棧底之類的分析見

函數調用的壓棧出棧過程分析

下面繼續分析C/C++的記憶體分布。



雖然0x10比一個變數需要的4個地址大了一些,但是0x10應該是規定的最小單位了。假如你要用的空間剛好是它的整數倍,其實是不浪費一分錢棧空間的,下邊做一個數組,證明棧空間大小剛好是所有非靜態變數佔用空間的大小。

這裡可以先無視hello的聲明,它不在棧空間,後面會分析。

   x6       #include<stdio.h>                                       x   x7       int main(){                                             x   x9               static int hello;                               x B+>x11              int array[100] = {7,4,1};                       x   x16              return 0;                                       x   x17      } 

下邊是彙編代碼:比普通變數情況下複雜些,多用了一些寄存器,多了一些push、pop操作,還有rep指令。

   x0x8048394 <main>                push   %ebp                     x   x0x8048395 <main+1>              mov    %esp,%ebp                x   x0x8048397 <main+3>              push   %edi                     x   x0x8048398 <main+4>              push   %ebx                     x   x0x8048399 <main+5>              sub    $0x190,%esp              xB+>x0x804839f <main+11>             lea    -0x198(%ebp),%ebx        x   x0x80483a5 <main+17>             mov    $0x0,%eax                x   x0x80483aa <main+22>             mov    $0x64,%edx               x100個變數(應該還有個標誌類型大小是4的東西吧)   x0x80483af <main+27>             mov    %ebx,%edi                x   x0x80483b1 <main+29>             mov    %edx,%ecx                x   x0x80483b3 <main+31>             rep stos %eax,%es:(%edi)        x   x0x80483b5 <main+33>             movl   $0x7,-0x198(%ebp)        x初始化了三個數。   x0x80483bf <main+43>             movl   $0x4,-0x194(%ebp)        x   x0x80483c9 <main+53>             movl   $0x1,-0x190(%ebp)        x   x0x80483d3 <main+63>             mov    $0x0,%eax                x   x0x80483d8 <main+68>             add    $0x190,%esp    x0x80483de <main+74>     pop    %ebx                             x   x0x80483df <main+75>     pop    %edi                             x   x0x80483e0 <main+76>     pop    %ebp                             x   x0x80483e1 <main+77>     ret                                     x   x

下邊是棧空間大小。兩者相減,0x190,等於400bytes,剛好是100個int。

(gdb) print $esp$1 = (void *) 0xbffff480(gdb) print $ebp$2 = (void *) 0xbffff618

我們都知道,”加了static就延長了變數的生命週期到程式結束“,或者還知道”加了static的變數地址不太一樣“。

確實,地址變了,靜態變數不在棧裡。

int main(){        static int hello;        int top;        return 0;}
地址,top在棧中,而hello不在

(gdb) print &top$3 = (int *) 0xbffff614(gdb) print &hello$4 = (int *) 0x8049614
棧大小是0x10
(gdb) print $esp$3 = (void *) 0xbffff608(gdb) print $ebp$4 = (void *) 0xbffff618
去掉int型的聲明,只留一個靜態變數的聲明,會發現一個有意思的現象 也就是說,連語句都被”最佳化“走了,不在main()函數裡邊執行了,不在main()函數裡執行意味著什麼。生命週期不光被向後延長到程式結束,也被向前延長了,它會在main之外,更早的地方被初始化。 那麼如果我在靜態變數聲明語句之前,main()函數之內執行,究竟允不允許呢。可能文法上會做屏蔽,但是按運行順序應該是可以的。 這句話換算成代碼的話,是這樣的:
#include<stdio.h>int main(){        hello = 4;        static int hello;        return 0;}
連初學者都認為這樣不可能,但是,如果我換一種寫法呢。
#include<stdio.h>static int hello;int main(){        hello = 4;        return 0;}
這樣大家就都認可了吧。 但是我覺得這兩個本質上是等價的, 只不過編譯器在碼農層面把這個操作給屏蔽了,可能太奇葩不便於理解。個人覺得是這樣的。不用去試了,確實編譯不過。。。。 換成可編譯的版本運行一次:
   x0x8048394 <main>                push   %ebp                     x   x0x8048395 <main+1>              mov    %esp,%ebp                xB+>x0x8048397 <main+3>              movl   $0x4,0x8049614           x   x0x80483a1 <main+13>             mov    $0x0,%eax                x   x0x80483a6 <main+18>             pop    %ebp                     x   x0x80483a7 <main+19>             ret                             x

main內聲明肯定是看不到的,直接用了一下而已,如果沒有那個指派陳述式,main內部什麼命理你個都看不到,直接就結束了。
我又想到另一個法子,因為編譯後我可以看地址, 有時候(可能吧)不做”變數列表“(也就是所有變數類型和數量的綜合)級的改動,其真實位址都是差不多的,所以可以嘗試先編譯運行,看到地址後,再強行提取變數,C語言的強大。(C語言不是這樣玩的好麼-_-) 這是我的設想:

#include<stdio.h>int main(){        int *ptr = 0x8049614;        printf("%d\n",*ptr);        static int hello = 100;        return 0;}
print *ptr = 0(期望100) 沒成功,但是還有希望,因為這次編譯地址變了:

再來:
#include<stdio.h>//static int hello = 100;int main(){        int *ptr = 0x8049578;        printf("%d\n",*ptr);        static int hello = 100;        return 0;}
不過討厭的是又變了。。。。。。。這個(全域變數區。怎麼叫來著。)地址分配很動態,不如棧的地址那麼穩固。
一定要抓住它,這下試試664。
終於被我抓住了。
靜態變數hello的聲明和定義都在13行,但我在12行就提前並列印了出來。 來來來,再來個普通運行。


然並卵,現實中哪有人運行完了再去看結果,硬把地址寫進去的,我也不知道這麼玩能應用在哪。
這個例子只為證明: 1.靜態變數的儲存地區不在棧,並且也不像棧的地址規則那麼穩定,但是還是能在一定程度上穩定住的,所以能抓住。 2.遇到那個“static”,這句聲明加定義就和放在main外邊沒區別,這種代碼的執行等於在程式正式執行之前就做完了,算初始化吧。 3.所以,無論靜態變數聲明在哪,它的生命週期都貫穿程式始終。 4.明明很早就聲明定義了,之所以你不能直接這樣提取靜態變數的值,應該只是編譯器設的規則,來避免一些不必要的人為錯誤吧。 5. static叫靜態,靜態靜在哪。動態又動在哪。動態當然是動在你運行它才有結果,不運行就沒有。棧雖然也會提前(運行函數體前)分配夠空間,但是最起碼變數的定義賦值是不會有的,而static,是一早就定義賦值完了,所以叫靜態。(所以有充分理由懷疑編譯器就是根據變數類型和數量,假設叫”變數列表“來分配棧空間的,誰叫我學渣沒怎麼學編譯原理呢,只能先猜測一下) 6.不要模仿


擴充訓練: 多加兩層函數呢。


請容許我複習一下C++: C++類靜態成員是否會有所不同。是很早就初始化了,還是從某一個對象運行該函數開始。假如說也是帶定義的形式,比如,static int i = 1;而不是static int i; 首先,第一種形式,ISO C++又不允許了(錯誤:ISO C++ 不允許在類內初始化非常量靜態成員)。 其次,又不允許使用A::i這種形式訪問A的靜態成員變數i。

“奇怪”的發現: 當用一個類連續聲明多個類對象的時候,後邊的類對象被某種最佳化機制放在不一樣的地方了(堆。全域靜態區。)
具體流程如下: 第一次編譯:  F f,f2;//對象在棧
第二次編譯:
F f,f2,f3,f4,f5,f6,f7,f8,f9;//只有f和f2在棧 //註:此時esp和ebp沒變,棧沒有擴容,是出於某種“懶惰”,還是“不屑”。
第三次編譯:
F f,f2,f3,f4;
F f5,f6,f7,f8,f9;//對象都在棧
第四次編譯:
F f,f2,f3,f4,f5,f6,f7,f8,f9;//所有對象都在棧。

首先,排除人為錯誤,雖然分配堆的時候F *pF = F();//print  &pF指標地址在棧,print pF指標指向的對象在堆。但是此處並不是指標,是實體。。。

編譯器做了某種最佳化,這個最佳化的區分絕對不是代碼使用不使用,f2就沒使用,為什麼和f一樣放到棧了。但是

我的總結就是,這個物件變數上次有地址,這次就還有,因為之前只有 F f,f2;//導致了f2在棧,其他的f3到f8都是後來追加了, 編譯器因為習慣,把f2保留在了棧,其他的扔一邊去了。這次也是,因為拆分成兩句進行聲明時f4,f5們也有了棧地址,所以再合并回去的時候, 編譯器出於習慣,繼續把他們放在棧中。 所以說,編譯器是帶記憶(緩衝)的 。

不過話說回來,一次聲明太多個物件變數,他們為什麼被扔到其他位置了。這是“把未聲明的變數放在靜態變數區”了。
而如果把E e,e2;//突然變成 E e,e2,e3,e33,e333,e3333,e33333,e333333,e3333333,e33333333;//再去編譯
結果所有對象也在棧中,隨機性過強,所以不再糾結這種編譯器的個例特性。浪費時間。
======================================================================================================================= 20160324:補充一個關於函數體與代碼塊的 關於入棧、出棧與棧幀,此文和前邊的一篇關於函數調用的博文已經寫得很清楚(也可能不清楚,兩篇有重合,歸類亂了點~~~~) 總之,關於函數的局部變數,因為是在存在當前棧幀的,所以外部變數在此時是完全屏蔽的,局部變數也活不到外部。 但是代碼塊是個特例(只是從表象知識點來說): 代碼塊可以聲明“局部”變數,而對這個“局部”變數的修改不會影響到上一層的“此”變數,說起來亂,上代碼:
#include<stdio.h>int main(){        int i = 0;        {                int j = 1;                i = 1;                printf("i:%d\n",i);                int i = 2;                printf("inside of block:i:%d\n",i);                printf("j:%d\n",j);        }        printf("outside of block:i:%d\n",i);}

i的列印結果是1,2,1. 這裡i是可以“重複聲明”的,並且聲明的局部的i的修改不會影響外部的。有些人根據表面概念,可能對此有些迷惑。這看似也是局部,代碼塊的局部和函數體的局部有什麼區別呢。
首先確定的是,如果不重複聲明,在代碼塊內的修改,對外部是全部生效的,這點通過for迴圈之類的代碼塊也會有所體會。 那麼如果重複聲明,區別在哪呢。其實這個“重複聲明”,不過是新聲明的一個變數罷了,你把它假設為k,這樣對k的任何修改,當然都不會影響i。

其他的機制啦,協調工作啦,都是編譯器自己映射好了,不用管了。
不過這樣做也有缺點,都要佔用棧空間嘛,又不是你真的去短暫覆蓋原變數過後再去恢複,沒有個記憶體用來記憶怎麼可能覆蓋了再恢複。 這樣的好處可能也就是你常用 for(int i = 0;condition;modify i); 這種東西的話,避免互相干擾吧。 所以,如果有需求,局部乾脆還是換個名字(k)吧,本質一樣免得降低可讀性。

====================================================================================================================== 關於main函數之前的那些彙編,我還不太懂,有空再摸索,想知道靜態變數的初始化,各種類,還有虛函數表的初始化過程。





相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.