1. 程式人生 > >C語言、記憶體管理、堆、棧、動態分配

C語言、記憶體管理、堆、棧、動態分配

昨晚整理了一晚上居然沒了?沒儲存還是沒登入我也忘了,賊心累

我捋了捋,還是得從作業系統,程序和記憶體開始理解。

程序

    從作業系統的角度簡單介紹一下程序。程序是佔有資源的最小單位,這個資源當然包括記憶體。在現代作業系統中,每個程序所能訪問的記憶體是互相獨立的(一些交換區除外)。而程序中的執行緒所以共享程序所分配的記憶體空間

    在作業系統的角度來看,程序=程式+資料+PCB(程序控制塊)

記憶體單位和編址

  • 位 :( bit ) 是電子計算機中最小的資料單位。每一位的狀態只能是0或1。
  • 位元組:1 Byte = 8 bit ,是記憶體基本的計量單位,
  • KB :1KB = 1024 Byte。也就是1024個位元組。MB : 1MB = 1024 KB。類似的還有GB、TB。
  • 記憶體編址計算機中的記憶體按位元組編址,每個地址的儲存單元可以存放一個位元組(8個bit)的資料,CPU通過記憶體地址獲取指令和資料,並不關心這個地址所代表的空間具體在什麼位置、怎麼分佈,因為硬體的設計保證一個地址對應著一個固定的空間,所以說:記憶體地址和地址指向的空間共同構成了一個記憶體單元。
  • 記憶體地址記憶體地址通常用十六進位制的資料表示,C、C++規定,16進位制數必須以 0x 開頭。使用十六進位制表示一個記憶體地址是因為:1,二進位制、十進位制、十六進位制之間相互轉換比較方便;2,一位十六進位制數可以表示4個二進位制位數,更大的數使用十六進位制數表示更加精短。3,計算機硬體設計需要。

為什麼32位機器最大隻能用到4GB記憶體

        在使用計算機時,其最大支援的記憶體是由  作業系統 和 硬體 兩方面決定的。

 硬體方面在計算機中 CPU的地址匯流排數目 決定了CPU 的 定址 範圍,這種由地址匯流排對應的地址稱作為實體地址。假如CPU有32根地址匯流排(一般情況下32位的CPU的地址匯流排是32位,也有部分32位的CPU地址匯流排是36位的,比如用做伺服器的CPU),那麼提供的可定址實體地址範圍 為 2^32=4GB(在這裡要注意一點,我們平常所說的32位CPU和64位CPU指的是CPU一次能夠處理的資料寬度,即位寬,不是地址匯流排的數目)。自從64位CPU出現之後,一次便能夠處理64位的資料了,其地址匯流排一般採用的是36位或者40位(即CPU能夠定址的實體地址空間為64GB或者1T)。CPU訪問任何儲存單元必須知道其實體地址。

  使用者在使用計算機時能夠訪問的最大記憶體不單是由CPU地址匯流排的位數決定的,還需要考慮作業系統的實現。實際上使用者在使用計算機時,程序所訪問到的地址是邏輯地址,並不是真實的實體地址,這個邏輯地址是作業系統提供的,CPU在執行指令時需要先將指令的邏輯地址變換為實體地址才能對相應的儲存單元進行資料的讀取或者寫入(注意邏輯地址和實體地址是一一對應的)。

  對於32位的windows作業系統,其邏輯地址編碼採用的地址位數是32位的,那麼作業系統所提供的邏輯地址定址範圍是4GB,而在intel x86架構下,採用的是記憶體對映技術(Memory-Mapped I/O, MMIO),也就說將4GB邏輯地址中一部分要劃分出來與BIOS ROM、CPU暫存器、I/O裝置這些部件的實體地址進行對映,那麼邏輯地址中能夠與記憶體條的實體地址進行對映的空間肯定沒有4GB了.基於以上的理論可以知道32位的系統,其最多隻能管理的運存只有:

                                                 2^32B=2^(2+10+10+10)B=2^2*(2^10*2^10*2^10)B=4GB。

       所以當我們裝了32位的windows作業系統,即使我們買了4GB的記憶體條,實際上能被作業系統訪問到的肯定小於4GB,一般情況是3.2GB左右。假如說地址匯流排位數沒有32位,比如說是20位,那麼CPU能夠定址到1MB的實體地址空間,此時作業系統即使能支援4GB的邏輯地址空間並且假設記憶體條是4GB的,能夠被使用者訪問到的空間不會大於1MB(當然此處不考慮虛擬記憶體技術),所以使用者能夠訪問到的最大記憶體空間是由硬體和作業系統兩者共同決定的,兩者都有制約關係。

