一文解析C語言中動態(tài)內(nèi)存管理
1. 靜態(tài)開辟內(nèi)存
通過前面的學(xué)習(xí),我們已經(jīng)掌握了兩種開辟內(nèi)存的方法,分別是:
#include<stdio.h> int main() { int val = 20; //在棧空間上開辟四個字節(jié) char arr[10] = { 0 }; //在??臻g上開辟10個字節(jié)的連續(xù)空間 return 0; }
但是靜態(tài)開辟的空間明顯有兩個缺陷:
- 空間開辟??是固定的。
- 數(shù)組在申明的時候,必須指定數(shù)組的?度,數(shù)組空間?旦確定了??不能調(diào)整。
2. 動態(tài)內(nèi)存
為了解決靜態(tài)內(nèi)存開辟的內(nèi)存空間固定的問題,C語言引?了動態(tài)內(nèi)存開辟,讓程序員??可以申請和釋放空間,就?較靈活了。
2.1 動態(tài)內(nèi)存開辟函數(shù)
(1) malloc函數(shù)
頭文件#include <stdlib.h>
聲明:void* malloc (size_t size);
- size -- 內(nèi)存塊的大小,以字節(jié)為單位
- 如果參數(shù) size 為0,malloc的?為是標(biāo)準(zhǔn)是未定義的,取決于編譯器。
作用:向內(nèi)存申請?塊連續(xù)可?的空間,并返回指向這塊空間的指針
- 如果開辟成功,則返回?個指向開辟好空間的指針。
- 如果開辟失敗,則返回?個 NULL 指針,因此malloc的返回值?定要做檢查。
返回值:返回值的類型是 void* ,所以malloc函數(shù)并不知道開辟空間的類型,具體在使?的時候使?者??來決定。
補(bǔ)充打印錯誤信息函數(shù):perror()
頭文件:#include <stdio.h>
聲明:void perror(const char *str)
str -- 這是 C 字符串,包含了一個自定義消息,將顯示在原本的錯誤消息之前。
作用:把一個描述性錯誤消息輸出到標(biāo)準(zhǔn)錯誤 stderr。首先輸出字符串 str,后跟一個冒號,然后是一個空格。
返回值:無返回值。
下列是malloc與perror的具體使用方法:
int main() { int* arr = (int*)malloc(sizeof(int) * 10); //開辟十個大小為整型的空間 //返回類型強(qiáng)轉(zhuǎn)為int* if (arr == NULL)//如果開辟失敗 { perror("malloc fail: ");//打印錯誤信息 return 1;//直接返回 } int i = 0; for (i = 0; i < 10; i++)//存入數(shù)據(jù) { arr[i] = i; } for (i = 0; i < 10; i++)//打印數(shù)據(jù) { printf("%d ", arr[i]); } return 0; }
輸出結(jié)果:
監(jiān)視觀察:
動態(tài)內(nèi)存的數(shù)據(jù)存放在堆區(qū)
(2) calloc函數(shù)
頭文件:#include <stdlib.h>
聲明:void *calloc(size_t nitems, size_t size)
- nitems -- 要被分配的元素個數(shù)。
- size -- 元素的大小。
作用: 分配所需的內(nèi)存空間,并返回一個指向它的指針
返回值:該函數(shù)返回一個指針,指向已分配的內(nèi)存。如果請求失敗,則返回 NULL。
malloc 和 calloc 之間的不同點是,malloc 不會設(shè)置內(nèi)存為零,而 calloc 會設(shè)置分配的內(nèi)存為零。
下列是calloc的使用實例:
int main() { int* arr = (int*)calloc(10, sizeof(int)); //開辟十個大小為整型的空間 //返回類型強(qiáng)轉(zhuǎn)為int* if (arr == NULL)//如果開辟失敗 { perror("calloc fail: ");//打印錯誤信息 return 1;//直接返回 } return 0; }
calloc的初始化觀察:
(3) realloc函數(shù)
頭文件:#include <stdlib.h>
聲明:void *realloc(void *ptr, size_t size)
- ptr -- 指針指向一個要重新分配內(nèi)存的內(nèi)存塊,該內(nèi)存塊之前是通過調(diào)用 malloc、calloc 或 realloc 進(jìn)行分配內(nèi)存的。如果為空指針,則會分配一個新的內(nèi)存塊,且函數(shù)返回一個指向它的指針。
- size -- 內(nèi)存塊的新的大小,以字節(jié)為單位。如果大小為 0,且 ptr 指向一個已存在的內(nèi)存塊,則 ptr 所指向的內(nèi)存塊會被釋放,并返回一個空指針。
作用:嘗試重新調(diào)整之前調(diào)用 malloc 或 calloc 所分配的 ptr 所指向的內(nèi)存塊的大小。
返回值:該函數(shù)返回一個指針 ,指向重新分配大小的內(nèi)存。如果請求失敗,則返回 NULL。
- 有時會我們發(fā)現(xiàn)過去申請的空間太?了,有時候我們?會覺得申請的空間過?了,那為了合理的時候內(nèi)存,我們?定會對內(nèi)存的??做靈活的調(diào)整。那 realloc 函數(shù)就可以做到對動態(tài)開辟內(nèi)存??的調(diào)整。
- realloc擴(kuò)容機(jī)制:
- 本地擴(kuò)容:原有空間之后有?夠?的空間,直接在原有內(nèi)存之后直接追加空間,原來空間的數(shù)據(jù)不發(fā)?變化。
異地擴(kuò)容:原有空間之后沒有?夠?的空間,在堆空間上另找?個合適??的連續(xù)空間。將新增數(shù)據(jù)與原本數(shù)據(jù)拷貝過來,并自動釋放原來空間。
下列是realloc的具體使用方法:
int main() { int* arr = (int*)calloc(10, sizeof(int)); //開辟十個大小為整型的空間 //返回類型強(qiáng)轉(zhuǎn)為int* if (arr == NULL)//如果開辟失敗 { perror("calloc fail: ");//打印錯誤信息 return 1;//直接返回 } //繼續(xù)新增空間 int* tmp = (int*)realloc(arr, sizeof(int) * 15); //不用arr是為了防止開辟失敗,被至為NULL if (tmp == NULL)//如果開辟失敗 { perror("calloc fail: ");//打印錯誤信息 return 1;//直接返回 } arr = tmp; return 0; }
新增內(nèi)存較小時一般是在原有基礎(chǔ)上新增空間。兩者地址相同。
int* tmp = (int*)realloc(arr, sizeof(int) * 100);//新增內(nèi)存較大時
新增內(nèi)存較大時則會重新開辟一段空間,將原來的空間釋放。兩者地址不同。
2.2 動態(tài)內(nèi)存釋放函數(shù)
動態(tài)內(nèi)存開辟的空間并不像靜態(tài)開辟內(nèi)存的空間會隨著一段程序的結(jié)束而回收,這時就需要我們手動回收,否則就會造成內(nèi)存泄漏。
內(nèi)存泄漏(Memory Leak)是指程序中已動態(tài)分配的堆內(nèi)存由于某種原因程序未釋放或無法釋放,造成系統(tǒng)內(nèi)存的浪費,導(dǎo)致程序運行速度減慢甚至系統(tǒng)崩潰等嚴(yán)重后果。
頭文件:#include <stdlib.h>
聲明:void free(void *ptr)
ptr -- 指針指向一個要釋放內(nèi)存的內(nèi)存塊,該內(nèi)存塊之前是通過調(diào)用 malloc、calloc 或 realloc 進(jìn)行分配內(nèi)存的。如果傳遞的參數(shù)是一個空指針,則不會執(zhí)行任何動作。
作用:釋放之前調(diào)用 calloc、malloc 或 realloc 所分配的內(nèi)存空間。
返回值:該函數(shù)不返回任何值。
下面使用free函數(shù)的實例:
int main() { int* arr = (int*)calloc(10, sizeof(int)); //開辟十個大小為整型的空間 //返回類型強(qiáng)轉(zhuǎn)為int* if (arr == NULL)//如果開辟失敗 { perror("calloc fail: ");//打印錯誤信息 return 1;//直接返回 } //繼續(xù)新增空間 int* tmp = (int*)realloc(arr, sizeof(int) * 100); if (tmp == NULL)//如果開辟失敗 { perror("calloc fail: ");//打印錯誤信息 return 1;//直接返回 } arr = tmp; free(arr);//釋放arr所指向的內(nèi)存 arr = NULL; return 0; }
釋放完之后記得將arr置為NULL,否則arr指向一段已經(jīng)回收的空間會變成野指針。
2.3 常見內(nèi)存分布
?般我們在學(xué)習(xí)C/C++語?的時候,我們會關(guān)注內(nèi)存中的三個區(qū)域:棧區(qū)、 堆區(qū)、靜態(tài)區(qū)。
- 局部變量與函數(shù)參數(shù)是放在內(nèi)存的棧區(qū),
- 全局變量,static修飾的變量是放在內(nèi)存的靜態(tài)區(qū)。
- 堆區(qū)是?來動態(tài)內(nèi)存管理的。
具體分布如下圖:
3. 動態(tài)內(nèi)存的常見錯誤
動態(tài)內(nèi)存開辟就像指針一樣,一不小心就會釀成大錯,以下介紹了一些常見的內(nèi)存開辟錯誤:
3.1 對NULL指針的解引用
void test() { int* p = (int*)malloc(INT_MAX / 4); *p = 20; //如果p的值是NULL,就會有問題 free(p); }
- INT_MAX是一個宏定義,他表示整型的最大值,值為2147483647。
- 當(dāng)malloc申請的空間太大時存在失敗的情況,失敗返回NULL指針。
- 而系統(tǒng)無法訪問NULL指針指向的地址,這時編譯器會報一個警告:
改正方法:
void test() { int* p = (int*)malloc(INT_MAX / 4); if (NULL == p) { perror("malloc fail: ");//打印錯誤信息 return 1; } *p = 20; free(p); p = NULL; }
這時就體現(xiàn)判斷是否為空指針的重要性了
3.2 對動態(tài)開辟空間的越界訪問
void test() { int i = 0; int* p = (int*)malloc(10 * sizeof(int)); if (NULL == p) { perror("malloc fail: ");//打印錯誤信息 return 1;//直接返回 } for (i = 0; i <= 10; i++) { *(p + i) = i; //當(dāng)i是10的時候越界訪問 } free(p); p=NULL; }
- malloc只申請了十個整型大小的空間。
- for循環(huán)循環(huán)了十一次,越界訪問,錯誤信息如下:
改正方法:
void test() { int i = 0; int* p = (int*)malloc(10 * sizeof(int)); if (NULL == p) { perror("malloc fail: ");//打印錯誤信息 return 1;//直接返回 } for (i = 0; i < 10; i++) { *(p + i) = i; //當(dāng)i是10的時候越界訪問 } free(p); p = NULL; }
3.3 對非動態(tài)開辟內(nèi)存使用free釋放
void test() { int a = 10; int* p = &a; free(p); p=NULL;//ok? }
- free()只能釋放有動態(tài)內(nèi)存開辟在堆上的空間。
- p指向的空間是靜態(tài)內(nèi)存開辟的,無法釋放,釋放就會出錯:
改正方法:
void test() { int a = 10; int* p = &a; }
靜態(tài)內(nèi)存開辟的空間并不需要釋放。
3.4 使?free釋放?塊動態(tài)開辟內(nèi)存的?部分
void test() { int* p = (int*)malloc(100); p++; free(p); //p不再指向動態(tài)內(nèi)存的起始位置 p = NULL; }
- p++跳過一個整型大小的空間。
- free()釋放p只會釋放當(dāng)前位置開始之后的空間,有一個整型大小的空間未被釋放,造成內(nèi)存泄漏。
改正方法:
void test() { int* p = (int*)malloc(100); free(p); p = NULL; }
不能隨意改變p指向的位置,開辟多少內(nèi)存就釋放多少內(nèi)存。
3.5 對同?塊動態(tài)內(nèi)存多次釋放
void test() { int* p = (int*)malloc(100); free(p); free(p); //重復(fù)釋放 }
- p已經(jīng)被釋放歸還給操作系統(tǒng),但是此時p還指向該內(nèi)存,是一個野指針。
- 再次釋放p就會出現(xiàn)內(nèi)存出錯問題。
改正方法:
void test() { int* p = (int*)malloc(100); free(p); p = NULL; }
釋放內(nèi)存之后記得將其置為空指針,這樣再次free空指針就不會進(jìn)行任何操作。
3.6 動態(tài)開辟內(nèi)存忘記釋放(內(nèi)存泄漏)
void test() { int* p = (int*)malloc(100); if (NULL != p) { *p = 20; }//內(nèi)存泄漏 } int main() { test(); }
當(dāng)我們動態(tài)內(nèi)存申請空間之后必須手動將其釋放,不會就會出現(xiàn)內(nèi)存泄漏的問題。
改正方法:
void test() { int* p = (int*)malloc(100); if (NULL != p) { *p = 20; } free(p); p = NULL; }
每次使用完動態(tài)內(nèi)存開辟空間之后記得釋放內(nèi)存。
4. 相關(guān)筆試題
4.1 題目一
void GetMemory(char* p) { p = (char*)malloc(100); } void Test(void) { char* str = NULL; GetMemory(str); strcpy(str, "hello world"); printf(str); }//請問運?Test函數(shù)會有什么樣的結(jié)果?
這段程序有兩個經(jīng)典錯誤:
內(nèi)存非法訪問:我們知道傳值調(diào)用時,形參只是實參的臨時拷貝,對形參的改變無法影響實參,這時str仍是空指針,而strcpy拷貝會對空指針進(jìn)行解引用操作,對NULL指針解引用會出錯!
內(nèi)存泄漏:在GetMemory()函數(shù)內(nèi)部動態(tài)申請了100字節(jié)的空間,因為p隨著函數(shù)結(jié)束而被銷毀,所以已經(jīng)再也找不到該空間,會造成內(nèi)存泄漏。
改正方法:
- 我們要想改變str就需要傳址調(diào)用,而str本身就是個指針變量,傳指針變量的地址需要二級指針來接收。
- 使用完之后必須釋放內(nèi)存。
void GetMemory(char** p) { *p = (char*)malloc(100); } void Test(void) { char* str = NULL; GetMemory(&str); strcpy(str, "hello world"); printf(str); // 釋放 free(str); str = NULL; }
4.2 題目二
char* GetMemory(void) { char p[] = "hello world"; return p; } void Test(void) { char* str = NULL; str = GetMemory(); printf(str); }
這段程序是經(jīng)典的野指針問題,局部變量出了作用就會銷毀歸還給操作系統(tǒng),而str還能指向這塊空間就會形成野指針。
改正方法:
因為只有存放在棧區(qū)的值才會被銷毀,所以我們將其放在其他區(qū)域如:靜態(tài)區(qū),而放在靜態(tài)區(qū)有兩種方法:static修飾與常量字符串。
const char* GetMemory1(void) { const char* p = "hello world"; return p; } char* GetMemory2(void) { static char p[] = "hello world"; return p; } void Test(void) { char* str = NULL; str = GetMemory1(); printf(str); printf("\n"); str = GetMemory2(); printf(str); } int main() { Test(); return 0; }
輸出結(jié)果:
4.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); } //請問運?Test函數(shù)會有什么樣的結(jié)果?
這又是一個經(jīng)典的內(nèi)存泄漏問題——p開辟出內(nèi)存未被釋放。
改正方法:
void Test(void) { char* str = NULL; GetMemory(&str, 100); strcpy(str, "hello"); printf(str); free(str); str = NULL; }
4.4 題目四
void Test(void) { char* str = (char*)malloc(100); strcpy(str, "hello"); free(str); if (str != NULL) { strcpy(str, "world"); printf(str); } } //請問運?Test函數(shù)會有什么樣的結(jié)果?
這也是個經(jīng)典野指針問題,str所開辟的空間已經(jīng)歸還給了操作系統(tǒng),這時再將world拷貝進(jìn)str就會出錯。
改正方法:
歸還內(nèi)存之后隨手將其值為NULL指針,后續(xù)語句就不會進(jìn)行。
void Test(void) { char* str = (char*)malloc(100); strcpy(str, "hello"); free(str); str = NULL; if (str != NULL) { strcpy(str, "world"); printf(str); } }
5. 柔性數(shù)組
5.1 柔性數(shù)組是什么
C99中,結(jié)構(gòu)體中的最后一個元素允許是未知大小的數(shù)組,這就叫作柔性數(shù)組,例如:
typedef struct st_type { int i; int a[0]; //柔性數(shù)組成員 }type_a;
有些編譯器會報錯?法編譯可以改成:
typedef struct st_type { int i; int a[]; //柔性數(shù)組成員 }type_a;
- 結(jié)構(gòu)中的柔性數(shù)組成員前?必須?少?個其他成員。
- 包含柔性數(shù)組成員的結(jié)構(gòu)?malloc()函數(shù)進(jìn)?內(nèi)存的動態(tài)分配,并且分配的內(nèi)存應(yīng)該?于結(jié)構(gòu)的??,以適應(yīng)柔性數(shù)組的預(yù)期??。
5.2 柔性數(shù)組的大小
依靠我們結(jié)構(gòu)體學(xué)過得內(nèi)存對齊的原則,我們可以計算結(jié)構(gòu)體的大小。
typedef struct st_type { int i; int a[0]; //柔性數(shù)組成員 }type_a; int main() { printf("%d\n", sizeof(type_a)); return 0; }
輸出結(jié)果:
從上述可知柔性數(shù)組成員是不計入結(jié)構(gòu)體大小的。
5.3 柔性數(shù)組的使用
柔性數(shù)組的使用與結(jié)構(gòu)體使用十分類似,具體使用如下:
#include <stdio.h> #include <stdlib.h> typedef struct st_type { int i; int a[]; //柔性數(shù)組成員 }type_a; int main() { int i = 0; type_a* p = (type_a*)malloc(sizeof(type_a) + 100 * sizeof(int)); //包含柔性數(shù)組成員的結(jié)構(gòu)?**malloc()函數(shù)**進(jìn)?內(nèi)存的動態(tài)分配, // 并且分配的內(nèi)存應(yīng)該?于結(jié)構(gòu)的??,以適應(yīng)柔性數(shù)組的預(yù)期??。 p->i = 100; for (i = 0; i < 100; i++)//存放數(shù)據(jù) { p->a[i] = i; } free(p); return 0; }
5.4 模擬實現(xiàn)柔性數(shù)組
先開辟一個結(jié)構(gòu)體大小,在開辟一個數(shù)組的大小。
柔性數(shù)組成員的空間都是malloc開辟的,所以模擬的柔性數(shù)組也需要malloc開辟。
具體實施如下:
#include <stdio.h> #include <stdlib.h> typedef struct st_type { int i; int* p_a; }type_a; int main() { //先開辟一個結(jié)構(gòu)體大小 type_a* p = (type_a*)malloc(sizeof(type_a)); p->i = 100; //在開辟一個數(shù)組大小 p->p_a = (int*)malloc(p->i * sizeof(int)); for (int i = 0; i < 100; i++) { p->p_a[i] = i; } //釋放空間 free(p->p_a); p->p_a = NULL; free(p); p = NULL; return 0; }
5.5 柔性數(shù)組的優(yōu)勢
通過與模擬的柔性數(shù)組對比,我們可以看出柔性數(shù)組的優(yōu)勢:
便內(nèi)存釋放: 如果我們的代碼是在一個給別人用的函數(shù)中,你在里面做了二次內(nèi)存分配,并把整個結(jié)構(gòu)體返回給用戶。用戶調(diào)用free可以釋放結(jié)構(gòu)體,但是用戶并不知道這個結(jié)構(gòu)體內(nèi)的成員也需要free,容易造成內(nèi)存泄漏。所以,如果我們把結(jié)構(gòu)體的內(nèi)存以及其成員要的內(nèi)存一次性分配好了,并返回給用戶一個結(jié)構(gòu)體指針,用戶做一次free就可以把所有的內(nèi)存也給釋放掉
這樣有利于訪問速度: 連續(xù)的內(nèi)存有益于提?訪問速度,也有益于減少內(nèi)存碎?。
以上就是一文解析C語言中動態(tài)內(nèi)存管理的詳細(xì)內(nèi)容,更多關(guān)于C語言動態(tài)內(nèi)存管理的資料請關(guān)注腳本之家其它相關(guān)文章!
- 深入了解C語言的動態(tài)內(nèi)存管理
- 詳解C語言中動態(tài)內(nèi)存管理及柔性數(shù)組的使用
- C語言動態(tài)內(nèi)存的分配最全面分析
- 一文帶你搞懂C語言動態(tài)內(nèi)存管理
- 詳解C語言中的動態(tài)內(nèi)存管理
- C語言動態(tài)內(nèi)存分配圖文講解
- 使用c語言輕松實現(xiàn)動態(tài)內(nèi)存管
- 一文帶你了解C語言中的動態(tài)內(nèi)存管理函數(shù)
- C語言動態(tài)內(nèi)存管理的原理及實現(xiàn)方法
- 詳解C語言中動態(tài)內(nèi)存管理
- C語言中常見的六種動態(tài)內(nèi)存錯誤總結(jié)
- C語言動態(tài)內(nèi)存管理的實現(xiàn)示例
相關(guān)文章
C++?std::chrono庫使用示例(實現(xiàn)C++?獲取日期,時間戳,計時等功能)
std::chrono是C++標(biāo)準(zhǔn)庫中的一個組件,用于表示和處理時間,這篇文章主要介紹了C++?std::chrono庫使用指南(實現(xiàn)C++?獲取日期,時間戳,計時等功能),需要的朋友可以參考下2023-06-06C語言實現(xiàn)學(xué)生信息管理系統(tǒng)(多文件)
這篇文章主要為大家詳細(xì)介紹了C語言實現(xiàn)學(xué)生信息管理系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2020-12-12