目錄
一、為什么存在動態內存分配
二、動態內存函數介紹
2.1 malloc
2.2 free
2.3 calloc
2.4 realloc
三、常見的動態內存錯誤
3.1 對NULL指針的解引用操作
3.2?對動態開辟空間的越界訪問
3.3?對非動態開辟內存使用free釋放
3.4 使用free釋放一塊動態開辟內存的一部分
3.5 對同一塊動態內存多次釋放
3.6 動態開辟內存忘記釋放(內存泄漏)
????????今天,博主給大家帶來的是動態內存分配的學習和講解。在之前,我們學習了通訊錄,文章中利用到一些動態內存分配的一些知識,有些可能大家會看不懂,那么相信通過今天的這篇文章,大家的問題就會迎刃而解。本篇,我們將從“為什么存在內存分配”,“動態內存函數介紹”,以及“常見的動態內存錯誤”三個板塊來為大家一 一解答。
一、為什么存在動態內存分配
在之前,我們學過的內存開辟有哪些呢?比如,創建一個變量,或者創建一個數組。
int a = 10;//在棧空間開辟四個字節char arr[10] = { 0 };//在棧空間開辟十個字節的連續空間
上面兩種開辟方式是我們常用的開辟內存的方式,但是這兩種開辟內存空間的方式有兩個特點:
① 空間開辟大小是固定的
②?數組在申明的時候,必須指定數組的長度,它所需要的內存在編譯時分配。
但是由于空間的需求,有時候我們需要空間的大小在程序運行的時候才能知道,那數組的編譯時開辟空間的方式就不能滿足我們的需求了,這時就要試試動態內存開辟的方式了。
二、動態內存函數介紹
在學習動態內存函數之前,我們需要知道動態內存開辟的空間是放在堆區的,如上圖所示,局部變量和形式參數占用的空間是在棧區的,全局變量以及靜態變量開辟的空間是在靜態區的。?
2.1 malloc
C語言提供了一個動態內存開辟函數
void* malloc? (?? size_t? ? size?? ) ;
這個函數向內存申請一塊 連續可用 的空間,并返回指向這塊空間的指針。① 如果開辟成功,則返回一個指向開辟好空間的指針。② 如果開辟失敗,則返回一個NULL指針,因此 malloc 的返回值一定要做檢查。③ 返回值的類型是 void* ,所以 malloc 函數并不知道開辟空間的類型,具體在使用的時候使用者自己來決定。④ 如果參數 size 為 0 , malloc 的行為是標準是未定義的,取決于編譯器。
int main()
{int* p = (int*)malloc(40);//開辟40個字節的空間if (p == NULL){perror("malloc");return 1;}//開辟成功for (int i = 0; i < 10; i++){printf("%d\n", *(p + i));}return 0;
}
因為返回的類型是void*,所以我們要根據自己的需求來進行強制類型轉換,其次,我們需要判斷是否開辟成功,如果返回值為NULL指針,那么就結束了,反之則是開辟成功。然后我們打印一下看看開辟成功的空間里面是什么。