C程式記憶體分配

對於一個由C語言編寫的程式而言,記憶體主要可以分為以下5個部分組成:

其中需要注意的是:程式碼段、資料段、BSS段在程式編譯期間由編譯器分配空間,在程式啟動時載入,由於未初始化的全域性變數存放在BSS段,已初始化的全域性變數存放在資料段,所以程式中應該儘量少的使用全域性變數以節省程式編譯和啟動時間;棧和堆在程式執行中由系統分配空間。

  • 程式碼區(text):用來存放CPU執行的機器指令(machine instructions),也有可能包含一些只讀的常數變數,例如字串常量等。通常,程式碼區是可共享的(即另外的執行程式可以呼叫它),因為對於頻繁被執行的程式,只需要在記憶體中有一份程式碼即可。這部分割槽域的大小在程式執行之前就已經確定,通常是只讀的,使其只讀的原因是防止程式意外地修改了它的指令。另外,程式碼區還規劃了局部變數的相關資訊。

全域性資料區(靜態區)(static):全域性變數和靜態變數的儲存是放在一塊的,初始化的全域性變數和靜態變數在一塊區域(資料區),未初始化的全域性變數和靜態變數在相鄰的另一塊區域(BSS區)。另外文字常量區,常量字串就是放在這裡,程式結束後由系統釋放。

  • 資料區(全域性初始化資料區 data):該區包含了在程式中明確被初始化的全域性變數、靜態變數(包括全域性靜態變數和區域性靜態變數)和常量資料(如字串常量)
  • BSS區(未初始化資料區。):存入的是全域性未初始化變數。BSS這個叫法是根據一個早期的彙編運算子而來,這個彙編運算子標誌著一個塊的開始。BSS區的資料在程式開始執行之前被核心初始化為0或者空指標(NULL)。llinux環境下可以用size命令 檢視C程式的儲存空間佈局,可以看出,此可執行程式在儲存時(沒有調入到記憶體)分為程式碼區(text)、資料區(data)和未初始化資料區(bss)3個部分。
[email protected]:~/桌面$ file struct
struct: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=0xb522d8002f06f33d9ce3d3b68a94ebc1910f4502, not stripped
[email protected]:~/桌面$ size struct
   text	   data	    bss	    dec	    hex	filename
   1366	    688	     16	   2070	    816	struct

 一個正在執行著的C編譯程式佔用的記憶體分為程式碼區、初始化資料區、未初始化資料區、堆區和棧區5個部分。

  • 棧儲存區:

(1)由編譯器自動分配釋放,通常存放程式臨時建立的區域性變數(但不包括static宣告的變數,static意味著在資料段中存放變數),即函式括大括號 “{ }” 中定義的變數,其中還包括函式呼叫時其形參,呼叫後的返回值等。

(2)呼叫原理:每當一個函式被呼叫,該函式返回地址和一些關於呼叫的資訊(比如某些暫存器的內容),被儲存到棧區。然後這個被呼叫的函式再為它的自動變數和臨時變數在棧區上分配空間,這就是C實現函式遞迴呼叫的方法。每執行一次遞迴函式呼叫,一個新的棧框架就會被使用,這樣這個新例項棧裡的變數就不會和該函式的另一個例項棧裡面的變數混淆

(3)棧是由高地址向低地址擴充套件的資料結構,有先進後出的特點,即依次定義兩個區域性變數,首先定義的變數的地址是高地址,其次變數的地址是低地址。函式引數進棧的順序是從右向左(主要是為了支援可變長引數形式)。

4)最後棧還具有“小記憶體、自動化、可能會溢位”的特點。棧頂的地址和棧的最大容量一般是系統預先規定好的,通常不會太大。由於棧中主要存放的是區域性變數,而區域性變數的佔用的記憶體空間是其所在的程式碼段或函式段結束時由系統回收重新利用,所以棧的空間是迴圈利用自動管理的,一般不需要人為操作。如果某次區域性變數申請的空間超過棧的剩餘空間時就有可能出現 “棧的溢位”,進而導致意想不到的後果。所以一般不宜在棧中申請過大的空間,比如長度很大的陣列、遞迴呼叫重複次數很多的函式等等。

  • 堆儲存區:

(1)通常存放程式執行中動態分配的儲存空間。它的大小,並不固定,可動態擴張或縮放。當程序呼叫malloc/free等函式分配記憶體時,新分配的記憶體就被動態新增到堆上(堆被擴張)/釋放的記憶體從堆中被提出(堆被縮減)。

