1. 程式人生 > >C語言再學習 -- 記憶體管理

C語言再學習 -- 記憶體管理

malloc ( )函式:

malloc ( ) 向系統申請分配指定size個位元組的記憶體空間。返回型別是 void* 型別。void* 表示未確定型別的指標。C,C++規定,void* 型別可以通過型別轉換強制轉換為任何其它型別的指標。

所在標頭檔案:

stdlib.h

函式宣告:

void *malloc(size_t size);

備註:void* 表示未確定型別的指標,void *可以指向任何型別的資料,更明確的說是指申請記憶體空間時還不知道使用者是用這段空間來儲存什麼型別的資料(比如是char還是int或者其他資料型別)。

函式返回值:

如果分配成功則返回指向被分配記憶體的指標(此儲存區中的初始值不確定),否則返回空指標NULL。當記憶體不再使用時,應使用free()函式將記憶體塊釋放。函式返回的指標一定要適當對齊,使其可以用於任何資料物件。

free ( )函式:

釋放之前呼叫 calloc、malloc 或 realloc 所分配的記憶體空間

所在標頭檔案:

stdlib.h

函式宣告:

void free(void *ptr)

備註:ptr -- 指標指向一個要釋放記憶體的記憶體塊,該記憶體塊之前是通過呼叫 malloc、calloc 或 realloc 進行分配記憶體的。如果傳遞的引數是一個空指標,則不會執行任何動作。

函式用法:

type *p;
p = (type*)malloc(n * sizeof(type));
if(NULL == p)
/*請使用if來判斷,這是有必要的*/
{
    perror("error...");
    exit(1);
}
.../*其它程式碼*/
free(p);
p = NULL;/*請加上這句*/

函式使用需要注意的地方:

1、malloc 函式返回的是 void * 型別,必須通過 (type *) 來將強制型別轉換

2、malloc 函式的實參為 sizeof(type),用於指明一個整型資料需要的大小。

3、申請記憶體空間後,必須檢查是否分配成功

4、當不需要再使用申請的記憶體時,記得釋放,而且只能釋放一次。如果把指標作為引數呼叫free函式釋放,則函式結束後指標成為野指標(如果一個指標既沒有捆綁過也沒有記錄空地址則稱為野指標),所以釋放後應該把指向這塊記憶體的指標指向NULL,防止程式後面不小心使用了它。

5、要求malloc和free符合一夫一妻制,如果申請後不釋放就是記憶體洩漏

,如果無故釋放那就是什麼也沒做。釋放只能一次,如果釋放兩次及兩次以上會出現錯誤(釋放空指標例外,釋放空指標其實也等於啥也沒做,所以釋放空指標釋放多少次都沒有問題)。

基礎知識講完了,下面開始進階階段:

1、malloc 從哪裡得來的記憶體空間

malloc 從堆裡獲得空間,函式返回的指標指向堆裡的一塊記憶體。作業系統中有一個記錄空閒記憶體地址的連結串列。當作業系統收到程式的申請時,就會遍歷該連結串列,然後尋找第一個空間大於所申請空間的堆結點,然後將該結點連結串列刪除,並將該結點空間分配給程式。

2、free 釋放了什麼

free 釋放的是指標指向的記憶體。注意,釋放的是記憶體,而不是指標。

指標是一個變數,只有程式結束時才被銷燬,釋放記憶體空間後,原來指向這塊空間的指標還是存在的,只不過現在指標指向的內容是未定義的,所以說是垃圾。因此,釋放記憶體後要把指標指向NULL,防止指標在後面不小心又被解引用了。

/*
 	動態分配記憶體演示
 */
#include <stdio.h>
#include <stdlib.h>
int *read(void)	//指標做返回值
{
	int *p_num=(int *)malloc(sizeof(int));
	if(p_num)
	{		//不可以在這個釋放動態記憶體
		printf("請輸入一個整數:");
		scanf("%d",p_num);
	}
	return p_num;
}
int main()	//動態分配記憶體可以實現跨函式儲存區,動態分配記憶體被釋放之前可以讓任何函式使用
{
	int *p_num=read();
	if(p_num)
	{
	printf("數字是%d\n",*p_num);
	free(p_num);
	p_num=NULL;
	}
	return 0;
}
思考一個問題,上面的例子為什麼不能在 int * read (void);函式裡 釋放和置空?

