
目錄
- 為什么存在動態記憶體分配
- 動態記憶體函式的介紹
- malloc和free
- free函式的注意事項
- 舉例一
- 舉例二
- 正確使用
- calloc
- realloc
- 常見的動態記憶體錯誤
- 對NULL指標的解參考操作
- 對動態開辟空間的越界訪問
- 對非動態開辟記憶體使用free釋放
- 使用free釋放一塊動態開辟記憶體的一部分
- 對同一塊動態記憶體多次釋放
- 動態開辟記憶體忘記釋放(記憶體泄漏)
- 幾個經典的筆試題
- 題目1:
- 題目二
- 題目3:
- 題目4:
- C/C++程式的記憶體開辟
- 柔性陣列
- 柔性陣列的特點
- 柔性陣列的使用
- 總結:
為什么存在動態記憶體分配
為什么存在動態記憶體分配
首先我們先了解一下記憶體中的幾個區域
堆疊區、堆區、靜態區

我們已經掌握的記憶體開辟方式有:
int val = 20;//在堆疊空間上開辟四個位元組
char arr[10] = {0};//在堆疊空間上開辟10個位元組的連續空間
但是上述的開辟空間的方式有兩個特點:
- 空間開辟大小是固定的,
- 陣列在申明的時候,必須指定陣列的長度,它所需要的記憶體在編譯時分配,
但是對于空間的需求,不僅僅是上述的情況,有時候我們需要的空間大小在程式運行的時候才能知道,那陣列的編
譯時開辟空間的方式就不能滿足了, 這時候就只能試試動態存開辟了,
動態記憶體函式的介紹
malloc和free
C語言提供了一個動態記憶體開辟的函式,它的函式原型是這樣的

這個函式向記憶體申請一塊連續可用的空間,并回傳指向這塊空間的指標, 如果開辟成功,則回傳一個指向開辟好空間的指標,并不會初始化空間,
如果開辟失敗,則回傳一個NULL指標,因此malloc的回傳值一定要做檢查,回傳值的型別是 void* ,所以malloc函式并不知道開辟空間的型別,具體在使用的時候使用者自己來決定, 如果引數 size
為0,malloc的行為是標準是未定義的,取決于編譯器,
C語言提供了另外一個函式free,專門是用來做動態記憶體的釋放和回收的,函式原型如下:
C語言提供了一個動態記憶體釋放的函式,它的函式原型是這樣的

free函式的注意事項
- free函式用來釋放動態開辟的記憶體,
- 如果引數 ptr 指向的空間不是動態開辟的,那free函式的行為是未定義的,
- 如果引數 ptr是NULL指標,則函式什么事都不做,
舉例一
int main()
{
int a[10] = {0};
int *p = a;
//那free函式的行為是未定義的,
free(p);//err
return 0;
}
舉例二
int main()
{
int *p = (int *)malloc(sizeof(int) * 10);
free(p);
p = NULL;
//如果引數 ptr是NULL指標,則函式什么事都不做,
free(p);
p = NULL;
return 0;
}
正確使用
void test()
{
int i = 0;
//動態申請
int *p = (int *)malloc(sizeof(int) * 10);
//申請失敗
if (p == NULL)
{
printf("malloc file\n");
exit(-1);
}
//申請成功
else
{
for (i = 0; i < 10; i++)
{
*(p + i) = i;
}
for (i = 0; i < 10; i++)
{
printf("%d\n", *(p + i));
}
}
//釋放堆上的申請的空間,指標置空
free(p);
p = NULL;
}
int main()
{
test();
return 0;
}
值得一提的是,這里的釋放完了必須得將指標置空,如果指標不置空,那么這個指標將會會成為野指標!野指標是一種很可怕的事情,并且錯誤不好排查,回到主體,很明顯執行完free后記憶體已經釋放了,記憶體也歸還給作業系統了,可是指標還指向這塊空間,不屬于你的記憶體是不允許訪問的,訪問了就得受到懲罰(程式崩潰)
釋放完了,習慣性地將指標置空,這樣才是最安全的,指標間的關系已經被切斷了
calloc

