C語言

當前位置 /首頁/計算機/C語言/列表

詳解C/C++中堆和棧及靜態資料區

本文是本站小編搜尋整理的關於詳解C/C++中堆和棧及靜態資料區,供參考學習,希望對大家有所幫助!想了解更多相關資訊請持續關注我們應屆畢業生考試網!

詳解C/C++中堆和棧及靜態資料區

  五大記憶體分割槽

在C++中,記憶體分成5個區,他們分別是堆、棧、自由儲存區、全域性/靜態儲存區和常量儲存區。下面分別來介紹:

棧,就是那些由編譯器在需要的時候分配,在不需要的時候自動清除的變數的儲存區。裡面的變數通常是區域性變數、函式引數等。

堆,就是那些由new分配的記憶體塊,他們的釋放編譯器不去管,由我們的應用程式去控制,一般一個new就要對應一個delete。如果程式設計師沒有釋放掉,那麼在程式結束後,作業系統會自動回收。

自由儲存區,就是那些由malloc等分配的記憶體塊,他和堆是十分相似的,不過它是用free來結束自己的生命的。

全域性/靜態儲存區,全域性變數和靜態變數被分配到同一塊記憶體中,在以前的C語言中,全域性變數又分為初始化的和未初始化的,在C++裡面沒有這個區分了,他們共同佔用同一塊記憶體區(未初始化的變數都被初始化成0或空串,C中也一樣)。

常量儲存區,這是一塊比較特殊的儲存區,他們裡面存放的是常量,不允許修改(當然,你要通過非正當手段也可以修改,而且方法很多)。

  明確區分堆與棧:

在bbs上,堆與棧的區分問題,似乎是一個永恆的話題,由此可見,初學者對此往往是混淆不清的,所以我決定拿他第一個開刀。

  首先,我們舉一個例子:

void f() { int* p=new int[5]; }

上面這條短短的一句話就包含了堆與棧,看到new,我們首先就應該想到,我們分配了一塊堆記憶體,那麼指標p呢?他分配的是一塊棧記憶體,所以這句話的意思 就是:在棧記憶體中存放了一個指向一塊堆記憶體的指標p。在程式會先確定在堆中分配記憶體的大小,然後呼叫operator new分配記憶體,然後返回這塊記憶體的首地址,放入棧中,他在VC6下的彙編程式碼如下:

00401028 push 14h

0040102A call operator new (00401060)

0040102F add esp,4

00401032 mov dword ptr [ebp-8],eax

00401035 mov eax,dword ptr [ebp-8]

00401038 mov dword ptr [ebp-4],eax

這裡,我們為了簡單並沒有釋放記憶體,那麼該怎麼去釋放呢?是delete p麼?哦,錯了,應該是delete []p,這是為了告訴編譯器:我刪除的是一個數組,VC6就會根據相應的Cookie資訊去進行釋放記憶體的工作。

好了,我們回到我們的主題:堆和棧究竟有什麼區別?

  主要的區別由以下幾點:

1、管理方式不同;

2、空間大小不同;

3、能否產生碎片不同;

4、生長方向不同;

5、分配方式不同;

6、分配效率不同;

管理方式:對於棧來講,是由編譯器自動管理,無需我們手工控制;對於堆來說,釋放工作由程式設計師控制,容易產生memory leak。

空間大小:一般來講在32位系統下,堆記憶體可以達到4G的空間,從這個角度來看堆記憶體幾乎是沒有什麼限制的。但是對於棧來講,一般都是有一定的空間大小的,例如,在VC6下面,預設的棧空間大小是1M(好像是,記不清楚了)。當然,我們可以修改:

開啟工程,依次操作選單如下:Project->Setting->Link,在Category 中選中Output,然後在Reserve中設定堆疊的最大值和commit。

注意:reserve最小值為4Byte;commit是保留在虛擬記憶體的頁檔案裡面,它設定的較大會使棧開闢較大的值,可能增加記憶體的開銷和啟動時間

碎片問題:對於堆來講,頻繁的new/delete勢必會造成記憶體空間的不連續,從而造成大量的碎片,使程式效率降低。對於棧來講,則不會存在這個 問題,因為棧是先進後出的佇列,他們是如此的一一對應,以至於永遠都不可能有一個記憶體塊從棧中間彈出,在他彈出之前,在他上面的後進的棧內容已經被彈出, 詳細的可以參考資料結構,這裡我們就不再一一討論了。

