1.為什么要有動態內存管理
我們已經掌握的內存開辟方法有:
int main()
{int val = 20;//在棧空間上開辟四個字節char arr[20] = { 0 };//在棧空間上開辟10個字節的連續空間return 0;
}
上述開辟的內存空間有兩個特點:
1.空間開辟的時候大小已經固定
2.數組在聲明的時候,必須指定數組的長度,數組空間一旦確定了大小就不能調整
但是對于所需內存空間的需求,不僅僅是上述的情況,有時候我們需要的空間大小在程序運行時才能知道,那數組就不能滿足我們的需求了。
C語言由此引入了動態內存開辟,讓程序員可以自己申請和釋放空間。
2.malloc和free
C語言提供了一個動態內存開辟的函數malloc,函數原型如下:
void* malloc(size_t size);
這個函數向內存申請?塊連續可?的空間,并返回指向這塊空間的指針。
? 如果開辟成功,則返回?個指向開辟好空間的指針。
? 如果開辟失敗,則返回?個 NULL 指針,因此malloc的返回值?定要做檢查。
? 返回值的類型是 void* ,所以malloc函數并不知道開辟空間的類型,具體在使?的時候使?者自己來決定。
? 如果參數 size 為0,malloc的?為是標準是未定義的,取決于編譯器。
malloc詳細解析網頁
C語言還提供了一個函數free,專門用來做動態內存的釋放和回收的,函數原型如下:
void* free(void* ptr);
free函數?來釋放動態開辟的內存。
? 如果參數 ptr 指向的空間不是動態開辟的,那free函數的?為是未定義的。
? 如果參數 ptr 是NULL指針,則函數什么事都不做。
malloc和free都聲明在 stdlib.h 頭?件中。
free詳細解析網頁
學完了兩個函數,我們來舉一個例子幫助大家理解:
int main()
{int* pz = (int*)malloc(10 * sizeof(int));//注意,malloc自己返回的指針類型是void*的,所以需要強制轉換為int*類型if (pz == NULL)//如果創建失敗,返回的將是空指針{perror("malloc");//打印錯誤,為什么創建失敗return 1;//結束程序}for (int i = 0; i < 10; i++){*(pz + i) = i + 1;}for (int i = 0; i < 10; i++){printf("%d ", *(pz + i));}free(pz);//動態內存使用完我們應該主動進行回收pz = NULL;//回收之后,內存空間將會釋放,但是pz指針仍然會指向那個地址,我們需要將他置為空指針return 0;
}
觀察結果,我們可以發現利用malloc創建的堆空間實現了與數組創建的棧空間一樣的效果,它們都是一塊連續的空間。上述是創建成功的示例,我們再來看一個失敗的案例:
3.calloc和realloc
C語言還提供了一個函數叫calloc,他也是用來實現動態內存分配的,函數原型如下:
void* calloc(size_t num, size_t size);
? 函數的功能是為 num 個??為 size 的元素開辟?塊空間,并且把空間的每個字節初始化為0。
? 與函數 malloc 的區別只在于 calloc 會在返回地址之前把申請的空間的每個字節初始化為0
calloc詳細解析網頁
看代碼:
int main()
{int* pz = (int*)calloc(10, sizeof(int));if (pz == NULL){perror("calloc");return 1;}for (int i = 0; i < 10; i++){printf("%d ", *(pz + i));}free(pz);pz = NULL;return 0;
}
近乎一樣的代碼,打印的結果完全不同,這就是malloc和calloc的區別。
接下來我們要學的最后一個函數是realloc,realloc函數的出現讓動態內存管理更加靈活。有時會我們發現過去申請的空間太?了,有時候我們?會覺得申請的空間過?了,那為了合理的申請內存,我們?定會對內存的??做靈活的調整。那 realloc 函數就可以做到對動態開辟內存大小的調整。他的函數原型如下:
void* realloc (void* ptr,size_t size);
? ptr 是要調整的內存地址
? size 調整之后新??
? 返回值為調整之后的內存起始位置。
? 這個函數調整原內存空間??的基礎上,還會將原來內存中的數據移動到 新 的空間。
realloc詳細解析網頁
realloc在調整內存空間時存在兩種情況:
? 情況1:原有空間之后有?夠?的空間
? 情況2:原有空間之后沒有?夠?的空間
情況1
當是情況1 的時候,要擴展內存就直接原有內存之后直接追加空間,原來空間的數據不發?變化。
情況2
當是情況2 的時候,原有空間之后沒有?夠多的空間時,擴展的?法是:在堆空間上另找?個合適??的連續空間來使?。這樣函數返回的是?個新的內存地址。原有空間的數值會被存入新的空間內。
由于上述的兩種情況,realloc函數的使?就要注意?些。
int main()
{int* pz1 = (int*)malloc(10 * sizeof(int));if (pz1 == NULL) {perror("malloc");return 1;}//業務處理//發現內存不夠,需要擴充//方法1//pz1 = (int*)realloc(pz1, 20 * sizeof(int));//方法2int* pz2 = NULL;pz2 = (int*)realloc(pz1, 20 * sizeof(int));if (pz2 == NULL){perror("realloc");free(pz1);return 1;}pz1 = pz2;//業務處理free(pz1);pz1 = NULL;return 0;
}
方法一存在下面風險:
內存泄漏風險:
若 realloc 失敗返回 NULL,原指針 pz1 會被直接覆蓋為 NULL。
后果:原內存(10個int的空間)徹底丟失,無法再被釋放,導致內存泄漏。空指針操作風險: 未檢查返回值直接使用 pz1,若 realloc失敗,后續操作 pz1 的行為會引發未定義行為(如訪問空指針導致程序崩潰)。
使用方法二可以很好的避免發生這些錯誤。
4.常見的動態內存錯誤
4.1對NULL指針的解引用操作
int main()
{int* p = (int*)malloc(INT_MAX);*p = 10;printf("%d\n", *p);free(p);p = NULL;return 0;
}
上述代碼我們沒有檢查malloc函數創建失敗返回空指針的可能,實際上改代碼p返回的就是空指針,而我們對空指針進行解引用操作的行為時錯誤的,所以大家在利用malloc等函數申請內存時一定要檢查是否成功申請,否則返回的可能是空指針。
4.2對動態開辟空間的越界訪問
int main()
{int* p = (int*)malloc(10 * sizeof(int));if (p == NULL){perror("malloc");return 1;}for (int i = 0; i < 12; i++){*(p + i) = i;//越界訪問}free(p);p = NULL;return 0;
}
上述代碼我們利用malloc函數像內存申請了40個字節的空間,但我們在for循環中卻訪問了48個字節的空間,在編譯時程序肯定是會崩潰的,這屬于越界訪問。
4.3對非動態開辟內存使用free釋放
int main()
{int a = 10;int* p = &a;free(p);p = NULL;return 0;
}
free函數只能釋放動態開辟的內存,他不能釋放棧空間的內存空間,上述代碼也會報錯。
4.4使用free釋放一塊動態開辟內存的一部分
int main()
{int* p = (int*)malloc(100);p++;free(p);p = NULL;return 0;
}
free函數不能這樣釋放內存,他的參數只能是開辟動態內存的起始地址,上述代碼也會在編譯時報錯。
4.5對同一塊動態內存多次釋放
int main()
{int* p = (int*)malloc(100);free(p);free(p);return 0;
}
free函數是不能對一塊動態內存進行重復釋放,編譯器會報錯。
4.6動態開辟內存忘記釋放(內存泄露)
void test()
{int* p = (int*)malloc(100);if (p != NULL){*p = 20;}
}int main()
{test();return 0;
}
上述代碼在運行時編譯器雖然不會直接報錯,但他是極不安全的,存在內存泄漏問題。
切記:動態開辟的空間?定要釋放,并且正確釋放。
5.動態內存經典筆試題分析
5.1題目1:
void GetMemory(char* p)
{p = (char*)malloc(100);
}void Test(void)
{char* str = NULL;GetMemory(str);strcpy(str, "hello world");printf(str);
}
上述代碼運行Test函數會產生什么樣的結果?
什么都不會打印。
看起來問題不大,其實錯漏百出:
指針傳遞問題(最核心問題): GetMemory函數接收的是char* p的副本(值傳遞), 函數內修改的是副本指針,不影響外部的str 導致str在Test函數中始終為NULL ,參考傳值調用
內存泄漏: malloc分配的內存沒有被釋放,而且由于指針問題,分配的內存甚至無法被訪問
空指針解引用: strcpy試圖向NULL指針寫入數據,會導致程序崩潰
我們這里提供兩種方法改進。
第一種,改用二級指針,類似傳址調用。
第二種,函數返回指針。
exit(EXIT_FAILURE);包含在頭文件#include<stdlib.h>
exit() 函數:立即終止程序,清理緩沖區并關閉所有打開的文件。
EXIT_FAILURE:標準宏(通常值為1),表示程序異常終止。
5.2題目2:
char* GetMemory(void)
{char p[] = "hello world";return p;
}void Test(void)
{char* str = NULL;str = GetMemory();printf(str);
}int main()
{Test();return 0;
}
打印結果:
這個代碼犯了一個很嚴重的錯誤,p數組他是一個局部變量,他只能在GetMemory函數內使用,出了該函數它的內存空間就被釋放掉了,該函數返回的也是懸空指針,建議使用動態分配堆內存代替數組。
> 記住,永遠不要返回局部變量的地址
5.3題目3:
void GetMemory(char** p, int num)
{*p = (char*)malloc(num);
}void Test(void)
{char* str = NULL;GetMemory(&str, 100);strcpy(str, "hello");printf(str);
}int main()
{Test();return 0;
}
上述代碼是可以打印正常結果的,但是還是我們上面所說的一些常見的動態內存錯誤,沒有檢查malloc返回的是否為空指針,沒有釋放動態開辟的內存,大家不要認為自己不會犯這種錯誤,作者反復提醒,望注意。
5.4題目4:
void Test(void)
{char* str = (char*)malloc(100);strcpy(str, "hello");free(str);if (str != NULL){strcpy(str, "world");printf(str);}
}int main()
{Test();return 0;
}
這個代碼同樣為檢查malloc函數創建失敗的可能,但這題還有更大的錯誤,str經free函數釋放后,變成了一個懸空指針,對懸空指針進行再操作的行為是未定義的(可能導致崩潰和數據損壞),所以我們應該養成釋放后即使置空指針的習慣。
6.柔性數組
6.1柔性數組的介紹
在C99中,結構體的最后一個元素允許是未知大小的數組,這就叫柔性數組成員,如下:
struct st_type
{int i;int arr[];
};
柔性數組的特點:
? 結構中的柔性數組成員前?必須?少存在?個其他成員。
? sizeof 返回的這種結構??不包括柔性數組的內存。
? 包含柔性數組成員的結構?malloc ()函數進?內存的動態分配,并且分配的內存應該?于結構的??,以適應柔性數組的預期??。
看下面代碼:
6.2柔性數組的使用
typedef struct st_type
{int i;int arr[];
}type_1;int main()
{type_1* pz = (type_1*)malloc(sizeof(type_1) + 20 * sizeof(int));pz->i = 100;for (int i = 0; i < 20; i++){pz->arr[i] = i;}free(pz);pz = NULL;return 0;
}
上述的代碼也可以設計成下面的形式:
typedef struct st_type
{int i;int* ps;
}type_1;int main()
{type_1* pz = (type_1*)malloc(sizeof(type_1));pz->i = 100;pz->ps = (int*)malloc(20 * sizeof(int));for (int i = 0; i < 20; i++){pz->ps[i] = i;}free(pz->ps);pz->ps = NULL;free(pz);pz = NULL;return 0;
}
上述代碼并沒有檢查malloc返回空指針的可能性,這是可以改進的一點。
除此以外,你認為代碼一和代碼二哪個更優秀呢?
代碼一更優秀,理由有二:
第?個好處是:?便內存釋放,如果我們的代碼是在?個給別??的函數中,你在??做了?次內存分配,并把整個結構體返回給??。??調?free可以釋放結構體,但是??并不知道這個結構體內的成員也需要free,所以你不能指望??來發現這個事。所以,如果我們把結構體的內存以及其成員要的內存?次性分配好了,并返回給???個結構體指針,??做?次free就可以把所有的內存也給釋放掉。
第?個好處是:這樣有利于訪問速度,連續的內存有益于提?訪問速度,也有益于減少內存碎?。
加深結構體中成員數組與指針理解閱讀
7.總結C/C++語言程序內存區域劃分
C/C++程序內存分配的?個區域:
- 棧區(stack):在執?函數時,函數內局部變量的存儲單元都可以在棧上創建,函數執?結束時 這些存儲單元?動被釋放。棧內存分配運算內置于處理器的指令集中,效率很?,但是分配的內 存容量有限。棧區主要存放運?函數?分配的局部變量、函數參數、返回數據、返回地址等。
- 堆區(heap):?般由程序員分配釋放, 若程序員不釋放,程序結束時可能由OS回收 。分配? 式類似于鏈表。
- 數據段(靜態區)(static)存放全局變量、靜態數據。程序結束后由系統釋放。
- 代碼段:存放函數體(類成員函數和全局函數)的?進制代碼。