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