1. 程式人生 > >堆疊和記憶體分配

堆疊和記憶體分配

一:記憶體管理概述:

圖一
如圖一所示,在計算機中,主要分為以上儲存區域中,分別是:硬碟、記憶體、高階快取 和暫存器。執行程式後,他們的執行速率自下而上(圖一)加快,與之相應的造價越高,其中,硬碟的執行效率最慢,暫存器的效率最快。在這幾個區域裡,今天重點介紹一下記憶體。在C++ 中,記憶體主要分為五個區,分別是:程式碼區、棧區、堆區、靜態(全域性)資料區。

(一)、程式碼區:

程式碼區是用來儲存程式的所有程式碼,以及字串常量等在編譯期間就能確定的值,在程式的整個生命週期內, 在常量資料區的資料都是可用的。在這個區域內,所有的資料都是隻讀的,不可以修改本區域的資料,之所以這樣,是因為在實際的實現中,最底層內部儲存格式的實現會使用特定的優化方案。比如說,編譯器可能只把字串常量儲存一次,而在幾個重疊的物件裡面引用它 。

(二)、棧區:

1、定義:

棧區主要存放編譯器在需要的時候自動分配,在不需要的時候自動銷燬的變數。主要是區域性變數和函式的引數等,在函式呼叫和傳參的時候,編譯器為區域性變數或形參開闢空間,注意,在這塊空間中,編譯器並不會自動對它進行任何的初始化,它所儲存的不是0,而是一個隨機值(可能是該儲存區上次被使用後的值),在函式結束後,所開闢的空間將自動銷燬,裡面所存的內容將不復存在,也就是釋放儲存區的內容。 這就是為什麼老師們在講課中,最喜歡用的字眼:引數壓棧和彈出。

2、易錯點:

在這裡,不得不說明,當一個自動變數的地址被儲存在一個生命期長於它的指標時,自動變數被釋放後,該指標就成了一個“懸空指標”,這一點是非常可怕的,因為“懸空指標的內容無法預測的”

(三):全域性靜態(資料)區:

全域性(靜態)資料區:顧名思義,它是用來儲存全域性靜態變數的儲存區域。只有在程式啟動的時候才被分配,直到程式開始執行時才被初始化,比如:函式的靜態變數就是在程式執行到定義該變數的程式碼時才被初始化的。在靜態區資料區中沒有被初始化的區域可以通過void* 指標來訪問或操縱,但是,static定義的靜態變數只能在本檔案中使用,不可在其它檔案中宣告使用。

(四)、堆區:

1、定義:

堆區是一個動態的儲存區域,使用庫函式malloc()和free(),和操作符new和delete以及一些相關變數來進行分配和回收,在堆區中,物件的生命週期可以比它村在記憶體中的生命週期短,換句話說:程式可以獲得一片記憶體區域而不用馬上對它進行初始化,同時,在物件被銷燬後,也不用馬上收回它所佔用的記憶體區,在這段時間內,使用者可以還可以用void*型的指標訪問這片區域,但是原始物件的非靜態區以及成員函式都不能被訪問或者操縱,因為我們知道實際上物件已經不存在了。

下面我們就來具體看看動態記憶體分配到底是怎麼分配的。

一、好奇心不僅害死貓:

(一)、陣列元素的儲存方式:int array[8];

圖二
如圖所示,陣列元素在記憶體中是連續存放的,當一個數組被宣告時, 它所需要的記憶體在編譯時就被分配。但是,在建立陣列時,須用一個常量來指定該陣列的長度,而陣列本身長度常常在執行時才知道,它所佔記憶體空間的大小便取決於輸入資料,在這種情況下,我們通常採取的解決辦法便是宣告一個比較大的陣列,以確保它能夠容納足夠多的資料。

(二)、優缺點:

1、優點:定義陣列時簡單、陣列大小一目瞭然。
2、缺點:有以下三點

