1. 程式人生 > >C\C++編譯器關於變數的記憶體分配順序總結

C\C++編譯器關於變數的記憶體分配順序總結


       關於《糾結的N皇后問題》中陣列越界導致的sum出現非正常的變化這個問題,繼而引發出關於記憶體到底是如何被分配的疑問,今天早上便著手進行探索,當然其中借鑑了廣大網友的總結,其中包括birdzb,NEO等牛人關於這方面的討論。特別是看到一些討論,感觸頗深啊http://www.programfan.com/club/showtxt.asp?id=191048。怎麼說呢,我還是覺得研究一下是有必要的,但要注意適可而止。

       知識儲備:1. 記憶體的分割槽:程式碼區,資料區,堆,棧。 四個區域司職不同,相互配合。

                         2. 變數的分類以及初始化情況:區域性變數,全域性變數,靜態的,非靜態的。C++裡面又包括成員變數。

一. 區域性變數

編譯器按照記憶體地址遞減的方式來給變數分配記憶體

區域性變數很多書籍中也叫自動變數,它宣告在函式塊內,作用範圍也在函式塊內。 不能被同一原始檔的其他函式使用,也不能被其他檔案中的函式使用。區域性變數儲存在棧中。無論區域性變數顯示初始化,或者未初始化,都只有當定義它們的程式塊被呼叫時(即執行時),才分配空間,宣告或定義時並不分配。區域性變數不是可執行模組的一部分!!除非顯示地對區域性變數進行初始化,否則,它們的初始值是不確定的。為了驗證“編譯器按照記憶體地址遞減的方式來給變數分配記憶體”,我們進行如下實驗:

測試一:

#include "iostream"
using namespace std;

void main() {
	int i,j;
	cout<<&i<<' '<<&j<<endl;
}

測試結果是:

&i = 0012FF44, &j = 0012FF40

可見,雖然i在j之前被定義,但在編譯器給變數分配記憶體時採用了記憶體地址遞減的方式,所以j在記憶體中的位置比i超前了4個位元組(因為是整型)。

測試二:

#include "iostream"
using namespace std;

void main() {
	int j,i;
	cout<<&i<<' '<<&j<<endl;
}

測試結果是:

&i = 0012FF40, &j =  0012FF44

根據上面說明的採用記憶體遞減的方式進行空間分配的話,首先分配的 j ,然後分配i,測試一首先分配i,然後分配j。

由此可見:在區域性變數分配空間的順序跟變數的宣告順序直接相關,同時按照記憶體由高到低的順序進行空間分配。

下面看一段程式碼可以加深對陣列越界出現昨天那個問題的理解。

int i,a[10];
 for(i=1;i<=10;i++)
  a[i]=0;

在for語句的比較部分本來是i<10;卻寫成了i<=10;因此實際上並不存在的a[10]被設定為0,也就是記憶體在陣列a之後的一個字(word)的記憶體被設定為0。如果用來編譯這段程式的編譯器按照記憶體地址遞減的方式來給變數分配記憶體,那麼記憶體中陣列a之後的一個字(word)實際上是分配給了整型變數i。此時本來迴圈計數器i的值為10,迴圈體內將並不存在的a[10]設定為0,實際上卻是將計數器i的值設定為0,這就陷入死迴圈。

#include <iostream.h>
void main()
{
 int i,a[4];
 cout<<&i<<endl<<&a[0]<<' '<<&a[1]<<' '<<&a[2]<<' '<<&a[3]<<' '<<&a[4]<<endl;
}

這段程式碼的執行結果:

0x0012FF44
0x0012FF34 0x0012FF38 0x0012FF3C 0x0012FF40 0x0012FF44
由此可以發現&i = &a[4],因為先宣告的是i,然後才是a[4],故i處於高位,a處於低位,但是越界後a[4]來到高位,覆蓋i

#include <iostream.h>
void main()
{
 int a[4],i;
 cout<<&i<<endl<<&a[0]<<' '<<&a[1]<<' '<<&a[2]<<' '<<&a[3]<<' '<<&a[4]<<endl;
}


這段程式碼的執行結果:

0x0012FF34
0x0012FF38 0x0012FF3C 0x0012FF40 0x0012FF44 0x0012FF48

由於首先宣告的是陣列a[4],於是在高位上優先分配a,低位上分配i,因此可以發現這是並沒有出現&i = &a[4]的情況。此時要是執行的話程式就不是假死了,直接是記憶體出錯。

二. 全域性變數

