C語言之動態內存管理
大綱:
- 儲存器原理
- 為什么存在動態內存的開辟
- malloc()
- free()
- calloc()
- realloc()
- 常見錯誤
- 例題
- 柔性數組
零(上).存儲器原理
之前我們提到了計算機的儲存器,我們再來回憶一下:
我們當時說:
棧區:
這是存儲器用來保存局部變量的部分。每當調用函數,函數的所有局部變量都在棧 上創建。它之所以叫棧是因為它看起來就像堆積而成的棧板:當進入函數時,變量會放到棧頂;離開函數時,把變量從棧頂拿走。奇怪的是,棧做起事來顛三倒四,它從存儲器的頂部開始,向下增長。
堆區:
堆用於動態存儲:程序在運行時創建一些數據, 然后使用很長一段時間,
數據段:
全局量位於所有函數之外,並對所有函數 可見。程序一開始運行時就會創建全局量, 你可以修改它們,
常量也在程序一開始運行時創建,但它們保存在只讀存儲器中。常量是一些在程序中要用到的不變量,你不能修改它們的 值,例如字符串字面值。
代碼段:
很多操作系統都把代碼放在存儲器地址的低位。代碼段也是只讀的, 它是存儲器中用來加載機器代碼的部分。
零(下).為什么存在動態內存的開辟
在我們之前的學習中,我們關於內存的開辟都是靜態的:
如:
int val = 20;//在棧空間上開辟四個字節 char arr[10] = { 0 };//在棧空間上開辟10個字節的連續空間
但是我們發現,這樣的內存開辟存在兩個特點:
1. 空間開辟大小是固定的。
2.數組在申明的時候,必須指定數組的長度,它所需要的內存在編譯時分配。
可是,我們對於空間的需求,不僅僅是上述的情況,有時我們需要的空間大小需要程序運行的時候,我們才能知道,那這樣對於數組大小開辟就十分不好滿足了。
所以,我們就只好來試試動態內存開辟了!
一.malloc()
再C語言中,提供了一個動態內存開辟的函數:
我們來看看它的聲明:文檔
void* malloc(size_t size);
再來看看文檔:
注意:
這個函數向內存申請一塊連續可用的空間,並返回指向這塊空間的指針。
如果開辟成功,則返回一個指向開辟好空間的指針。
如果開辟失敗,則返回一個NULL指針,因此malloc的返回值一定要做檢查。
返回值的類型是 void* ,所以malloc函數並不知道開辟空間的類型,具體在使用的時候使用者自己來決定。
參數是你要開辟多少個字節,如果參數 size 為0,malloc的行為是標准是未定義的,取決於編譯器。
寫一個例子:
//void* malloc(size_t size);
//malloc
#include <stdio.h>
#include <stdlib.h> #include <limits.h> #include <errno.h> #include <string.h> int main() { int arr[10] = {0};//在棧區上申請了40個字節的空間 //動態內存開辟 - 堆區上 //INT_MAX----整形的最大字節,位於limit.h文件中 //int* p = (int*)malloc(INT_MAX);//開辟失敗的情況 int* p = (int*)malloc(40);//希望把40個字節當成一個10個整型的數組,因為我們開辟的指針類型是int*,所以我們也將返回值強行轉換為int* if (p == NULL) { //strerror 在string.h文件中 //errno 在errno.h 文件中 printf("內存開辟失敗: %s\n",strerror(errno));//打印錯誤信息,errno提供錯誤碼,strerror將提供的錯誤碼翻譯為一個字符串 perror("內存開辟失敗");//直接打印錯誤信息,直接包裝好的一個函數,在 stdio.h 中 char* p = strerror(errno);//如果我們只想得到錯誤信息,並不想打印出來,我們就可以用strerror(errno)獲得 printf("%s\n", p); } else { //開辟成功 int i = 0; for (i = 0; i < 10; i++) { *(p + i) = 0; } for (i = 0; i < 10; i++) { printf("%d ", p[i]); } //不再使用p指向的動態內存 //手動釋放動態開辟的內存 free(p);//這是我們開辟內存,最后且必要有的一步,釋放我們開辟的內存!! p = NULL; //...... } return 0; }
注意:
我們在開辟內存的時候,一定要檢查開辟成功了沒有,即下面這段代碼:
//假設 p 是我們賦予內存的指針
if (p == null)
{ //沒有開辟成功
//...
} else { //開辟成功 //... }
以及最后一定要釋放我們開辟的空間,即:
free(p);//這是我們開辟內存,最后且必要有的一步,釋放我們開辟的內存!! p = NULL;
所以,我們在這在介紹一下free()
二.free()
聲明:文檔
void free(void* ptr);
注意:
free函數用來釋放動態開辟的內存。
如果參數 ptr 指向的空間不是動態開辟的,那free函數的行為是未定義的。
如果參數 ptr 是NULL指針,則函數什么事都不做。
及時釋放,及時置NULL
示例同上
三.calloc()
它與malloc()都是用來開辟內存的,只不過malloc()沒有初始化,而calloc()則對於開辟的內存進行了初始化(全部置0),並且參數也由一個變成兩個。
聲明:文檔
void* calloc (size_t num, size_t size);
注意:
函數的功能是為 num 個大小為 size 的元素開辟一塊空間,並且把空間的每個字節初始化為0。
與函數 malloc 的區別只在於 calloc 會在返回地址之前把申請的空間的每個字節初始化為全0。
示例:
int main()
{
//int arr[10];
//開辟一個連續的空間
//malloc開辟的空間不初始化
//malloc參數只有1個
//calloc開辟的空間是初始化的
//calloc參數有2個
int*p = (int*)calloc(10, sizeof(int)); if (p == NULL) { printf("%s\n", strerror(errno)); } else { int i = 0; for (i = 0; i < 10; i++) { printf("%d ", *(p + i)); } printf("\n"); //釋放 free(p); p = NULL; } return 0; }
我們在這來觀察一下內存:
開辟后:
正好四十個字節置為了0.
所以:
以后我們要是對申請的內存空間的內容要求初始化,那么可以很方便的使用calloc函數來完成任務。
四.realloc()
有時會我們發現過去申請的空間太小了,有時候我們又會覺得申請的空間過大了,那為了合理的時候內存,
我們一定會對內存的大小做靈活的調整。那 realloc 函數就可以做到對動態開辟內存大小的調整。
聲明:文檔
void* realloc (void* ptr, size_t size);
注意:
ptr 是要調整的內存地址
size 調整之后新大小
返回值為調整之后的內存起始位置。
這個函數調整原內存空間大小的基礎上,還會將原來內存中的數據移動到 新 的空間。
realloc在調整內存空間的是存在兩種情況:
情況1:原有空間之后有足夠大的空間
情況2:原有空間之后沒有足夠大的空間
情況1: 當是情況1 的時候,要擴展內存就直接原有內存之后直接追加空間,原來空間的數據不發生變化。
情況2: 當是情況2 的時候,原有空間之后沒有足夠多的空間時,擴展的方法是:在堆空間上另找一個合適大小的連續空間來使用。
這樣函數返回的是一個新的內存地址。 由於上述的兩種情況,realloc函數的使用就要注意一些。
舉個例子:
#include <stdio.h>
int main()
{
int* ptr = malloc(100); if (ptr != NULL) { //業務處理 } else { exit(EXIT_FAILURE); } //擴展容量 //代碼1 --- 不可行 ptr = realloc(ptr, 1000);//這樣可以嗎?(如果申請失敗會如何?) //所以這樣不可行,若是開辟失敗,我們並無法得知,而且還會非法訪問! //代碼2 --- 可行 int* p = NULL; p = realloc(ptr, 1000); if (p != NULL) { ptr = p;//這里要記得用我們原來的地址接收返回的地址 //上面我們提到:要是原有空間之后沒有足夠多的空間時,擴展的方法是:在堆空間上另找一個合適大小的連續空間來使用。 //這樣函數返回的是一個新的內存地址,所以我們要記得接收! } //業務處理 free(ptr);//一定要記得釋放 ptr = NULL;//置NULL return 0; }
注意點:
若是開辟成功,則要記得用原來指針來接收返回的指針
及時釋放,及時置NULL
五.常見錯誤
1.對NULL指針的解引用操作
//1. 對NULL指針的解引用操作
//避免出現:對 malloc/calloc/realloc 函數的返回值做檢測
int main()
{
int*p = (int*)malloc(INT_MAX); //p是有可能為NULL指針的,當為NULL的時候,*p就是非法訪問內存 *p = 0; return 0; }
所以我們要記得對 malloc/calloc/realloc 函數的返回值做檢測
如:
//假設 p 是我們賦予內存的指針
if (p == null)
{ //沒有開辟成功
//...
} else { //開辟成功 //... }
2.對動態開辟空間的越界訪問
//2. 對動態開辟空間的越界訪問
int main()
{
int*p = (int*)malloc(10 * sizeof(int)); if (p == NULL) { return 1; } else { int i = 0; //越界 for (i = 0; i <= 10; i++) { *(p + i) = 0;//等於10的時候就越界了 } free(p); p = NULL; } return 0; }
對於越界的問題,我們從數組那便已經提到要注意了
3.對非動態開辟內存使用free釋放
//3. 對非動態開辟內存使用free釋放
int main()
{
int a = 10; int*p = &a; //... free(p); p = NULL; return 0; }
4. 使用free釋放一塊動態開辟內存的一部分
//4. 使用free釋放一塊動態開辟內存的一部分
int main()
{
int*p = (int*)malloc(10 * sizeof(int)); if (p == NULL) { return 1; } else { int i = 0; //err for (i = 0; i <5; i++) { *p++ = 0;//這里p++是有副作用的,會導致p指向的值改變 //*(p + i) = 0;//這里應該寫為*(p + i) } //釋放 free(p);//我們釋放內存時,一定要從我們開始的位置進行釋放! p = NULL; } return 0; }
5.對同一塊動態內存多次釋放
//5. 對同一塊動態內存多次釋放
int main()
{
int*p = (int*)malloc(10 * sizeof(int)); if (p == NULL) { return 1; } else { int i = 0; //err for (i = 0; i <5; i++) { *(p + i) = 0; } //多次釋放會有問題 free(p); free(p); p = NULL; } return 0; }
6.動態開辟內存忘記釋放(內存泄漏)
//6.動態開辟內存忘記釋放(內存泄漏)
void test()
{
int* p = (int*)malloc(100); if (NULL != p) { *p = 20; } } int main() { test(); while (1);//未釋放內存 }
所以我們一定要記得及時釋放,及時置NULL
六.例題
1.
//例題一
void GetMemory(char* p)
{
p = (char*)malloc(100); } void Test(void) { char* str = NULL; GetMemory(str); strcpy(str, "hello world"); printf(str); } int main() { Test(); return 0; } //運行Test()會有什么結果
我們在這要注意GetMemory()函數的參數為 char*,而我們傳過去的str也是char* ,所以這就造成了我們在函數里修改其值,到函數結尾的時候它並不會進行實質性的改變,
就像交換兩個整型變量的值的時候,我們要是把參數寫為int,那這個函數其實並沒有什么用。要是想要修改它就得要比他高一級。
所以在這,相當於GetMemory()函數什么也沒干;
str依然是個NULL
而strcpy()函數是要對傳進的參數進行斷言的,不能為空指針,而我們傳遞過去了一個空指針;
所以這個程序會崩。
而要是想要改變它,我們就得這樣寫:
#include <stdio.h> #include <stdlib.h> #include <string.h> //例題一 void GetMemory(char** p) { *p = (char*)malloc(100); } void Test(void) { char* str = NULL; GetMemory(&str); strcpy(str, "hello world"); printf(str); } int main() { Test(); return 0; }
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; } //運行Test()會有什么結果
我們要注意,在一個自定義函數結束的時候,它所創建的變量會被銷毀;
所以p返回的地址內容不再是函數里所創建的 h 了,而是被銷毀后,我們也不知道的內容;
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; } //運行Test()會有什么結果
在例三,我們是置str為NULL,然后我們傳過去的是str的地址,並不是NULL;
所以在函數里是對str指向的NULL內容進行改變,而不是NULL本身;
但是,這里有一點 程序並無free(),所以就會造成內存泄漏的問題!
因此,該函數最后的結果就為屏幕上輸出 hello
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; } //運行Test()會有什么結果
這題是提前釋放了內存,但並沒有及時置NULL,之后再進行strcpy(),理應是非法訪問,可是編譯器卻給出了world的結果;
這就說明,我們也不要太相信編譯器!
VS 2019 :
gcc:
七.柔性數組
1.柔性數組:
也許你從來沒有聽說過柔性數組(flexible array)這個概念,但是它確實是存在的。 C99 中,結構中的最后一個元素允許是未知大小的數組,這就叫做『柔性數組』成員。
例如:
typedef struct st_type
{
int i; int a[0];//柔性數組成員 }type_a;
若有一些編譯器報錯,則可換為以下寫法:
typedef struct st_type
{
int i; int a[];//柔性數組成員//柔性數組指的是這個數組的大小是柔性可變的 }type_a;
2.柔性數組的特點:
結構中的柔性數組成員前面必須至少一個其他成員。
sizeof 返回的這種結構大小不包括柔性數組的內存。
包含柔性數組成員的結構用malloc ()函數進行內存的動態分配,並且分配的內存應該大於結構的大小,以適應柔性數組的預期大小。
例如:
typedef struct st_type
{
int i; int a[0];//柔性數組成員 }type_a; int main() { printf("%d\n", sizeof(type_a));//輸出的是4//在計算機包含柔型數組成員的結構體的大小的時候,不包含柔性數組成員
return 0;
}
因為sizeof 返回的這種結構大小不包括柔性數組的內存,所以結果為 4.
3.柔性數組的使用
如:
struct S
{
int n; int arr[];//柔性數組指的是這個數組的大小是柔性可變的 }; int main() { //struct S s;//不是創建的 struct S* ps = (struct S*)malloc(sizeof(struct S) + 10 * sizeof(int));//前半部分是指結構體除柔性數組外的大小,后半部分是給柔性數組分配的大小 ps->n = 100; int i = 0; for (i = 0; i < 10; i++) { ps->arr[i] = i; } //釋放 free(ps); ps = NULL; return 0; }
這樣就給柔性數組分配了10個整形元素大小
4.柔性數組的優勢
那么說了這么多,那柔性數組的優勢在哪呢?
我們來看下面這兩段代碼:
typedef struct st_type
{
int i; int a[0];//柔性數組成員 }type_a; //代碼1 int main() { int i = 0; type_a* p = (type_a*)malloc(sizeof(type_a) + 100 * sizeof(int)); //業務處理 p->i = 100; for (i = 0; i < 100; i++) { p->a[i] = i; } free(p); return 0; }
//代碼2
typedef struct st_type
{
int i; int* p_a; }type_a; int main() { int i = 0; type_a* p = malloc(sizeof(type_a)); p->i = 100; p->p_a = (int*)malloc(p->i * sizeof(int)); //業務處理 for (i = 0; i < 100; i++) { p->p_a[i] = i; } //釋放空間 free(p->p_a); p->p_a = NULL; free(p); p = NULL; return 0; }
上述代碼1和代碼2實現了同樣的功能,但是硬要讓我選擇一個,那我選擇代碼1
原因如下:
1.方便內存釋放
如果我們的代碼是在一個給別人用的函數中,你在里面做了二次內存分配,並把整個結構體返回給用戶。用戶調用free可以釋放結構體,但是用戶並不知道這個結構體內的成員也需要free,
所以你不能指望用戶來發現這個事。所以,如果我們把結構體的內存以及其成員要的內存一次性分配好了,並返回給用戶一個結構體 指針,用戶做一次free就可以把所有的內存也給釋放掉。
2.這樣有利於訪問速度.
連續的內存有益於提高訪問速度,也有益於減少內存碎片。(其實,我個人覺得也沒多高了,反正你跑不了 要用做偏移量的加法來尋址)
此處參考:C語言結構體里的成員數組和指針
|------------------------------------------------------------------
到此,對於動態內存管理的講解便結束了!
若有錯誤之處,還望指正!
因筆者水平有限,若有錯誤,還請指正!