生長方向:對於堆來講,生長方向是向上的,也就是向著記憶體地址增加的方向;對於棧來講,它的生長方向是向下的,是向著記憶體地址減小的方向增長。

分配方式:堆都是動態分配的,沒有靜態分配的堆。棧有2種分配方式:靜態分配和動態分配。靜態分配是編譯器完成的,比如區域性變數的分配。動態分配由alloca函式進行分配,但是棧的動態分配和堆是不同的,他的動態分配是由編譯器進行釋放,無需我們手工實現。

分配效率:棧是機器系統提供的資料結構,計算機會在底層對棧提供支援:分配專門的暫存器存放棧的地址,壓棧出棧都有專門的指令執行,這就決定了棧的 效率比較高。堆則是C/C++函式庫提供的,它的機制是很複雜的,例如為了分配一塊記憶體,庫函式會按照一定的演算法(具體的演算法可以參考資料結構/操作系 統)在堆記憶體中搜索可用的足夠大小的空間,如果沒有足夠大小的空間(可能是由於記憶體碎片太多),就有可能呼叫系統功能去增加程式資料段的記憶體空間,這樣就 有機會分到足夠大小的記憶體,然後進行返回。顯然,堆的效率比棧要低得多。

從這裡我們可以看到,堆和棧相比,由於大量 new/delete的使用,容易造成大量的記憶體碎片;由於沒有專門的系統支援,效率很低;由於可能引發 使用者態和核心態的切換,記憶體的申請,代價變得更加昂貴。所以棧在程式中是應用最廣泛的,就算是函式的呼叫也利用棧去完成,函式呼叫過程中的引數,返回地 址,EBP和區域性變數都採用棧的方式存放。所以,我們推薦大家儘量用棧,而不是用堆。

雖然棧有如此眾多的好處,但是由於和堆相比不是那麼靈活,有時候分配大量的記憶體空間,還是用堆好一些。

無論是堆還是棧,都要防止越界現象的發生(除非你是故意使其越界),因為越界的結果要麼是程式崩潰,要麼是摧毀程式的堆、棧結構,產生以想不到的結 果,就算是在你的程式執行過程中,沒有發生上面的問題,你還是要小心,說不定什麼時候就崩掉,那時候debug可是相當困難的:)

對了,還有一件事,如果有人把堆疊合起來說,那它的意思是棧,可不是堆,呵呵,清楚了?

  static用來控制變數的儲存方式和可見性

函式內部定義的變數,在程式執行到它的定義處時,編譯器為它在棧上分配空間,函式在棧上分配的空間在此函式執行結束時會釋放掉,這樣就產生了一個問 題: 如果想將函式中此變數的值儲存至下一次呼叫時,如何實現? 最容易想到的方法是定義一個全域性的變數,但定義為一個全域性變數有許多缺點,最明顯的缺點是破壞了此變數的訪問範圍(使得在此函式中定義的變數,不僅僅受此 函式控制)。

需要一個數據物件為整個類而非某個物件服務,同時又力求不破壞類的封裝性,即要求此成員隱藏在類的內部,對外不可見。

  static的內部機制:

靜態資料成員要在程式一開始執行時就必須存在。因為函式在程式執行中被呼叫,所以靜態資料成員不能在任何函式內分配空間和初始化。

這樣,它的空間分配有三個可能的地方,一是作為類的外部介面的標頭檔案,那裡有類宣告;二是類定義的內部實現,那裡有類的成員函式定義;三是應用程式的main()函式前的全域性資料宣告和定義處。

靜態資料成員要實際地分配空間,故不能在類的宣告中定義(只能宣告資料成員)。類宣告只宣告一個類的“尺寸和規格”,並不進行實際的記憶體分配,所以在類宣告中寫成定義是錯誤的。它也不能在標頭檔案中類宣告的外部定義,因為那會造成在多個使用該類的原始檔中,對其重複定義。

static被引入以告知編譯器,將變數儲存在程式的靜態儲存區而非棧上空間,靜態資料成員按定義出現的先後順序依次初始化,注意靜態成員巢狀時,要保證所巢狀的成員已經初始化了。消除時的順序是初始化的反順序。

  static的優勢:

可以節省記憶體,因為它是所有物件所公有的,因此,對多個物件來說,靜態資料成員只儲存一處,供所有物件共用。靜態資料成員的值對每個物件都是一樣,但它的值是可以更新的。只要對靜態資料成員的值更新一次,保證所有物件存取更新後的相同的值,這樣可以提高時間效率。

引用靜態資料成員時,採用如下格式:

<類名>::<靜態成員名>

如果靜態資料成員的訪問許可權允許的話(即public的成員),可在程式中,按上述格式來引用靜態資料成員。

PS:

(1)類的靜態成員函式是屬於整個類而非類的物件,所以它沒有this指標,這就導致了它僅能訪問類的靜態資料和靜態成員函式。

(2)不能將靜態成員函式定義為虛擬函式。

(3)由於靜態成員聲明於類中,操作於其外,所以對其取地址操作,就多少有些特殊,變數地址是指向其資料型別的指標,函式地址型別是一個“nonmember函式指標”。

(4)由於靜態成員函式沒有this指標,所以就差不多等同於nonmember函式,結果就產生了一個意想不到的好處:成為一個callback函式,使得我們得以將C++和C-based X Window系統結合,同時也成功的應用於執行緒函式身上。

(5)static並沒有增加程式的時空開銷,相反她還縮短了子類對父類靜態成員的訪問時間,節省了子類的記憶體空間。

(6)靜態資料成員在<定義或說明>時前面加關鍵字static。

(7)靜態資料成員是靜態儲存的,所以必須對它進行初始化。

(8)靜態成員初始化與一般資料成員初始化不同:

初始化在類體外進行,而前面不加static,以免與一般靜態變數或物件相混淆;

初始化時不加該成員的訪問許可權控制符private,public等;

初始化時使用作用域運算子來標明它所屬類;

所以我們得出靜態資料成員初始化的格式:

<資料型別><類名>::<靜態資料成員名>=<值>

(9) 為了防止父類的影響,可以在子類定義一個與父類相同的靜態變數,以遮蔽父類的影響。這裡有一點需要注意:我們說靜態成員為父類和子類共享,但 我們有重複定義了靜態成員,這會不會引起錯誤呢?不會,我們的編譯器採用了一種絕妙的手法:name-mangling 用以生成唯一的標誌。

  C語言變數的儲存類別

記憶體中供使用者使用的儲存空間分為程式碼區與資料區兩個部分。變數儲存在資料區,資料區又可分為靜態儲存區與動態儲存區。

靜態儲存是指在程式執行期間給變數分配固定儲存空間的方式。如全域性變數存放在靜態儲存區中,程式執行時分配空間,程式執行完釋放。

動態儲存是指在程式執行時根據實際需要動態分配儲存空間的方式。如形式引數存放在動態儲存區中,在函式呼叫時分配空間,呼叫完成釋放。

對於靜態儲存方式的變數可在編譯時初始化,預設初值為O或空字元。對動態儲存方式的變數如不賦初值,則它的值是一個不確定的值。

在C語言中,具體的儲存類別有自動(auto)、暫存器(register)、靜態(static)及外部(extern)四種。靜態儲存類別與外部儲存類別變數存放在靜態儲存區,自動儲存類別變數存放在動態儲存區,暫存器儲存類別直接送暫存器。

  變數儲存類別定義方法:

儲存類別型別變量表;

例如:

(1)a,b,c為整型自動儲存類別變數:

auto int a,b,c;

(2)x,y,z為雙精度型靜態儲存類別變數:

static double x,y,z;

  1、變數有哪些儲存型別?

變數的儲存型別由“儲存型別指明符”來說明。儲存型別指明符可以是下列類鍵字之一:

auto

register

extern

static

  下面是詳細的解釋:

auto 儲存類指明符--用於說明具有區域性作用域的變數,它表示變數具有區域性(自動)生成期,但由於它是所有區域性作用域變數說明的預設儲存類指明符,所以使用得很 少。要注意的是,所有在函式內部定義的變數都是區域性變數,函式內部定義的變數其作用域只在函式內部。它的生存期為該函式執行期間,一旦離開這個函式或這個 函式終止,區域性變數也隨之消失。

