對C和C 語言記憶體的理解

2021-06-11 21:41:29 字數 3452 閱讀 2532

在c和c++語言開發中,指標、記憶體一直是學習的重點。因為c語言作為一種偏底層的中低階語言,提供了大量的記憶體直接操作的方法,這一方面使程式的靈活度最大化,同時也為bug埋下很多隱患。

因此,無論如何,我們都要對記憶體有乙個清晰的理解。

32位作業系統支援4gb記憶體的連續訪問,但通常把記憶體分為兩個2gb的空間,每個程序在執行時最大可以使用2gb的私有記憶體(0x00000000—0x7fffffff)。即理論上支援如下的大陣列:

char szbuffer[2*1024*1024*1024];
當然,由於在實際執行時,程式還有**段、臨時變數段、動態記憶體申請等,實際上是不可能用到上述那麼大的陣列的。

至於高階的2gb記憶體位址(0x80000000—0xffffffff),作業系統一般內部保留使用,即供作業系統核心**使用。在windows和linux平台上,一些動態鏈結庫(windows的dll,linux的so)以及ocx控制項等,由於是跨程序服務的,因此一般也在高2gb記憶體空間執行。

可以看到,每個程序都能看到自己的2gb記憶體以及系統的2gb記憶體,但是不同程序之間是無法彼此看到對方的。當然,作業系統在底層做了很多任務作,比如磁碟上的虛擬記憶體交換(請看下以標題),不同的記憶體塊動態對映等等。

虛擬記憶體的基本思想是:用廉價但緩慢的磁碟來擴充快速卻昂貴的記憶體。在一定時刻,程式實際需要使用的虛擬記憶體區段的內容就被載入物理記憶體中。當物理記憶體中的資料有一段時間未被使用,它們就可能被轉移到硬碟中,節省下來的物理記憶體空間用於載入需要使用的其他資料。

在程序執行過程中,作業系統負責具體細節,使每個程序都以為自己擁有整個位址空間的獨家訪問權。這個幻覺是通過「虛擬記憶體」實現的。所有程序共享機器的物理記憶體,當記憶體使用完時就用磁碟儲存資料。在程序執行時,資料在磁碟和記憶體之間來回移動。記憶體管理硬體負責把虛擬位址翻譯為實體地址,並讓乙個程序始終執行於系統的真正記憶體中,應用程式設計師只看到虛擬位址,並不知道自己的程序在磁碟與記憶體之間來回切換。

從潛在的可能性上說,與程序有關的所有記憶體都將被系統所使用,如果該程序可能不會馬上執行(可能它的優先順序低,也可能是它處於睡眠狀態),作業系統可以暫時取回所有分配給它的物理記憶體資源,將該程序的所有相關資訊都備份到磁碟上。

程序只能操作位於物理記憶體中的頁面。當程序引用乙個不在物理記憶體中的頁面時,mmu就會產生乙個頁錯誤。記憶體對此事做出響應,並判斷該引用是否有效。如果無效,核心向程序發出乙個「segmentation violation(段違規)」的訊號,核心從磁碟取回該頁,換入記憶體中,一旦頁面進入記憶體,程序便被解鎖,可以重新執行——程序本身並不知道它曾經因為頁面換入事件等待了一會。

對於程式設計師,我們最重要的是能理解不同程序間私有記憶體空間的含義。c和c++的編譯器把私有記憶體分為3塊:基棧、浮動棧和堆。如下圖:

(1)基棧:也叫靜態儲存區,這是編譯器在編譯期間就已經固定下來必須要使用的記憶體,如程式的**段、靜態變數、全域性變數、const常量等。

(2)浮動棧:很多書上稱為「棧」,就是程式開始執行,隨著函式、物件的一段執行,函式內部變數、物件的內部成員變數開始動態占用記憶體,浮動棧一般都有生命週期,函式結束或者物件析構,其對應的浮動棧空間的就拆除了,這部分內容總是變來變去,記憶體占用也不是固定,因此叫浮動棧。

(3)堆:c和c++語言都支援動態記憶體申請,即程式執行期可以自由申請記憶體,這部分記憶體就是在堆空間申請的。堆位於2gb的最頂端,自上向下分配,這是避免和浮動棧混到一起,不好管理。我們用到malloc和new都是從堆空間申請的記憶體,new比malloc多了物件的支援,可以自動呼叫建構函式。另外,new建立物件,其成員變數位於堆裡面。

我們來看乙個例子:

const

int n = 100;

void func(void

)

