1. 程式人生 > >關於C語言開大陣列溢位的問題

關於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分題。實在好奇什麼題的話……算了,貼個題目罷……