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(當然此處不考慮虛擬內存技術),所以用戶能夠訪問到的最大內存空間是由硬件和操作系統兩者共同決定的,兩者都有制約關係。

引用自 :https://www.cnblogs.com/dolphin0520/archive/2013/05/31/3110555.html

C程序內存分配

對於一個由C語言編寫的程序而言,內存主要可以分爲以下5個部分組成:

其中需要注意的是:代碼段、數據段、BSS段在程序編譯期間由編譯器分配空間,在程序啓動時加載,由於未初始化的全局變量存放在BSS段,已初始化的全局變量存放在數據段,所以程序中應該儘量少的使用全局變量以節省程序編譯和啓動時間;棧和堆在程序運行中由系統分配空間。

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

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

  • 數據區(全局初始化數據區 data):該區包含了在程序中明確被初始化的全局變量、靜態變量(包括全局靜態變量和局部靜態變量)和常量數據(如字符串常量)
  • BSS區(未初始化數據區。):存入的是全局未初始化變量。BSS這個叫法是根據一個早期的彙編運算符而來,這個彙編運算符標誌着一個塊的開始。BSS區的數據在程序開始執行之前被內核初始化爲0或者空指針(NULL)。llinux環境下可以用size命令 查看C程序的存儲空間佈局,可以看出,此可執行程序在存儲時(沒有調入到內存)分爲代碼區(text)、數據區(data)和未初始化數據區(bss)3個部分。
phoenix@Phoenix:~/桌面$ 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
phoenix@Phoenix:~/桌面$ 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;
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章