C语言再学习 -- 内存管理
參看:malloc()和free()
參看:百度百科 -- malloc函數
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; }
該例子說明,函數返回時函數所在的棧和指針被銷毀,申請的內存并沒有跟著銷毀。因為申請的內存在堆上,而函數所在的棧被銷毀跟堆完全沒有啥關系,所以使用完后記得釋放、置空。
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、小結
堆和棧的區別可以用如下的比喻來看出:?
使用棧就象我們去飯館里吃飯,只管點菜(發出申請)、付錢、和吃(使用),吃飽了就走,不必理會切菜、洗菜等準備工作和洗碗、刷鍋等掃尾工作,他的好處是快捷,但是自由度小。使用堆就象是自己動手做喜歡吃的菜肴,比較麻煩,但是比較符合自己的口味,而且自由度大。
總結
以上是生活随笔為你收集整理的C语言再学习 -- 内存管理的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 领导者的资质——学习笔记(2):领导者的
- 下一篇: 【高并发】——幂等的实现方案