- 函式的功能是為 num 個大小為 size 的元素開辟一塊空間,并且把空間的每個位元組初始化為0,
- 與函式 malloc 的區別只在于 calloc 會在回傳地址之前把申請的空間的每個位元組初始化為全0,
int mian()
{
int *p = (int *)calloc(10,sizeof(int));
//申請失敗
if (p == NULL)
{
printf("%s\n",strerror(errno));
exit(-1);
}
return 0;
}
函式的功能是為 num 個大小為 size 的元素開辟一塊空間,并且把空間的每個位元組初始化為0,從除錯視窗中很好觀察

realloc
realloc函式的出現讓動態記憶體管理更加靈活,
有時會我們發現過去申請的空間太小了,有時候我們又會覺得申請的空間過大了,那為了合理的使用記憶體,
我們一定會對記憶體的大小做靈活的調整,那 realloc 函式就可以做到對動態開辟記憶體大小的調整, 函式原型
如下

在第一種情況下,釋放原來的塊,在第二種情況下,原始塊沒有變化,回傳值指向一個存盤空間,該存盤空間保證對任何型別的物件進行適當的對齊
如果函式未能分配請求的記憶體塊,則回傳一個空指標,并且引數ptr指向的記憶體塊沒有被釋放(它仍然有效,內容沒有改變),
-
ptr是要調整的記憶體地址 size調整之后新大小 回傳值為調整之后的記憶體起始位置,
-
這個函式調整原記憶體空間大小的基礎上,還會將原來記憶體中的資料移動到 新 的空間,
realloc在調整記憶體空間的是存在兩種情況:
情況1:原有空間之后有足夠大的空間
情況2:原有空間之后沒有足夠大的空間

-
情況1 當是情況1 的時候,要擴展記憶體就直接原有記憶體之后直接追加空間,原來空間的資料不發生變化,
-
情況2 當 是情況2
的時候,原有空間之后沒有足夠多的空間時,擴展的方法是:在堆空間上另找一個合適大小的連續空間來 使用,這樣函式回傳的是一個新的記憶體地址,