這個函式如果執行,其中n由於是全域性靜態變數,位於基棧,ch和pbuff這兩個函式內部變數,ch位於浮動棧,而pbuff指向的由malloc分配的記憶體區,則位於堆疊。

在記憶體理解上,最著名的例子就是執行緒啟動時的引數傳遞。

函式啟動乙個執行緒,很多時候需要向執行緒傳引數,但是執行緒是非同步啟動的,即很可能啟動函式已經退出了,而執行緒函式都還沒有正式開始執行,因此,絕不能用啟動函式的內部變數給執行緒傳參。道理很簡單,函式的內部變數在浮動棧,但函式退出時,浮動棧自動拆除,記憶體空間已經被釋放了。當執行緒啟動時,按照給的引數指標去查詢變數,實際上是在讀一塊無效的記憶體區域,程式會因此而崩潰。

那怎麼辦呢?我們應該直接用malloc函式給需要傳遞的引數分配一塊記憶體區域,將指標傳入執行緒,執行緒收到後使用,最後執行緒退出時,free釋放。

我們來看例子:

//

這個結構體就是參數列

typedef struct _clisten_listenaccepttask_param_

sclistenaccepttaskparam;

//習慣性寫法,設定結構體後,立即宣告結構體的尺寸,為後續malloc提供方便

const ulong sclistenaccepttaskparamsize = sizeof(sclistenaccepttaskparam);

//這裡接收到連線請求,申請引數區域,將關鍵資訊帶入引數區域,幫助後續執行緒工作。

bool clisten::listentaskcallback(void* pcallparam,int&nstatus) //

這是執行緒函式,負責處理上文accept到的socket

bool clisten::listenaccepttask(void* pcallparam,int&nstatus)

無規則的濫用記憶體和指標會導致大量的bug,程式設計師應該對記憶體的使用保持高度的敏感性和警惕性,謹慎地使用記憶體資源。

使用記憶體時最容易出現的bug是:

(1)壞指標值錯誤:在指標賦值之前就用它來引用記憶體,或者向庫函式傳送乙個壞指標,第三種可能導致壞指標的原因是對指標進行釋放之後再訪問它的內容。可以修改free語句,在指標釋放之後再將它置為空值。

free(p); p = null;
這樣,如果在指標釋放之後繼續使用該指標,至少程式能在終止之前進行資訊轉儲。

(2)改寫(overwrite)錯誤:越過陣列邊界寫入資料,在動態分配的記憶體兩端之外寫入資料,或改寫一些堆管理資料結構(在動態分配記憶體之前的區域寫入資料就很容易發生這種情況)

p = malloc(256); p[-1] = 0; p[256] = 0;
(3)指標釋放引起的錯誤:釋放同乙個記憶體塊兩次,或釋放一塊未曾使用malloc分配的記憶體,或釋放仍在使用中的記憶體,或釋放乙個無效的指標。乙個極為常見的與釋放記憶體有關的錯誤就是在 for(p=start;p=p->next) 這樣的迴圈中迭代乙個鍊錶,並在迴圈體內使用 free(p) 語句。這樣,在下一次迴圈迭代時,程式就會對已經釋放的指標進行解除引用操作,從而導致不可預料的結果。

我們可以這樣迭代:

struct node *p, *tart, *temp;

for(p = start; p ; p =temp)

談對C語言中記憶體分配的理解

在我們學習c語言的過程中,掌握記憶體分配是很有必要的。下面是我對記憶體分配的理解,如有不同的見解,請多多指教。在c語言中,物件可以使用靜態方式和動態的方式分配記憶體空間。靜態分配 編譯器在處理程式源 時分配。一般程式設計師在源 中直接定義了記憶體分配的大小,程式在執行的過程中,不會更改已經分配的空間...

對C語言指標的理解

c語言中有很多變數,比如說我們常見的int,float變數,而指標變數也是一種變數,不過不一樣的是,指標變數裡面存放的是位址。假設下面的是計算機的記憶體,每乙個小格格代表乙個位元組 1byte 一位元組等於8個位元位 1byte 8bit,能存放8位二進位制數 在c語言中,int佔4個位元組,cha...

對C語言指標的理解

之前,剛剛學習c語言的時候,學習其它資料型別的時候,感覺很好理解,但是學到指標的時候,筆者就蒙蔽了,因為看到課本上的文字,雲裡霧裡的,什麼指標,什麼指標變數,對於筆者來說都是傻傻分不清,感覺指標和指標變數好像一樣,或者試圖去背下來,但是老是搞混。那時候不知道,我的心思是在忙於社團工作,還是在哪個漂亮...