C語言之動態記憶體管理
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; }
這裡是 strerror()的文件:點我
這裡是 errno()的文件:點我
這裡是 perror()的文件:點我
注意:
我們在開闢記憶體的時候,一定要檢查開闢成功了沒有,即下面這段程式碼:
//假設 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()函式中傳了一個NULL;
而對NULL指標我們是無法擴充套件記憶體的,相當於GetMemory()函式什麼也沒幹;
而strcpy()函式是要對傳進的引數進行斷言的,不能為空指標,而我們傳遞過去了一個空指標;
所以這個程式會崩。
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()會有什麼結果
這個要注意我們的例一是直接傳了NULL過去,
而在例三,我們是置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語言結構體裡的成員陣列和指標
|------------------------------------------------------------------
到此,對於動態記憶體管理的講解便結束了!
若有錯誤之處,還望指正!
因筆者水平有限,若有錯誤,還請指正!
&n