void test()
{
int i = 0;
//動態申請
int *p = (int *)malloc(sizeof(int) * 10);
//申請失敗
if (p == NULL)
{
printf("malloc file\n");
exit(-1);
}
//申請成功
for (i = 0; i < 10; i++)
{
*(p + i) = i;
}
int *pt = (int *)realloc(p, 20 * sizeof(int));
//申請成功
if (pt != NULL)
{
//p去維護新的空間
p = pt;
//初始化后面的10塊空間
for (int i = 10; i < 20; i++)
{
*(p + i) = i;
}
//列印
for (int i = 0; i < 20; i++)
{
printf("%d ",*(p + i));
}
}
else //如果ptr開辟失敗,p還是維護自己原本的空間
{
//列印
for (int i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}
}
//釋放空間,指標置空,防止記憶體泄漏和野指標
free(p);
p = NULL;
}
由于上述的兩種情況,realloc函式的使用就要注意一些,
常見的動態記憶體錯誤
對NULL指標的解參考操作
void test()
{
int *p = (int *)malloc(INT_MAX/4);
*p = 20;//如果p的值是NULL,就會有問題
free(p);
}
未加檢查,如果動態記憶體開辟失敗回傳的是NULL指標,對空指標解參考程式會崩,并且free也不會作業
對動態開辟空間的越界訪問
void test()
{
int i = 0;
int *p = (int *)malloc(10*sizeof(int));
//開辟失敗
if(NULL == p)
{
exit(EXIT_FAILURE);
}
//開辟成功
for(i=0; i<=10; i++)
{
*p++ = i;
//*(p + i) = i; p自己沒有變
//p[i] = i; 等價上面
}
//釋放
free(p);
p = NULL;
return;
}
這里的p++,使得p的指向發生了變化,他已經不再指向這塊記憶體的起始地址了,跑遠了,修正方法很簡單*(p + i) = i,p自身并沒有改變,只是p的步子越邁越大
對非動態開辟記憶體使用free釋放
void test()
{
int a = 10;
int *p = &a;
free(p);//ok?
}
錯誤,free只能釋放動態申請的記憶體
使用free釋放一塊動態開辟記憶體的一部分
void test()
{
int *p = (int *)malloc(100);
p++;
free(p);//p不再指向動態記憶體的起始位置
}
只是從第二塊空間開始釋放的
前面這塊空間并沒有別釋放,就會導致記憶體泄漏
對同一塊動態記憶體多次釋放
void test()
{
//錯誤寫法,同一塊記憶體不能釋放多次
int *p = (int *)malloc(100);
free(p);
free(p);//重復釋放
//正確釋放
int *p = (int *)malloc(100);
free(p);
p = NULL;
free(p);//p 為NULL free就不會執行
p = NULL;
}
同一塊記憶體不能多次釋放
動態開辟記憶體忘記釋放(記憶體泄漏)
void test()
{
int *p = (int *)malloc(100);
if(NULL != p)
{
*p = 20;
}
}
int main()
{
test();
}
test函式被呼叫完記憶體并未歸還給作業系統,這是因為動態申請的記憶體沒有釋放free§ 后再將p置空
幾個經典的筆試題
題目1:
void GetMemory(char *p) {
p = (char *)malloc(100);
}
void Test(void) {
char *str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
看這一組,代碼看上去有一種迷惑行為,先將str置空指標,再呼叫GetMemory(char *p)函式接受這個空指標,此時指標變數p也是指向空,值得一提的是一級指標傳遞給一級指標,形參是實參的一份臨時拷貝,p的改變并不會影響外面的str,所以str還是一個空指標,
strcpy(str, “hello world”);將hello world拷貝給str會對空指標解參考這就會導致程式崩潰,
解決方案:二級指標
void GetMemory(char **p) {
//傳址呼叫,改變外面的str指向
*p = (char *)malloc(100);
}
void Test(void) {
char *str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
傳二級指標,里面的p的改變就會影響外面的str,strcpy(str, “hello world”);對str解參考就可行,
解決方案:回傳指標
char* GetMemory(char *p) {
p = (char *)malloc(100);
return p;
}
void Test(void) {
char *str = NULL;
str = GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
利用回傳值改變str的指向,str不再指向空,
題目二
char *GetMemory(void) {
char p[] = "hello world";
return p; }
void Test(void) {
char *str = NULL;
str = GetMemory();
printf(str);
}
str = GetMemory(); 這行代碼是在呼叫GetMemory,當GetMemory呼叫完了就會回傳一個指標變數,他是一個區域變數,區域變數出了作用域就會被銷毀了,此時的GetMemory函式中的p在回傳回來就是一個野指標了,野指標指向哪里是不確定的,當在列印的時候就會是隨機值
題目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);
}
這段代碼并不存在導致程式崩潰的問題,但是有一個記憶體泄漏的問題,str指向的記憶體并沒有被釋放
題目4:
void Test(void) {
char *str = (char *) malloc(100);
strcpy(str, "hello");
free(str);
if(str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
很明顯這段程式的錯誤就是str指向的空間被釋放了,但是str并沒有置為空指標,但是str還是能夠記得上一次存放的地址,繼續將world存放進這段記憶體中,列印出來的是word,
C/C++程式的記憶體開辟

- C/C++程式記憶體分配的幾個區域:
- 堆疊區(stack):在執行函式時,函式內區域變數的存盤單元都可以在堆疊上創建,函式執行結束時這些 存盤單元自動被釋放,堆疊記憶體分配運算內置于處理器的指令集中,效率很高,但是分配的記憶體容量有 限,
堆疊區主要存放運行函式而分配的區域變數、函式引數、回傳資料、回傳地址等, - 堆區(heap):一般由程式員分配釋放, 若程式員不釋放,程式結束時可能由OS回收 ,分配方式類似 于鏈表,
- 資料段(靜態區)(static)存放全域變數、靜態資料,程式結束后由系統釋放,
- 代碼段:存放函式體(類成員函式和全域函式)的二進制代碼
1、實際上普通的區域變數是在堆疊區分配空間的,堆疊區的特點是在上面創建的變數出了作用域就銷毀,
2、但是被static修飾的變數存放在資料段(靜態區),資料段的特點是在上面創建的變數,直到程式結束才銷毀 所以生命周期變長,
柔性陣列
- C99 中,結構中的最 后一個元素允許是未知大小的陣列,這就叫做『柔性陣列』成員
typedef struct st_type
{
int i;
int a[0];//柔性陣列成員
}type_a;
有些編譯器會報錯無法編譯可以改成:
typedef struct st_type
{
int i;
int a[];//柔性陣列成員
}type_a;
柔性陣列的特點
結構中的柔性陣列成員前面必須至少一個其他成員,
sizeof 回傳的這種結構大小不包括柔性陣列的記憶體,
包含柔性陣列成員的結構用malloc ()函式進行記憶體的動態分配,并且分配的記憶體應該大于結構的大小,以適應
柔性陣列的預期大小,
typedef struct st_type
{
int i;
int a[0];//柔性陣列成員
}type_a;
printf("%d\n", sizeof(type_a));//輸出的是4
首先可以看到這是一次包含柔性陣列的結構體宣告,并創建了一個結構體變數,包含柔性陣列的結構體也存在記憶體對齊,并且在sizeof計算的時候,結構大小不包括柔性陣列的記憶體,
柔性陣列的使用
typedef struct st_type
{
int i;
int a[0];//柔性陣列成員
}type_a;
void func()
{
struct st_type *p = (struct st_type *)malloc(sizeof(struct st_type) + sizeof(int) * 10);
//開辟失敗
if (!p)
{
printf("%s\n", strerror(errno));
exit(-1);
}
//申請成功
else
{
//初始化結構體成員
p->i = 10;
int i = 0;
for (; i < 10; i++)
{
p->a[i] = i;
}
//列印
i = 0;
for (; i < 10; i++)
{
printf("%d ", p->a[i]);
}
}
//想到擴容怎么辦,注意這里擴容的話要擴容的是整個結構體大小,并不是只擴容陣列的大小
struct st_type *ptr = (struct st_type *)realloc(p,sizeof(struct st_type) + sizeof(int) * 20);
//開辟成功,ptr去維護新的空間,舊的空間被釋放
if (ptr != NULL)
{
int j = 10;
for (;j < 20; j++)
{
ptr->a[j] = j;
}
j = 10;
for (; j < 20; j++)
{
printf("%d ", ptr->a[j]);
}
}
//開辟失敗,p繼續維護舊的空間
else
{
printf("擴容失敗\n");
//p賦值給ptr
ptr = p;
}
//釋放ptr所指向的記憶體空間,并將指標置空
free(ptr);
ptr = NULL;
}
- 使用柔性陣列的好處在于不需要二次分配記憶體,如果我們把結構體的記憶體以及其成員要的記憶體一次性分配好了,并回傳給用戶一個結構體
指標,用戶做一次free就可以把所有的記憶體也給釋放掉, - 連續的記憶體有益于提高訪問速度,也有益于減少記憶體碎片(頻繁malloc的次數多了也是會有記憶體消耗的),
總結:
柔性陣列就是一個結構體的最后一個成員,可以是一個未知大小的陣列,這個陣列你想要讓他變大變小可以配合著動態記憶體開辟來一起使用,這個時候這個成員就被稱為柔性陣列成員
完

轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/303049.html
標籤:其他