(1)、此類宣告在程式中引入了人為的限制,如果程式需要使用的元素數量超過了先前宣告的長度,則無法處理。
(2)、解決(1)中的問題,毫無疑問會想到把陣列的宣告更大一些,但是,如果程式的元素數量比較少時,會造成大量的空間被浪費掉(如圖三)
(3)、若元素個數遠遠大於陣列容納範圍時,程式因做出合理的響應,不應該由此失敗
圖三
那麼,這時候,人們好奇,有沒有一種可能,使得我要多大的的記憶體就有多大? 我想存什麼型別的資料就開闢相應位元組大小的空間呢?正是因為這些好奇心的出現,才有了我們現在的動態記憶體分配。

二、malloc和free共同維護記憶體池

在C語言中,提供了兩個特殊的函式,malloc和free 。

(一) malloc:

(1)、用途:

malloc用於動態記憶體分配,當程式在執行過程中需要一定的記憶體空間時, 就呼叫malloc函式,由它從記憶體池中取出相應大小的空間,並返回指向該空間的指標。(注意:該空間此時沒有以任何方式進行初始化)

(2)、宣告標頭檔案:stdlib.h
(3)、函式形式
 void *  malloc( size_t size );//void* 任意型別
(4)、使用說明:

malloc 所分配的是一塊連續的記憶體,它的引數部分是需要分配的位元組數,(A)如果記憶體中的可用記憶體可以滿足這個需求,就返回一個指向被分配的記憶體塊起始位置的指標。例如:如果請求分配100個位元組大小的記憶體,那麼它分配的記憶體就是100個連續的位元組,並不會分開位於多塊不同的區域,同時,實際分配的記憶體可能會比請求的稍微多一點,該行為是由編譯器決定的, 故而,程式設計師不能指望它分配過多多餘的空間。(B)、倘若記憶體池是空的,或者它的可用記憶體無法滿足請求時,malloc會向作業系統,申請得到更多的記憶體,並在這塊新的記憶體上執行分配任務,若作業系統無法向malloc提供更多的記憶體,那麼,它就返回一個NULL指標。(C)對每個從malloc返回的指標都進行檢查,確保它非NULL指標是非常重要的。

(二):free()

free( )用於釋放動態開闢的記憶體,它的引數要麼是malloc( )返回的值,要麼是NULL, 向free( )傳遞一個NULL引數不會產生任何效果。

從堆上分配的空間稱為動態記憶體分配,程式在執行的時候,使用malloc()或操作符new 申請任意多的記憶體,由程式設計師自己負責使用free()和delete釋放記憶體,這樣,動態記憶體的生命週期由使用者決定,使用非常靈活。

(三)在堆、棧、和靜態分配空間的生命週期如下:

圖四

細心的人可能會發現,我上面說,用於動態記憶體開闢的,除了malloc和free外,還說了一對“操作符”new和delete;那麼,都是用於動態記憶體的開闢,兩者之間有什麼不同之處呢? 下面我們就共同來探討吧。

三、new和delete的詳細解析:

操作符new和delete究竟做了什麼?讓我們一起看一下下面這段程式碼吧

class A
{
    public:
            A()
            {
                cout<<"A is here !"<<endl;
            }
            ~A()
            {
                cout<<"A is dead !"<<endl;
            }
            private:
                int i;
};
   A* pA =new A;
     delete pA;

在上述程式中,new主要做了兩件事,首先呼叫new操作符,在堆上分配一個A 型別物件大小的記憶體空間,相當於有了一塊地基,然後呼叫A型別的建構函式A(),在這塊已有的空間上添磚砌瓦,建造一個屬於A的建築(物件)。
對於delete 操作符,它做了與之恰恰相反的兩件事,首先呼叫解構函式,銷燬物件,在呼叫delete操作符,釋放記憶體。

A:new

圖五

B:delete

圖六

用new和delete開闢的是一片記憶體,那麼,如果想像陣列一樣,一次性開闢多個記憶體,該怎麼辦呢?

現在就要用到new T[N ]和delete T[N];

a、new T[N ]開闢連續空間

這裡寫圖片描述

b、delete T[N]銷燬連續空間

delete [ ]銷燬空間和delete相似,唯一不同的是,需要從空間的首地址向前推四個位元組開始釋放。