該例子說明,函式返回時函式所在的棧和指標被銷燬,申請的記憶體並沒有跟著銷燬。因為申請的記憶體在堆上,而函式所在的棧被銷燬跟堆完全沒有啥關係,所以使用完後記得釋放、置空。

3、工作機制

malloc函式的實質體現在,它有一個將可用的記憶體塊連線為一個長長的列表的所謂空閒連結串列。呼叫malloc函式時,它沿連線表尋找一個大到足以滿足使用者請求所需要的記憶體塊。然後,將該記憶體塊一分為二(一塊的大小與使用者請求的大小相等,另一塊的大小就是剩下的位元組)。接下來,將分配給使用者的那塊記憶體傳給使用者,並將剩下的那塊(如果有的話)返回到連線表上。呼叫free函式時,它將使用者釋放的記憶體塊連線到空閒鏈上。到最後,空閒鏈會被切成很多的小記憶體片段,如果這時使用者申請一個大的記憶體片段,那麼空閒鏈上可能沒有可以滿足使用者要求的片段了。於是,malloc函式請求延時,並開始在空閒鏈上翻箱倒櫃地檢查各記憶體片段,對它們進行整理,將相鄰的小空閒塊合併成較大的記憶體塊。如果無法獲得符合要求的記憶體塊,malloc函式會返回NULL指標,因此在呼叫malloc動態申請記憶體塊時,一定要進行返回值的判斷。

程序中的記憶體區域劃分
(1)程式碼區 存放程式的功能程式碼的區域,比如:函式名
(2)只讀常量區 主要存放字串常量和const修飾的全域性變數
(3)全域性區 主要存放已經初始化的全域性變數和static修飾的全域性變數
(4)BSS段 主要存放沒有初始化的全域性變數和static修飾的區域性變數,BSS段會在main函式執行之前自動清零
(5)堆區 主要表示使用malloc/calloc/realloc等手動申請的動態記憶體空間,記憶體由程式設計師手動申請和手動釋放
(6)棧區 主要存放區域性變數(包括函式的形參),const修飾的區域性變數,以及塊變數,該記憶體區域由作業系統自動管理


記憶體地址從小到大分別是:
程式碼區 只讀常量區 全域性區 BSS段 堆 棧
其中堆區和棧區並沒有明確的分割線,可以適當的調整

圖示1:可執行程式在儲存器中的存放


//程序中的記憶體區域劃分
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int i1=10;//全域性區
int i2=20;//全域性區
int i3;//BSS段
const int i4=40;//只讀常量區
void fn(int i5)//棧區
{
	int i6=60;//棧區
	static int i7=70;//全域性區
	const int i8=80;//棧區
	int* p1=(int*)malloc(4); //p1指向堆區 p1本身在棧區
	int* p2=(int*)malloc(4); //p2指向堆區 p2本身在棧區
	char* str="good";   //str 指向只讀常量區
	//strs 在棧區
	char strs[]="good";
	printf("只讀常量區:&i4=%p\n",&i4);
	printf("只讀常量區:str=%p\n",str);
	printf("----------------------------\n");
	printf("全域性區:&i1=%p\n",&i1);
	printf("全域性區:&i2=%p\n",&i2);
	printf("全域性區:&i7=%p\n",&i7);
	printf("----------------------------\n");
	printf("BSS段:&i3=%p\n",&i3);
	printf("----------------------------\n");
	printf("堆區:p1=%p\n",p1);
	printf("堆區:p2=%p\n",p2);
	printf("----------------------------\n");
	printf("棧區:&i5=%p\n",&i5);
	printf("棧區:&i6=%p\n",&i6);
	printf("棧區:&i8=%p\n",&i8);
	printf("棧區:strs=%p\n",strs);
}
int main()
{
	printf("程式碼區:fn=%p\n",fn);
	printf("----------------------------\n");
	fn(10);
	return 0;
}
輸出結果:

程式碼區:fn=0x8048494
---------------------
只讀常量區:&i4=0x80486e0
只讀常量區:str=0x80486e4
---------------------
全域性區:&i1=0x804a01c
全域性區:&i2=0x804a020
全域性區:&i7=0x804a024
---------------------
BBS段:&i3=0x804a034
BBS段:&i9=0x804a030
---------------------
堆區:p1=0x88e8008
堆區:p2=0x88e8018
---------------------
棧區:&i5=0xbfbcc060
棧區:&i6=0xbfbcc04c
棧區:&i8=0xbfbcc048
棧區:strs=0xbfbcc037

//字串儲存形式的比較
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main()
{
	//pc儲存字串的首地址,不能存內容
	//pc指向只讀常量區,pc本身在棧區
	char* pc="hello";
	//str儲存字串的內容,而不是地址
	//str指向棧區,str本身在棧區
	//將字串內容拷貝一份到字串陣列
	char str[]="hello";
	printf("字串地址:%p\n","hello");
	printf("只讀常量區 pc=%p\n",pc);
	printf("棧區 &pc=%p\n",&pc);
	printf("棧區 str=%p\n",str);
	printf("棧區 &str=%p\n",&str);
	printf("------------------------\n");
	//修改指向
	pc="1234";
//	str="1234";//陣列名是個常量不可改變  error
	//修改指向的內容
//	*pc='A';//error  指標不可修改
	str[0]='A';
	printf("--------------------------\n");
	//在堆區申請的動態記憶體
	char* ps=(char*)malloc(10);
	//修改指向的內容
	strcpy(ps,"hello");
	//修改指向
	char* p=ps;
	ps="Good";
	//釋放記憶體
	printf("堆區 ps=%p\n",ps);
	printf("堆區 p=%p\n",p);
	free(p);
	p=NULL;
	return 0;
}
輸出結果:
字串地址:0x80486e0
只讀常量區 pc=0x80486e0
棧區 &pc=0xbf9499a8
棧區 str=0xbf9499b6
棧區 &str=0xbf9499b6
------------------------
--------------------------
堆區 ps=0x804877a
堆區 p=0x8eb2008


什麼是堆、棧?

參看:堆疊詳解

:是大家共有的空間,分全域性堆和區域性堆。全域性堆就是所有沒有分配的空間,區域性堆就是使用者分配的空間,堆在作業系統對程序初始化的時候分配,執行過程中也可以像系統要額外的堆,但是記得用完了要還給作業系統,要不然就是記憶體洩漏

:是個執行緒獨有的,儲存其執行狀態和區域性自動變數的。棧線上程開始的時候初始化,每個執行緒的棧互相獨立,因此,棧是 thread safe的。每個C++物件的資料成員也存在在棧中,每個函式都有自己的棧,棧被用來在函式之間傳遞引數。作業系統在切換執行緒的時候回自動的切換棧,就是切換 SS/ESP暫存器。棧空間不需要再高階語言裡面顯式的分配和釋放。

示例:

//main.cpp 
int a = 0; 全域性初始化區 
char *p1; 全域性未初始化區 
main() 
{ 
int b; 棧 
char s[] = "abc"; 棧 
char *p2; 棧 
char *p3 = "123456"; 123456在常量區,p3在棧上。 
static int c =0; 全域性(靜態)初始化區 
p1 = (char *)malloc(10); 
p2 = (char *)malloc(20); 
分配得來得10和20位元組的區域就在堆區。 
strcpy(p1, "123456"); 123456放在常量區,編譯器可能會將它與p3所指向的"123456"優化成一個地方。 
}

堆和棧的理論知識:

1、申請方式:

stack 棧 :遵循 LIFO後進先出的規則,它的生長方向是向下的,是向著記憶體地址減小的方向增長的,棧是系統提供的功能,特點是快速高效,缺點是有限制,資料不靈活。由系統自動分配。例如,宣告在函式中一個區域性變數 int b; 系統自動在棧中為 b 開闢空間。

heap 堆:對於堆來講,生長方向是向上的,也就是向著記憶體地址增加的方向。需要程式設計師自己申請,並指明大小。

在 C 中 malloc 函式,如: P1 = (char *)malloc (10);

在 C++ 中用 new 運算子,如: P2 = new char[10];

它申請的記憶體是在堆中,但是注意P1、P2本身是在棧中的。

2、申請後系統的響應

