溫故C語言內存管理
1. 內存管理簡介
在計算機系統,特別是嵌入式系統中,內存資源是非常 有限的。尤其對於移動端開發者來說,硬件資源的限制使得其在程序設計中首要考慮的問題就是如何 有效地管理內存資源。
常見內存使用錯誤:
- 內存申請未成功,就進行使用
- 內存申請成功,但沒有初始化
- 內存初始化成功,但越界訪問
- 忘記釋放內存或者釋放一部分
內存管理不當的危害?
- 沒有初始化,會造成內存出錯
- 越界訪問內存可能導致崩潰
- 忘記釋放內存造成內存泄露
C語言的內存管理:
C語言為用戶提供瞭相應內存管理的AP接口,如 malloc()
,free()
,new()
等函數,需要開發者手動管理。而java
、C#
則有自動內存回收機制,基本無需再對內存進行操作瞭。
2. 內存分類 棧區(stack)
由系統自動分配
堆區(heap)
在程序的執行過程中才能分配,由程序員決定
全局區(靜態區)
靜態區存放程序中所有的全局變量和靜態變量
常量區
常量字符串就是放在這裡的
代碼段:
代碼段(code segment/text segment)。通常是指用來存放程序執行代碼的一塊內存區域。代碼區的指令中包括操作碼和要操作的對象(或對象地址引用)。如果是立即數(即具體的數值,如5
)直接包含在代碼中;如果是局部數據,將在棧區分配空間,然後引用該數據地址。
數據段:
數據段(data segment)通常是指用來存放程序中已初始化的全局變量的一塊內存區域。數據段屬於靜態內存分配。
BSS段:
BSS段(Block Started by Symbol)。指用來存放程序中未初始化的全局變量的一塊內存區域。
BSS段本質上也屬於數據段,都用來存放C程序中的全局變量。區別在於.data段中存放初始化為非零的全局變量,而把顯式初始化為0或者並未顯式初始化(C語言規定未顯式初始化的全局變量值默認為0)
的全局變量存在BSS段。
3. 棧區(stack)
由編譯器 自動分配釋放,存放函數的參數值、局部變量的值等,是一種先進後出的內存結構。
哪些是分配在棧空間?
- 局部變量的值存放在棧上
- 在函數體中定義的變量通常是在棧上
函數棧分配:
在函數調用時,第一個進棧的是主函數中函數調用後的下一條指令(函數調用語句的下一條可執行語句)的地址,然後是函數的各個參數,在大多數的C編譯器中,參數是由右往左入棧的,然後是函數中的局部變量。
棧內存什麼時候回收?
棧內存的分配和釋放也由編譯器在函數進入和退出時插入指令自動完成,生命周期和函數、局部變量一樣。
棧空間的大小:
在 Windows下,棧是向低地址擴展的數據結構,是塊連續的內存的區域。棧空間一般較小,棧大小與編譯器有關。默認情況下,visual studio 2010
的棧大小為1M
。但在平時應用程序中,由於函數會使用棧結果,所以隻能用略小於1M大小的棧如果申請的空間超過棧的剩餘空間時,將提示Stack overflow。
示例代碼:
#include<stdio.h> struct A {}; class B {}; void fun(int a , int b) //參數a,b在棧上, 在函數體結束的時候,棧內存釋放 { int c;//局部變量,在棧上, 在函數體結束的時候,棧內存釋放 } int main() { int a = 10;//局部變量在棧上, 在main函數結束的時候,棧內存釋放 char b[] = "hello";//數組變量也在棧上, 在main函數結束的時候,棧內存釋放 char *c = NULL;//在棧上, 在main函數結束的時候,棧內存釋放 { A d;//結構體變量, 在棧上 B e;//類對象在棧上 } //d,e 在離開這個{}時,棧內存銷毀釋放 //測試棧的大小 //char buf[1024 * 1024] = { 'A' };//1M時崩潰瞭 char buf[1000* 1024] = { 'A' };//棧空間略小於1M //經過編譯期設置為5M之後,棧空間變大瞭 char buf[49 * 1024 * 1024 / 10] = { 'A' };//棧空間略小於5M printf("%d" , sizeof(buf) ); return 0; }
4. 堆區(heap)
需程序員自己申請,並可在運行時指定空間大小,並由程序員手動進行釋放,容易產生 memory leak
。
哪些是分配在堆空間?
調用 malloc
,realloc
,calloc
函數
//分配得來得10*4字節的區域在堆區 p1 = (char*)malloc(10*sizeof(int));
堆空間需要手動釋放:
堆是由 malloc()
等函數分配的內存塊,內存釋放由程序員調用free()
函數手動釋放
堆空間的大小:
堆空間一般較大,與64位/32位,編譯器有關,受限於計算機系統中有效的虛擬內存;理論上32位系統堆內存可以達到4G的空間,實際上2G以內,64位128G以內(虛擬內存16TB)
示例代碼:
#include<stdio.h> #include<stdlib.h> int main() { //手動分配、這裡就是分配瞭堆內存 int *p = (int*)malloc(10 * sizeof(int )); //手動釋放 free(p); int MB = 0; while (malloc(1024 * 1024))//每次分配1M { MB++; } printf("分配瞭 %d MB \n", MB); return 0; }
棧與堆的區別:
類型 | 分配釋放 | 大小 | 是否連續 | 申請效率 |
---|---|---|---|---|
棧區 | 由編譯器自動分配釋放 | 較小 | 一塊連續的內存區域 | 由系統自動分配,速度快 |
堆區 | 由程序員分配釋放 | 較大 | 堆是向高地址擴展的數據結構,是不連續的內存區域 | 速度慢,容易產生內存碎片 |
5. 全局區(靜態區)
全局變量和靜態變量的存儲是放在一塊的,初始化的全局變量和靜態變量在塊區域。
哪些是分配在全局靜態區?
- 全局變量
- static靜態變量
全局靜態區何時釋放?
全局變量、靜態變量在整個程序運行的生存期都存在,所以在程序結束時才釋放
示例代碼:
#include<stdio.h> //儲存在全局靜態區 int a; //全局變量,未初始化 short b = 10; //全局變量,已賦值 char *c = NULL; //全局變量,已賦值 static int f = 200; //靜態變量 int main() { static int d = 100; static int e = 200; printf("%p\n", &a); printf("%p\n", &b); printf("%p\n", &c); printf("%p\n", &d); printf("%p\n", &e); printf("%p\n", &f); }
6. 常量區
字符串常量是放在常量區,當你初始化賦值的時候,這些常量就先在常量區開辟一段空間,保存此常量,以後相同的常量就都使用一個地址。
示例代碼:
#include<stdio.h> //“AAA”是字符串常量,存放在常量區 char *p = "AAA"; int main() { //p1是局部變量,在棧上, “AAA”是字符串常量,存放在常量區 char *p1 = "AAA"; //p2是局部變量,在棧上,“AAA”不是字符串常量,她隻是一種初始化的寫法 char p2[]= "AAA"; //p3是局部變量,在棧上, “AAA”是字符串常量,存放在常量區 char *p3 = "AAA"; //p4是局部變量,在棧上, “AAB”是字符串常量,存放在常量區 char *p4 = "AAB"; printf("%p\n", p); printf("%p\n", p1); printf("%p\n", p2); printf("%p\n", p3); printf("%p\n", p4); }
7. malloc、calloc、realloc函數
三個函數的作用?
它們都能分配堆內存、成功返回內存的首地址,失敗就返回NULL
。
malloc
函數:
void *malloc( size_t size );
該函數將在堆上分配一個 size
byte大小的內存。不對內存進行初始化,所以新內存其值將是隨機的。
calloc
函數:
void *calloc( size_t number, size_t size );
該函數功能與 malloc
相同,它將分配count
個size
大小的內存,自動初始化該內存空間為零。
realloc
函數:
void *realloc( void *memblock, size_t size );
該函數將ptr
內存大小增大或減小到newsize
。
realloc
函數返回的兩種情況:
- 如果當前連續內存塊足夠
realloc
的話,隻是將p1所指向的空間擴大,並返回p1的指針地址。 - 如果當前連續內存塊不夠長度,再找一個足夠長的地方,分配一塊新的內存p2,並將p1指向的內容Copy到p2,並釋放p1指向的舊內存,然後返回p2。
示例代碼:
#include<stdio.h> #include<stdlib.h> int main() { //malloc ,參數是字節數 , 並且這塊內存空間的值是隨機的 int *p = (int *)malloc(5 * sizeof(int)); p[0] = 123; for (int i = 0; i < 5; ++i) { printf("%d ", p[i]); //後面4個值隨機 } printf("\n------------------------------------------------------------\n " ); //calloc,參數兩個, 自動將內存空間初始化為0 int *p2 = (int *)calloc(5, sizeof(int)); p2[4] = 123; for (int i = 0; i < 5; ++i) { printf("%d ", p2[i]); } printf("\n------------------------------------------------------------\n "); //realloc ,可以調整內存空間的大小 ,並且拷貝原來的內容(調大,或者 縮小) //int *p3 =(int *) realloc(p, 6* sizeof(int));//調大一點點,兩個地址相同 //int *p3 = (int *)realloc(p, 2 * sizeof(int));//縮小,兩個地址相同 int *p3 = (int *)realloc(p, 100 * sizeof(int));//調很大,兩個地址不同 ,釋放原來的內存空間 for (int i = 0; i <2; ++i) { printf("%d ", p3[i]); } printf("\np地址: %p , p3的地址: %p ", p, p3); return 0; }
8. strcpy、memcpy、memmove函數
頭文件:
#include <string.h>
strcpy
函數
char *strcpy( char *strDestination, const char *strSource );
把src
所指由\0
結束的字符串復制到dest
所指的數組中。
註意事項:
src
和dest
所指內存區域不能重疊,且dest
必須有足夠的空間來容納src
的字符串,src
的結尾必須是'\0'
,返回指向dest
的指針。
memcpy
函數
void *memcpy( void *dest, const void *src, size_t count );
由src
所指內存區域復制 count
個字節到dest
所指內存區域。
註意事項:
函數返回指向dest
的指針和 strcpy
相比,memcpy
不是遇到\0
就結束,而一定會拷貝n
個字節註意src
和dest
所指內存區域不能重疊,否則不能保證正確。
memmove
函數
void *memmove( void *dest, const void *src, size_t count );
函數功能:與 memcpy
相同。
註意事項:
src
和dest
所指內存區域可以重疊,memmove
可保證拷貝結果正確,而memcpy
不能保證。函數返回指向dest
的指針。
memset
函數
void *memset( void *dest, int c, size_t count );
常用於內存空間的初始化。將已開辟內存空間s
的首n
個字節的值設為值c
,並返回s
。
示例代碼:
#include<stdio.h> #include<string.h> #include<assert.h> //模擬memcpy函數實現 void * MyMemcpy(void *dest, const void *source, size_t count) { assert((NULL != dest) && (NULL != source)); char *tmp_dest = (char *)dest; char *tmp_source = (char *)source; while (count--)//不判斷是否重疊區域拷貝 *tmp_dest++ = *tmp_source++; return dest; } //模擬memmove函數實現 void * MyMemmove(void *dest, const void *src, size_t n) { char temp[256]; int i; char *d =(char*) dest; const char *s =(char *) src; for (i = 0; i < n; i++) temp[i] = s[i]; for (i = 0; i < n; i++) d[i] = temp[i]; return dest; } int main() { //strcpy進行字符串拷貝 //註意: 1. src字符串必須以'\0'結束, 2. dest內存大小必須>=src char a[5]; //char b[5] = "ABC";//字符串結尾會自動的有\0 , 此處 b[4]就是'\0' char b[5]; b[0] = 'A'; b[1] = 'B'; b[2] = 'C'; b[3] = '\0';//必須加\0,否則strcpy一直向後尋找\0 strcpy(a, b); printf("%s\n", a); //memcpy函數, 直接拷貝內存空間,指定拷貝的大小 int a2[5]; int b2[5] = { 1,2,3,4,5 };//不需要'\0'結束 memcpy(a2, b2, 3 *sizeof(int) );//指定拷貝的大小, 單位 字節數 printf("%d , %d ,%d\n" , a2[0] , a2[1], a2[2]); MyMemcpy(a2 + 3, b2 + 3, 2 * sizeof(int)); printf("%d , %d \n", a2[3], a2[4]); //演示內存重疊的情況 char a3[6] = "123"; //MyMemcpy(a3 + 1, a3, 4); //得到11111 memcpy(a3 + 1, a3, 4);//雖然它是正確的,但是不保證,重疊拷貝應該避免使用它 printf("%s\n", a3); //memmove功能與memcpy一樣,但是瞭考慮瞭重疊拷貝的問題,可以保證正確 char a4[6] = "123"; //MyMemmove(a4 + 1, a4, 4);//可以保證正確 memmove(a4 + 1, a4, 4);//可以保證正確 printf("%s\n", a4); //memset比較簡單, 把內存區域初始化化為某個值 char a5[6]; memset(a5, 0, 6); for (int i = 0; i < 6; ++i) { printf("%d", a5[i]); } return 0; }
9. 實現動態數組
思路:
利用 realloc
函數,當數組元素滿的時候,擴充內存區域,然後加入元素!
示例代碼:
#include<stdio.h> #include<stdlib.h> #include<assert.h> //為瞭代碼的可讀性,將設計為C++中的類,利用struct 代替 //動態數組 struct Array { //自動構造函數,它初始化 Array() { grow = 3; size = 3; n = 0; //分配並初始化內存 pHead = (int *)calloc(size , sizeof(int)); assert(pHead != NULL); } void AddElem(int e) { if (n >= size)//說明數組滿瞭 { //需要擴大內存 size += grow; pHead = (int *)realloc( pHead, size * sizeof(int) ); assert(pHead != NULL); } pHead[n++] = e; //添加元素 } void Print() { printf("\n\n數組總空間:%d , 元素個數: %d \n", size, n); for (int i = 0; i < n; ++i) { printf("%d " , pHead[i]); } } int size;//總空間, 不是固定的,可以增大的 int n;//當前數組的元素個數 int grow;//每次數組內存滿瞭的時候,增長量 int *pHead;//數組的起始地址 }; int main() { Array arr; arr.AddElem(1); arr.AddElem(2); arr.AddElem(3); arr.AddElem(4); arr.AddElem(5); arr.AddElem(6); arr.AddElem(7); arr.AddElem(8); arr.Print(); arr.AddElem(11); arr.AddElem(22); arr.AddElem(33); arr.AddElem(44); arr.AddElem(55); arr.Print(); return 0; }
10. 內存越界
何謂內存訪問越界,簡單的說,你向系統申請瞭一塊內存,在使用這塊內存的時候,超出瞭你申請的范圍。
- 訪問到野指針指向的區域,越界訪問
- 數組下標越界訪問
- 使用已經釋放的內存
- 企圖訪問一段釋放的棧空間
- 容易忽略 字符串後面
的'\0'
註意:
strlen
所作的是一個計數器的工作,它從內存的某個位置(可以是字符串開頭,中間某個位置,甚至是某個不確定的內存區域)開始掃描,直到碰到第一個字符串結束符'\0'
為止,然後返回計數器值( 長度不包含’\0′)。
示例代碼:
#include<stdio.h> #include<stdlib.h> #include<string.h> char * fun() { char arr[10]; return arr; }//arr是棧內存,離開此花括號,棧被釋放回收 int main() { //1.訪問到野指針指向的區域,越界訪問 char *p;//沒有初始化,野指針,亂指一氣 //strcpy(p, "hello");//非法越界訪問 //2.數組下標越界訪問 int * p2 = (int *)calloc(10, sizeof(int)); for (size_t i = 0; i <= 10; i++) { p2[i] = i;//很難察覺的越界訪問, 下標越界 } //3.使用已經釋放的內存 char *p3 = (char *)malloc(10); free(p3); if (p3 != NULL)//這裡if不起作用 { strcpy(p3, "hello");//錯誤,p3已經被釋放 } //4.企圖訪問一段釋放的棧空間 char *p4 = fun(); //p4指向的棧空間已經被釋放 strcpy(p4, "hello"); printf("%s\n",p4); //5.容易忽略 字符串後面的'\0' char *p5 = (char *)malloc(strlen("hello"));//忘記加1 strcpy(p5, "hello");//導致p5的長度不夠,越界 return 0; }
11. 內存泄露(Memory Leak)
是指程序中己動態分配的堆內存由於某種原因程序未釋放或無法釋放,造成系統內存的浪費,導致程序運行速度減慢甚至系統崩潰等嚴重後果。
- 丟失瞭分配的內存的首地址,導致無法釋放
- 丟失分配的內存地址
- 企圖希望傳入指針變量獲取對內存,殊不知是拷貝
- 每循環一次,泄露一次內存
- 非法訪問常量區
示例代碼:
#include<stdio.h> #include<stdlib.h> #include<string.h> char * GetBuf() { return (char *)malloc(10); } void GetBuf2(char *p)//p已經是一份拷貝,和原參數無任何關系 { p= (char *)malloc(10); } char * GetBuf3() { char *p = "hello";//常量內存區,不可更改 return p; } int main() { //1.丟失瞭分配的內存的首地址,導致無法釋放 GetBuf();//忘記接收返回值瞭 //2.丟失分配的內存地址 char *p1= (char *)malloc(10); char *p2 = (char *)malloc(10); p1 = p2;//這一步,導致第一次分配的堆內存丟失,無法釋放 //3.企圖希望傳入指針變量獲取對內存,殊不知是拷貝 char *p3 = NULL; GetBuf2(p3); //應該使用指針的指針,或者引用 //strcpy(p3, "hello"); //錯誤,這裡的p3仍然為NULL //4.每循環一次,泄露一次內存 char * p4 = NULL; for (int i = 0; i < 10; ++i) { p4= (char *)malloc(10); } strcpy(p4, "hello"); // 這裡的p4隻指向最後一次分配的,前面的全部內存泄漏 //5.非法訪問常量區 char *p5 = GetBuf3(); strcpy(p5, "hello"); return 0; }
12. 內存池技術簡介
內存碎片:
內存碎片一般是由於空閑的內存空間比要連續申請的空間小,導致這些小內存塊不能被充分的利用,當你需要分配大的連續內存時,盡管剩餘內存的總和足夠,但系統找不到連續的內存,所以導致分配失敗malloc/free
大量使用會造成內存碎片
為什麼會產生內存碎片?
如果有100個單位的連續空閑內存,那麼先申請5單元的連續內存,再申請50單元的內存這時釋放一開始的5單元的內存。這時,如果你一直申請比5單元大的內存單元,那麼開始的那連續的5單元就一直不能被使用。
內存池技術:
內存的申請、釋放是低效的,我們隻在開始申請一塊大內存(不夠繼續申請),然後每次需要時都從這塊內存取出,並標記這塊內存是否被使用。釋放時僅僅標記而不真的free
,隻有內存都空閑的時候,才釋放給操作系統。這樣減少瞭 malloc
、free
次數,從而提高效率。
13. C語言實現內存池
設計思路:
先分配幾個大的連續內存塊(MemoryBlock),每個內存塊用鏈表鏈接起來,然後通過一個內存池結構(MemoryPool)管理!
代碼實現:
#include<stdio.h> #include<stdlib.h> #include<string.h> #include<assert.h> class MemoryBlock { public: int nSize; //該內存塊的總大小 (單元個數X每個單元大小),以字節為單位 int nFree; //該內存塊還有多少個空閑的單元 int nFirst; //當前可用空閑單元的序號,從0開始 MemoryBlock* pNext; //指向下一個MemoryBlock內存塊 char aData[1]; //用於標記分配內存開始的位置 //.....這個結構下面全是內存 public: MemoryBlock(int unitCount, int unitSize) { nSize = unitCount* unitSize; nFree = unitCount; nFirst = 0; pNext = NULL; char *p = aData;//獲取內存單元的首地址 for (int i = 0; i < unitCount -1; ++i) { *((short *)p) = i + 1; //第0塊的下個空閑索引是不是第1塊 p += unitSize; } *((short *)p) = -1;//最後一塊沒有下一個空閑空間瞭,為-1 } void * operator new (size_t t, int size) { int headSize = sizeof(MemoryBlock); return ::operator new(headSize + size); } }; //分配固定內存的內存池 class MemoryPool { public: //初始大小 (每一個MemoryBlock中初始的單元個數) int nInitCount; //(後面增加的MemoryBlock中單元個數) int nGrowSize; //分配單元大小,MemoryBlock中每個單元的大小 int nUnitSize; //內存塊鏈表 MemoryBlock* pBlock; public: MemoryPool( int _nInitCount, int _nGrowSize, int _nUnitSize) { nInitCount = _nInitCount; nGrowSize = _nGrowSize; nUnitSize = _nUnitSize; pBlock = NULL; } char * Alloc() //每次隻返回 nUnitSize 大小的內存 { if (pBlock == NULL) { MemoryBlock * p =(MemoryBlock *) new (nInitCount * nUnitSize) MemoryBlock(nInitCount, nUnitSize); assert(p != NULL); pBlock = p; } MemoryBlock * pB = pBlock; while (pB !=NULL && pB->nFree==0) { pB = pB->pNext; } if (pB == NULL)//一直沒找到瞭可以分配的MemoryBlock,說明內存池已滿 { pB = (MemoryBlock *) new (nGrowSize * nUnitSize) MemoryBlock(nGrowSize, nUnitSize); assert(pB != NULL); pB->pNext = pBlock; pBlock = pB; } //得到第一個可用的空閑內存地址 char *pFree = pB->aData + pB->nFirst * nUnitSize; //把nFirst值改為下一個空閑的索引 (存儲在當前內存的前兩個字節) pB->nFirst = *((short*)pFree); pB->nFree--; return pFree; } void Free(void *p) { //考慮這個地址落在哪個 MemoryBlock 上 MemoryBlock * pB = pBlock; while (pB != NULL && p < pB->aData || p > pB->aData+ pB->nSize ) { pB = pB->pNext; } if (pB!= NULL)//找到瞭p所在的MemoryBlock { //銷毀之前先讓它的前兩個字節指向nFirst (當前空閑的索引) *((short*)p) = pB->nFirst; //nFirst的值指向當前釋放的 pB->nFirst = ((char *)p - pB->aData) / nUnitSize; pB->nFree++; } else { printf("錯誤,此內存並非內存池分配的!\n"); } } void Print() { printf("\n\n\n"); MemoryBlock * pB = pBlock; while (pB != NULL ) { printf("\n首地址:%p 總大小:%d 空閑個數: %d 下一個空閑:%d \n", pB->aData , pB->nSize, pB->nFree ,pB->nFirst); for (int i = 0; i < pB->nSize / nUnitSize; ++i) { printf("\t %d" , * ((int *) ( pB->aData + i * nUnitSize ))); } pB = pB->pNext; printf("\n---------------------------------------------------------\n"); } } }; int main() { MemoryPool pool(3, 3, 4); int *p1 = (int *)pool.Alloc(); *p1 = 111; int *p2 = (int *)pool.Alloc(); *p2 = 222; int *p3 = (int *)pool.Alloc(); *p3 = 333; pool.Print(); int *p4 = (int *)pool.Alloc(); *p4 = 444; pool.Print(); int *p5 = (int *)pool.Alloc(); *p5 = 555; pool.Print(); pool.Free( p1); pool.Free(p2); pool.Free(p3); pool.Print(); p1 = (int *)pool.Alloc(); *p1 = 111; p2 = (int *)pool.Alloc(); *p2 = 222; p3 = (int *)pool.Alloc(); *p3 = 333; pool.Print(); return 0; }
到此這篇關於溫故C語言內存管理的文章就介紹到這瞭,更多相關C語言內存管理內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!