此時我們發現開辟的空間里面存的是一堆沒見過的隨機數數,其實是malloc函數申請的空間,在申請成功后,直接返回這片空間的起始地址,不會初始化空間的內容。?
2.2 free
void free (? void*?? ptr? ) ;
上面我們學習了malloc函數,我們發現,malloc只負責申請空間,那么申請的這個空間當我們使用完之后會怎么樣呢?其實這塊空間并不會主動的還給操作系統,除非程序結束,否則這塊空間將會一直存在堆區。這個時候就需要另一個內存函數了。總的來說: malloc申請的內存空間,當程序退出時,還給操作系統,當程序不退出時,動態申請的內存不會主動釋放。需要free函數來釋放空間。
int main()
{int* p = (int*)malloc(40);if (p == NULL){perror("malloc");return 1;}//開辟成功free(p); //釋放開辟的空間p = NULL; //置空return 0;
}
注意:p本來指向的空間被釋放后,p就變成野指針了,比較危險,這時候我們需要主動將p置為空指針。
①free只能釋放動態開辟的空間,不能釋放靜態區或者棧區開辟的空間(標準未定義)?如果參數 ptr 指向的空間不是動態開辟的,那free函數的行為是未定義的。
② 如果參數 ptr 是 NULL 指針,則函數什么事都不做。malloc和free都聲明在 stdlib.h 頭文件中
2.3 calloc
void*?? calloc (? size_t?? num? ,?? size_t?? size? ) ;
① 函數的功能是為 num 個大小為 size 的元素開辟一塊空間 ,并且把空間的每個字節初始化為 0 。② 與函數 malloc 的區別只在于 calloc 會在返回地址之前把申請的空間的每個字節初始化為全 0 。
int main()
{int* p = (int*)calloc(10, sizeof(int));//開辟十個連續的sizeof(int)大小的空間if (p == NULL){perror("calloc");return 1;}//打印數據int i = 0;for (i = 0; i < 10; i++){printf("%d ", p[i]);}//釋放free(p);p = NULL;return 0;
}
當我們打印完之后,發現calloc會把每個字節初始化為0。
總的來說:calloc函數和malloc函數很相似,功能也是一樣,唯一不同的就是,會把開辟的每個字節都初始化為0。
????????所以如何我們對申請的內存空間的內容要求初始化,那么可以很方便的使用calloc 函數來完成任務。
2.4 realloc
void*?? realloc? (? void*?? ptr? ,?? size_t?? size? ) ;
① ptr 是要調整的內存地址(也就是被調整空間的起始地址,這塊空間之前已經開辟好了)如果ptr為空指針,那么它的功能和malloc就是一樣的,開辟一個新的空間。② size 調整之后新大小 (需要調整的新的空間的大小)③ 返回值為調整之后的內存起始位置。這個函數調整原內存空間大小的基礎上,還會將原來內存中的數據移動到 新 的空間。④ realloc 在調整內存空間的是存在兩種情況:情況1 :原有空間之后有足夠大的空間情況2: 原有空間之后沒有足夠大的空間

情況1
當是情況1 的時候,要擴展內存就直接原有內存之后直接追加空間,原來空間的數據不發生變化。
情況2
當是情況2 的時候,原有空間之后沒有足夠多的空間時,擴展的方法是:在堆空間上另找一個合適大小的連續空間來使用,同時把原來空間的內容拷貝過來,然后自動釋放原來的內存空間,這樣函數返回的是一個新的內存地址。
int main()
{int* p = (int*)malloc(40);if (p == NULL){perror("malloc");return 1;}//初始化為1~10int i = 0;for (i = 0; i < 10; i++){p[i] = i + 1;}//增加一些空間int* ptr = (int*)realloc(p, 80);if (ptr != NULL) //開辟成功{p = ptr;ptr = NULL;}else //開辟失敗{perror("realloc");return 1;}//開辟成功,打印數據for (i = 0; i < 20; i++){printf("%d ", p[i]);}free(p); //釋放空間p = NULL; //置空return 0;
}
注意:考慮到可能開辟失敗,所以我們需要先進行判斷一下,如果開辟成功,則將返回的指針賦值給p來維護。
當我們開辟完之后,打印一下看看效果。
當我們使用完動態開辟的內存之后,仍然需要手動去釋放內存空間。