:只要棧的剩餘空間大於所申請的空間,系統將為程式提供記憶體,否則將報異常提示棧溢位。

:首先應該知道作業系統有一個記錄空閒記憶體地址的連結串列,當系統收到程式的申請時,會遍歷連結串列,尋找第一個空間大於所申請空間的堆結點,然後將該結點從空閒結點連結串列中刪除,並將該結點的空間分配給程式,另外,對於大多數系統,會在這塊記憶體空間中的首地址處記錄本次分配的大小。這樣,程式碼中的 delete 語句才能正確的釋放本記憶體空間。另外,由於找到的堆結點的大小不一定正好等於申請的大小,系統會自動將多餘的那部分重新放入空閒連結串列中。

3、申請大小的限制

:在 Windows下,棧是向低地址擴充套件的資料結構,是一塊連續的記憶體的區域,這句話的意思是棧頂的地址和棧的最大容量是系統預先規定好的。在 Windows 下,棧的大小是2M(也有的說是1M,總之是一個編譯時就確定的常數),如果申請的空間超過棧的剩餘空間時,將提示 overflow。因此,能從棧獲得的空間較小。

:堆是向高地址擴充套件的資料結構,是不連續的記憶體區域。這是由於系統是用連結串列來儲存的空閒記憶體地址的,自然是不連續的,而連結串列的遍歷方向是由低地址向高地址。堆的大小受限於計算機系統中有效的虛擬記憶體。由此可見,堆獲得的空間比較靈活,也比較大。

4、申請效率的比較

:由系統自動分配,速度較快。但程式設計師是無法控制的。

:是由 new 分配的記憶體,一般速度比較慢,而且容易產生記憶體碎片,不過用起來最方便。另外,在 Windows 下,最好的方式使用 virtualAlloc 分配記憶體,它不是在堆,也不是在棧,是直接在程序的地址空間中保留一塊記憶體,雖然用起來最不方便。但是速度快也最靈活。

5、堆和棧中的儲存內容

:在函式呼叫時,第一個進棧的是主函式中後的下一條指令(函式呼叫語句的下一條執行語句)的地址,然後是函式的各個引數,在大多數的 C 編譯器中,引數是由右往左入棧的,然後是函式中的區域性變數。注意靜態變數是不入棧的。

當本次函式呼叫結束後,區域性變數先出棧,然後是引數,最後棧頂指標指向最開始存的地址,也就是主函式中的下一條指令,程式由該店繼續執行。

:一般是在堆的頭部用一個位元組存放堆的大小,堆中的具體內容由程式設計師安排。

6、存取效率的比較

char s1[] = "aaaaaaaaaaaaaaa"; 
char *s2 = "bbbbbbbbbbbbbbbbb"; 
aaaaaaaaaaa是在執行時刻賦值的; 
而bbbbbbbbbbb是在編譯時就確定的; 
但是,在以後的存取中,在棧上的陣列比指標所指向的字串(例如堆)快。 
比如: 
#include 
void main() 

char a = 1; 
char c[] = "1234567890"; 
char *p ="1234567890"; 
a = c[1]; 
a = p[1]; 
return; 

對應的彙編程式碼 
10: a = c[1]; 
00401067 8A 4D F1 mov cl,byte ptr [ebp-0Fh] 
0040106A 88 4D FC mov byte ptr [ebp-4],cl 
11: a = p[1]; 
0040106D 8B 55 EC mov edx,dword ptr [ebp-14h] 
00401070 8A 42 01 mov al,byte ptr [edx+1] 
00401073 88 45 FC mov byte ptr [ebp-4],al 
第一種在讀取時直接就把字串中的元素讀到暫存器cl中,而第二種則要先把指標值讀到edx中,在根據edx讀取字元,顯然慢了。

7、小結

堆和棧的區別可以用如下的比喻來看出: 
使用棧就象我們去飯館裡吃飯,只管點菜(發出申請)、付錢、和吃(使用),吃飽了就走,不必理會切菜、洗菜等準備工作和洗碗、刷鍋等掃尾工作,他的好處是快捷,但是自由度小。使用堆就象是自己動手做喜歡吃的菜餚,比較麻煩,但是比較符合自己的口味,而且自由度大。