register 儲存類指明符--當聲明瞭這個指明符後,編譯程式將盡可能地為該變數分配CPU內部的暫存器作為變數的儲存單元,以加快執行速度。注意,暫存器與儲存器是 不同的。暫存器一般在CPU內部,而儲存器一般指外部的(比如記憶體條),CPU內部的暫存器其運算速度是很高的。當暫存器已分配完畢,就自動地分配一個外 部的記憶體。它的作用等價於auto,也只能用於區域性變數和函式的參量說明。

static 儲存類指明符--表示變數具有靜態生成期。static變數的的特點是它離開了其作用域後,其值不會消失。

當回到該作用域之後又可以繼續使用這個static變數的值。

例:利用static變數統計呼叫函式的次數

int two(); /*函式原型說明*/

void main()

{

int a=0;

a=two(); /*a的值等於1*/

a=two() /*a的值等於2*/

a=two(); /*a的值等於3*/

}

int two()

{

static int b=0;    /*定義了一個區域性的static變數*/

b ;

return b;

}

如果不是一個static變數就不會有這個效果了

int two(); /*函式原型說明*/

void main()

{

int a=0;

a=two(); /*a的值等於1*/

a=two() /*a的值等於1*/

a=two(); /*a的值等於1*/

}

int two()

{

int b=0;

b ;

return b;

}

變數a的值總是1,原因是在函式two()中,變數b不是一個static變數,其值隨著離開two函式就消失了,當回到two函式時又被重新賦值0。

extern 儲存類指明符--一般用在工程檔案中。在一個工程檔案中因為有多個程式檔案,當某一個變數在一個程式檔案中定義了之後,如果在另一個程式檔案中予以定義, 就會出現重複定義變數的錯誤。使用extern儲存型別指明符就可以指出在該檔案外部已經定義了這個變數。extern變數的作用域是整個程式。

2、變數儲存在記憶體的什麼地方?

1)變數可以儲存在記憶體的不同地方,這依賴於它們的`生成期。在函式上部定義的變數(全域性變數或static外部變數)和在函式內部定義的static變 量,其生存期就是程式執行的全過程。這些變數被儲存在資料段(Data Segment)中。資料段是在記憶體中為這些變數留出的一段大小固定的空間,它分 為二部分,一部分用來初始化變數,另一部分用來存放未初始化的變數。

2)在函式內部定義的auto變數(沒有用關鍵字static定義的變數)的生成期從程式開始執行其所在的程式塊程式碼時開始,到程式離開該程式塊時為止。 作為函式引數的變數只在呼叫該函式期間存在。這些變數被儲存在棧(stack)中。棧是記憶體中的一段空間,開始很小,以後逐漸自動變大,直到達到某個預定 義的界限。

3)當用malloc等函式給指標分配一個地址空間的時候,這個分配的記憶體塊位於一段名為“堆(heap)”的記憶體空間中。堆開始時很小,但呼叫 malloc或clloc等記憶體分配函式時它就會增大。堆可以和資料段或棧共用一個記憶體段,也可以有它自己的記憶體段,這完全取決於編譯選項和作業系統。與 棧相似,堆也有一個增長界限,並且決定這個界限的規則與棧相同。

  C語言變數的作用域和儲存型別

  一、作用域和生存期

C程式的識別符號作用域有三種:區域性、全域性、檔案。識別符號的作用域決定了程式中的哪些語句可以使用它,換句話說,就是識別符號在程式其他部分的可見性。通常,識別符號的作用域都是通過它在程式中的位置隱式說明的。

  1.區域性作用域

前面各個例子中的變數都是區域性作用域,他們都是宣告在函式內部,無法被其他函式的程式碼所訪問。函式的形式引數的作用域也是區域性的,它們的作用範圍僅限於函式內部所用的語句塊。

void add(int);

main()

{

int num=5;

add(num);

printf("%dn",num); /*輸出5*/

}

void add(int num)

{

num++;

printf("%dn",num); /*輸出6*/

}

上面例子裡的兩個num變數都是區域性變數,只在本身函式裡可見。前面我們說了,在兩個函數出現同名的變數不會互相干擾,就是這個道理。所以上面的兩個輸出,在主函式裡仍然是5,在add()函式裡輸出是6。

TAG標籤:cc 中堆 棧及 靜態資料 #