關於C語言開大數組溢出的問題

       上週的CCF/CSP認證成績出來了,第四題用粗暴的Dijkstra的思想強行遍歷,本來估計能拿個60分,結果爆0分,耿耿於懷。

       我考試用的是C++。

#include<iostream>
using namespace std;
int main()
{
    int dis[8000][8000];
    //代碼
}

       沒記錯的話,當時是因爲像上面代碼一樣在main函數裏面開了個8000*8000的數組(這道題用Vector來模擬鏈表確實是很省空間的做法,但既然用了O(n^2)的算法,就不指望能過60分以後的數據了,還有,這篇文章的重心不在怎麼解題,所以我就不放題目了),結果DEV-CPP編譯每次都報溢出,我就想着估計是DEV-CPP的問題吧,畢竟我平時用的都是CodeBlocks。當時還仔細算了下:

       8000*8000*4/1024/1024≈244 MB.

       8192*8192*4/1024/1024≈256 MB.

       題目給了256MB的內存,能開8192*8192的數組,只開8000*8000怎麼樣都不應該爆空間吧。於是開着100*100的小數組,將各種可能的樣例測試通過後,我就提交了代碼,提交前刻意把100*100的改回8000*8000,想盡可能地蹭分。

       結果——這道題0分了。


       我不服,於是我用CodeBlocks將代碼重新敲了一遍……吶,結果如下,看來不是代碼編輯器配置的問題了。


      

       再之後,改成1000*1000也會溢出。不科學啊?我以前刷過一些OJ,也開過百萬級別的數組而且沒有報錯過啊!我立刻查了幾道曾經在hduoj上提交的題目的代碼,數組都是這麼開的:

#include<iostream>
using namespace std;
int dis[8000][8000];
int main()
{
    //代碼
}

       區別很明顯,開成全局數組。改了重新碼了一遍的代碼,編譯一下,果然通過了。提交了代碼,也果然是我預計的60分(別說我沒追求),空間使用也在估計的範圍左右……



       △所以,發生了啥?爲啥數組作爲全局變量空間可以開那麼大,作爲main函數裏面的局部變量卻只能開那麼小?

       這就涉及到了C語言的內存分配問題,上課的時候都聽說過,C語言佔用的內存可以分爲5個區:

           ①代碼區(Text Segment):不難理解,就是用於放置編譯過後的代碼的二進制機器碼。

           ②堆區(Heap):用於動態內存分配。一般由程序員分配和釋放,若程序員不釋放,結束程序時有可能由操作系統回收。(其實就是malloc()函數能夠掌控的內存區域)

           ③棧區(Stack):由編譯器自動分配和釋放,一般用來存放局部變量、函數參數(敲黑板劃重點了!)。

           ④全局初始化數據區/靜態數據區(Data Segment):顧名思義,就是存放全局變量靜態變量的地方。這個區域被整個進程共享。

           ⑤未初始化數據區(BSS):在運行時改變值。改變值(初始化)的時候,會根據它是全局變量還是局部變量,進到他們該去的區。否則會持續待在BSS裏面與世無爭。(待會兒會用實驗來證明並感受它的存在。)


       代碼區和堆區不贅述,不在我關注的範圍內。貌似空間大小取決於內存的大小和CPU的尋址空間。

       我今天只講一講Stack和Data Segment這兩個導致我爆0分的玩意兒。

       在Windows下,Data Segment的所允許的空間大小取決於剩餘內存的大小,也就是說,如果電腦剩餘8G內存的話,int類型的二維數組甚至可以開到46340*46340的大小;

       而Stack的空間只有2M!!也就是2*1024*1024=2097152字節,局部變量空間頂多放得下下524288個int類型!


       行吧,知道上述幾個關鍵後,一開始的問題就不是問題了。但我想在局部中開一個大數組怎麼辦?很簡單,將它歸到Data Segment中:

#include<iostream>
using namespace std;
int main()
{
    static int dis[8000][8000];
    //代碼
}

       由於靜態變量和全局變量一樣,都是存在Data Segment中的,所以這麼做,相當於把大數組開在了Data Segment中,不會因爲堆棧溢出2M空間而報錯了。(這樣做的話,需要注意局部函數的初始化)。

      





       △深入:BSS區的存在!

       其實,文章本來在這裏就要結束了,但是我閒着蛋疼,手動二分,強行找到了Stack區所能開的int數組的大小(真實情況下不可能是2M剛好),然後發現了神奇的一幕(後來才知道BSS區的存在),於是乾脆就寫出來了:

#include<iostream>
using namespace std;
int main()
{
    int dis[520072]; //520072
}

       520072是我手動二分得到的結果,如果開520073的話會堆棧溢出(不同電腦不知道一不一樣)。

      520073*4/1024/1024≈1.984MB(接近2MB,沒毛病)

       然而,神奇的一幕出現了:

#include<iostream>
using namespace std;
int main()
{
    int dis[520072];//520072
    int a1;int a2;int a3;int a4;int a5;int a6;int a7;int a8;int a9;
    int b1;int b2;int b3;int b4;int b5;int b6;int b7;int b8;int b9;
}

       理論上,我開第520073個整型出來的時候,編譯器應該報堆棧溢出的錯誤纔對,然而並沒有!然而並沒有!

       這就涉及到了我剛纔提到的未初始化數據區(BSS)的存在了——在運行時改變值。改變值(初始化)的時候,會根據它是全局變量還是局部變量,進到他們該去的區。否則會持續待在BSS裏面與世無爭。

       在給未初始化的變量賦值之前,它們始終待在BSS區,所以Stack區並不會溢出。而在局部定義數組的時候,數組會自動初始化爲全0,所以數組在剛被定義的時候就塞進Stack區了,纔會出現int dis[520073]直接報堆棧溢出的問題。

       證明上述說法的代碼如下:

#include<iostream>
using namespace std;
int main()
{
    int dis[520072];//520072
    int a=10;
}
這段代碼在運行時會堆棧溢出。

#include<iostream>
using namespace std;
int main()
{
    int dis[520072];//520072
    int a;
    while(1){}
    a=10;
}
這段代碼會正常進入循環中,始終不會報堆棧溢出。

      



       行了,就說這麼多了,祭奠我可憐的0分題。實在好奇什麼題的話……算了,貼個題目罷……







發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章