全域性變數沒有宣告在任何一個函式內,作用範圍在程式執行始終存在。能被同一原始檔的任何函式使用,也能被其他檔案中的函式使用,但要使用extern關鍵字。全域性變數儲存在資料段中。全域性變數顯示初始化時,或者未初始化時,在程式映像中有不同的分割槽:已初始化的全域性變數是可執行模組的一部分。未初始化的全域性變數則不是可執行模組的一部分,只有當定義它們的程式被呼叫時(即執行時),才分配空間,宣告或定義時並不分配。未初始化的全域性變數在執行時被初始化為0。

全域性變數的空間一般分配在資料區,因此並不像區域性變數那樣在函式被呼叫的時候才按照宣告順序由遞減的方式分配空間,全域性變數在編譯階段就會把空間分配完畢,這其中的機制根據編譯器的優化以及作業系統的原理都有很大的不同。

猜測一: 全域性變數的顯示初始為0與預設初始化為0效果一致,並不會導致記憶體分配地址出現不同,這時候遵循的規則是按章全域性變數命名的字母順序分配空間。

#include <iostream.h>
int a;
int c;
int b;
void main() {
	cout<<&a<<' '<<&b<<' '<<&c<<endl;
}

0x0042E058 0x0042E05C 0x0042E060

#include <iostream.h>

int c=0;
int a;
int b;
void main() {
	cout<<&a<<' '<<&b<<' '<<&c<<endl;
}


0x0042E058 0x0042E05C 0x0042E060

由上述程式碼可以推測當所有的變數都預設初始化,或者顯示初始化為0時,空間分配是按照宣告變數的字母順序按照空間遞增順序進行分配的。

新增陣列測試用例:

#include <iostream.h>
int a[2];
int b;
void main() {
	cout<<&a[0]<<' '<<&a[1]<<' '<<&a[2]<<endl<<&b<<endl;
}


0x0042E058 0x0042E05C 0x0042E060
0x0042E060

#include <iostream.h>
int b;
int a[2];
void main() {
	cout<<&a[0]<<' '<<&a[1]<<' '<<&a[2]<<endl<<&b<<endl;
}


0x0042E058 0x0042E05C 0x0042E060
0x0042E060
可見兩者的測試結果相同,說明的驗證是正確的,作為預設初始化或者顯示初始化為0的全域性變數,其記憶體的分配與宣告順序無關,與變數命名的字母順序有關。進一步測試:

#include <iostream.h>
int c[2];
int b;

void main() {
	cout<<&c[0]<<' '<<&c[1]<<' '<<&c[2]<<endl<<&b<<endl;
}


0x0042E05C 0x0042E060 0x0042E064
0x0042E058

#include <iostream.h>

int b;
int c[2];
void main() {
	cout<<&c[0]<<' '<<&c[1]<<' '<<&c[2]<<endl<<&b<<endl;
}


0x0042E05C 0x0042E060 0x0042E064
0x0042E058
陣列也滿足這樣的猜測,其記憶體的分配按照變數名的字母順序進行遞增分配。

但是這時候要是有個有個變數初始化為0,結果就不按字母順序進行遞增分配了,而是按照宣告順序進行。

$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$

上述猜測只對全域性變數的命名為單字母成立,且字母的排序按照ASCII碼中字母的順序進行比較。如果全域性變數的命名是多字母,或者陣列中數值是巨集並不是數字(若MAX = 4,那麼a[MAX]被分配的地址與a[4]有可能不同),甚至因為程式碼的優化也會影響到地址的分配。這時候的規律是很難找的,而且也沒有意義,最重要的是在自己寫程式的時候儘量不要出現程式越界之後溢位覆蓋的情形,那樣是在太危險了。

不得不說別人說的很有道理,猜測一直接 陷入混亂,可能編譯器的規則實在太多了,不能深究,掌握一些基本的就可以了。

$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$

猜測二:全域性變數初始化為非零的初始值時,記憶體分配按照宣告順序進行的

#include <iostream.h>
int b=1;
int c=2;
int a=1;

void main() {
	cout<<&a<<' '<<&b<<' '<<&c<<endl;
}


0x0042D708 0x0042D700 0x0042D704

#include <iostream.h>
int a=1;
int b=1;
int c=2;
void main() {
	cout<<&a<<' '<<&b<<' '<<&c<<endl;
}


0x0042D700 0x0042D704 0x0042D708
由上述兩個測試可以推測,當所有的全域性變數初始化為非零值時,空間分配按照宣告順序按空間遞增進行分配。 

總結

       事實上,全域性變數不管有沒有被初始化,其實都是被存放在DATA這個域中的,但是唯一不同的是這個DATA資料域有的時候又被劃分成幾個小的區域(說有的時候是因為並不是所有的系統都一定會這樣做),分成initialized和un-initialized,因此,我們討論的全域性變數預設初始化或者初始化為零時,資料儲存在un-initilized區域中,被初始化為非零時,資料儲存在initialized區域中