?當然,上面情況只是減少增加空間,如果要減少空間就比較簡單了,直接在原來的基礎上減少,地址返回的也是原來的地址。
好了,到這里動態內存管理的基本知識就介紹清楚了,實際上把這四個內存函數了解清楚,基本上對動態內存的知識點也就基本掌握了。接下來,我們來了解一下動態內存管理常見的內存錯誤,通過解釋這些錯誤,來更清楚更深入的了解動態內存管理。
三、常見的動態內存錯誤
3.1 對NULL指針的解引用操作
當我們動態內存開辟的時候,會存在開辟失敗的情況,此時返回的就是空指針。
如下代碼就是一個典型的例子:
void test()
{int *p = (int *)malloc(INT_MAX/4);*p = 20;//如果p的值是NULL,就會有問題free(p);
}
此時動態內存開辟可能失敗了,就導致返回的指針為空指針,也就是p為空指針,如果再對p這個空指針進行解引用操作,那么就會報錯 。為了解決這種問題,我們在平時寫代碼的時候,為了避免空指針異常,要對動態開辟的空間返回的指針進行空指針判斷檢查。(好習慣)
3.2?對動態開辟空間的越界訪問
這里直接拿代碼來解釋吧!
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) = i;//當i是10的時候越界訪問}free(p);
}
這段代碼中,我們使用malloc開辟40個字節大小的空間,然后進行空指針檢查判斷,緊接著,在我們賦值的時候,我們最多只能訪問到p[9]這塊空間,代碼中我們i的最大值為10,此時很明顯,就出現了越界訪問。
總的來說:開辟多少空間,就只能使用多少空間,沒有開辟的,屬于操作系統的空間,我們不可以隨便進行操作訪問。
3.3?對非動態開辟內存使用free釋放
void test()
{int a = 10;int *p = &a;free(p);//ok?
}
在前面講free的時候說過,free釋放的空間,只能是動態內存開辟的空間,不能是靜態區或者棧區開辟的空間,上面代碼的例子中,a是局部變量,不是動態開辟的空間,所以不能被free釋放。
3.4 使用free釋放一塊動態開辟內存的一部分
int main()
{int* p = (int*)malloc(40);if (p == NULL){printf("malloc");return 1;}int i = 0;for (i = 0; i < 5; i++){*p = i + 1;p++; //此時p不再指向malloc開辟的空間的起始地址}//釋放free(p);p = NULL;return 0;
}
上面代碼中,我們使用malloc開辟一塊空間,然后將起始地址返回給p,也就是說p指向molloc動態開辟的空間的起始地址,緊接著進行空指針檢查判斷,然后給p指向的空間進行賦值,但是,在賦值的過程中,p的指向發生了變化(如下圖),不再指向malloc開辟的空間的起始地址,此時在對p指向的空間進行釋放,這種做法是不被允許的,會報錯。
總結:free中的參數只能是動態內存開辟空間的起始地址,對于動態開辟的空間的起始地址不要隨便的更改指向。
3.5 對同一塊動態內存多次釋放
void test()
{int *p = (int *)malloc(100);free(p);free(p);//重復釋放
}
上面這種情況也是一直很低級的錯誤,也是不被允許的,會報錯,最好的解決辦法就是,在釋放完之后,將p指針置為空,這樣在后面多次釋放也不影響,因為p已經是空指針了。
總結:不能對同一塊動態內存多次釋放,解決辦法:在第一次釋放完之后,將指針置為空指針(好習慣)
3.6 動態開辟內存忘記釋放(內存泄漏)
void test()
{int *p = (int *)malloc(100);if(NULL != p){*p = 20;}
}
int main()
{test();while(1);
}
對于上面這個代碼,在test這個函數種,我們開辟了100個字節的動態空間,但是在最后,我們并沒有對這塊空間進行free釋放,這時候當我們跳出函數之后,指針變量p也銷毀了,這個時候,p原來指向的空間我們根本找不到了,但是這塊空間仍然存在,仍然被占用,只是我們丟失了它的起始地址,不能再對這塊空間進行操作或者訪問了,這就造成了這塊空間仍然存在占用內存,但是我們卻訪問不到,并無法釋放,這就是所謂的內存泄露。
針對這個問題的解決:在我們使用完動態空間之后,一定要進行free釋放。
總結:動態開辟的空間一定要釋放,并且正確釋放(切記)
好了,今天的動態內存分配和管理講到這里就結束了,聽到這里,相信大家的一些關于動態內存分配管理的問題就會迎刃而解了吧。如果哪里有問題,歡迎在評論區留言。如果覺得小編寫的還不錯的,那么可以一鍵三連哦,您的關注點贊和收藏是對小編最大的鼓勵。謝謝大家!!!