(2)堆與資料結構中的堆是兩回事,分配方式倒是類似於連結串列。

(3)堆是低地址向高地址擴充套件的資料結構,是一塊不連續的記憶體區域。在標準C語言上,使用malloc等記憶體分配函式是從堆中分配記憶體的,在Objective-C中,使用new建立的物件也是從堆中分配記憶體的。

(4)堆具有“大記憶體、手工分配管理、申請大小隨意、可能會洩露”的特點,堆記憶體是作業系統劃分給堆管理器來管理的,管理器向使用者(使用者程序)提供API(malloc和free等)來使用堆記憶體。需要程式設計師手動分配釋放,如果程式設計師在使用完申請後的堆記憶體卻沒有及時把它釋放掉,那麼這塊記憶體就丟失了(程序自身認為該記憶體沒被使用,但是在堆記憶體記錄中該記憶體仍然屬於這個程序,所以當需要分配空間時又會重新去申請新的記憶體而不是重複利用這塊記憶體),就是我們常說的-記憶體洩漏,所以記憶體洩漏指的是堆記憶體被洩露了。

之所以分成這麼多個區域,主要基於以下考慮:

  • 一個程序在執行過程中,程式碼是根據流程依次執行的,只需要訪問一次,當然跳轉和遞迴有可能使程式碼執行多次,而資料一般都需要訪問多次,因此單獨開闢空間以方便訪問和節約空間。
  • 臨時資料及需要再次使用的程式碼在執行時放入棧區中,生命週期短。
  • 全域性資料和靜態資料有可能在整個程式執行過程中都需要訪問,因此單獨儲存管理。
  • 堆區由使用者自由分配,以便管理。
#include <stdio.h>
int a = 0;                  // 全域性初始化區
char p1;                    // 全域性未初始化區
int main(int argc, const char * argv[]) {
      static int c = 0;//全域性(靜態)初始化區
      int b ;                 // 棧
      char s[] = "abc";       //"abc\0"在常量區,s在棧區
      char p2 ;               // 棧
      char *p3 = "123456";     // "123456\0"在常量區,p3在棧上。
      char *p4 = "123456";//"123456\0"在常量區,p4在棧區
//p3和p4是一樣的,都指向同一個位置,"123456\0"所在位置
      p1 = (char )malloc(10); // 分配的10位元組的區域就在堆區
      p2 = (char )malloc(20); // 分配的20位元組的區域就在堆區
      printf("%p\n",p1);      // 0xffffffb0
      printf("%p\n",p2);      // 0xffffffc0
      return 0;                
//p1 變數的地址 0xffffffb0 比 p2 變數的地址 0xffffffc0 要小
}

棧和堆對比:

C語言函式返回值實現機制

我們知道,在子函式中返回區域性變數的值是不會出什麼問題的,但是,返回一個區域性變數的指標或者引用時,在後續解引用這個指標時就得不到理想的結果,原因在於:子函式中的自動變數(棧記憶體中的變數)會在子函式返回後被釋放掉,但是返回值會被儲存在cpu的暫存器中,因此,在返回子函式後,返回值能從暫存器中將返回值賦值給呼叫函式中的變數,如果返回值是一個指標,那麼該指標所指的記憶體地址會被儲存在暫存器中,但是,指標指向的記憶體卻被釋放掉了(即值未定義)。因此,在編寫程式碼時一般不會返回指向區域性變數的指標,除非一下三種情況:

1)子函式中定義了靜態區域性變數,函式可以返回指向該靜態區域性變數的指標。因為該變數分佈在記憶體的靜態區(在函式編譯時就將被初始化),所以在子函式返回時該變數仍然存在。

char * func1()
{
    static char name[]="Jack";
    return nema;
}

這裡“Jack”儲存在只讀儲存區,不能更改,將其賦值給name陣列,即複製到靜態儲存區,因此name中儲存的字串不是常量字串,可以通過陣列下表進行更改。

2)子函式返回一個常量的指標,比如字串常量,整形常量等。因為常量是定義在只讀儲存區,所以該常量也不會在子函式返回時被釋放。

char * func2()
{
    char *name="Jack";
    return nema;
}

3)子函式返回一個指向動態分配記憶體的指標。動態記憶體是在堆記憶體中分配的,需要程式設計師手動釋放,否則造成記憶體洩漏,所以在手動釋放該指標之前都可以返回該指標。

char * func3()
{
    char *name = (char *)malloc(20);
    return nema;
}