C語言動態記憶體分配篇
目錄
一、為什么存在動態記憶體管理/分配?
記憶體的存盤形式劃分
二、動態記憶體函式的介紹
malloc
free
malloc和free的實際應用
calloc
realloc
三、常見的動態記憶體錯誤
對NULL指標的解參考操作
對動態開辟的空間越界訪問
對非動態開辟記憶體使用 free 釋放
使用 free 釋放一塊動態開辟記憶體的一部分
對同一塊動態記憶體的多次釋放
動態開辟記憶體忘記釋放(導致記憶體泄露)
四、C/C++程式的記憶體開辟
五、柔性陣列
柔性陣列的特點
柔性陣列的使用
柔性陣列的優勢
一、為什么存在動態記憶體管理/分配?
(1)因為記憶體太寶貴,
(2)如果全部是靜止記憶體不能釋放,對于小的程式可以運行完畢,但是對于大的程式,還沒運行完,記憶體就要被占用完,此時就要發生記憶體泄露,
(3)假設給定一個占用記憶體可變大小的變數(假設是陣列的長度len),那么給該變數通過函式動態分配記憶體后,分配記憶體的大小是根據陣列的長度len決定的,假定用戶輸入len的大小是5,系統就會動態的給該陣列分配長度為5的記憶體, 該段代碼運行結束后,系統呼叫free()函式釋放分配的記憶體,然后接著運行剩下的程式,
換句話說,動態分配記憶體可以根據需要去申請記憶體,用完后就還回去,讓需要的程式用,
我們先看個例子:
int a = 20; //區域變數 在堆疊區上開辟四個位元組
char ch[10] = {0}; //區域變數 在堆疊空間上開辟10個位元組的連續空間
int g_a = 10; //全域變數 在靜態區上開辟十個位元組
上述的開辟空間的方式有兩個特點:
- 空間開辟大小是固定的,
- 陣列在申明的時候,必須指定陣列的長度,它所需要的記憶體在編譯時分配,
但是對于空間的需求,不僅僅是上述的情況,有時候我們需要的空間大小在程式運行的時候才能知道,那陣列的編譯時開辟空間的方式就不能滿足了, 這時候就只能試試動態存開辟了,
記憶體的存盤形式劃分

二、動態記憶體函式的介紹
malloc
- 專門用來動態記憶體開辟的函式
//函式原型
void *malloc (size_t size);
//void* 表示任意型別的指標
//size_t 表示的是unsigned int(無符號整型)
//size 表示所要開辟的空間單位是位元組
這個函式向記憶體申請一塊連續可用的空間,并回傳指向這塊空間的指標,
- 如果開辟成功,則回傳一個指向開辟好空間的指標,
- 如果開辟失敗,則回傳一個NULL指標,因此 malloc 的回傳值一定要做檢查,
- 回傳值的型別是 void* ,所以 malloc 函式并不知道開辟空間的型別,具體在使用的時候使用者自己來決定,
- 如果引數 size 為 0 ,malloc 的行為是標準是未定義的,取決于編譯器,
free
- 專門用來做動態記憶體的釋放和回收的函式
//函式原型
void free(void *ptr);
//void *prt 表示所要釋放的指標型別
free函式用來釋放動態開辟的記憶體,
- 如果引數 ptr 指向的空間不是動態開辟的,那 free 函式的行為是未定義的,
- 如果引數 ptr 是NULL指標,則函式什么事都不做,
malloc和free的實際應用
代碼如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main()
{
//1.通過動態開辟申請10個int型別的空間
//根據實際使用強制型別轉換為想要的型別
int *p = (int*)malloc(10 * sizeof(int));
//2.malloc有可能申請空間失敗,所以需要判斷一下
if (p == NULL)//判斷p指標是否為空
{
printf("%s\n", strerror(errno));
}
else
{
//正常使用空間
int i = 0;
for (i = 0; i < 10; i++)
{
*(p + i) = i;
}
for (i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}
}
//當動態申請的空間不再使用的時候,就應該還給作業系統
free(p);//釋放p所指向的動態記憶體
p = NULL;//是否有必要
return 0;
}
執行結果:

思考:
p = NULL; 是否有必要加上?
解答:
由于 free 完后本身是不會置為空指標的,因此我們需要手動將其變為空指標,所以p = NULL是有必要的,
calloc
- 能夠讓動態分配在申請空間的同時就進行初始化的函式
//函式原型
void *calloc(size_t num, size_t size);
//
- 函式的功能是為 num 個大小為 size 的元素開辟一塊空間,并且把空間的每個位元組初始化為 0 ,
- 與函式 malloc 的區別只在于 calloc 會在回傳地址之前把申請的空間的每個位元組初始化為全 0 ,
例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main()
{
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));
}
}
//free函式用來釋放動態開辟的空間
free(p);
p = NULL;
return 0;
}
執行結果:

總結:所以如何我們對申請的記憶體空間的內容要求初始化,那么可以很方便的使用 calloc 函式來完成任務,而 calloc 函式會將所申請到的記憶體空間全部初始化成 0 ,意味著 calloc 比 malloc 運行時間更長,所以在選擇這兩個函式時可以根據是否需要初始化來選擇,
realloc
- 能夠將動態記憶體靈活分配的函式
- 有時會我們發現過去申請的空間太小了,有時候我們又會覺得申請的空間過大了,那為了合理的時候記憶體, 我們一定會對記憶體的大小做靈活的調整,那 realloc 函式就可以做到對動態開辟記憶體大小的調整,
//函式原型
void *realloc(void *ptr, size_t size);
//void *ptr 表示被調整的指標指向的地址
//size_t size 表示改變之后的空間記憶體大小,單位是位元組
- ptr 是要調整的記憶體地址
- size 調整之后新大小回傳值為調整之后的記憶體起始位置
- 這個函式調整原記憶體空間大小的基礎上,還會將原來記憶體中的資料移動到新的空間
- realloc在調整記憶體空間的是存在兩種情況:
- 情況1: 原有空間之后有足夠大的空間
- 情況2: 原有空間之后沒有足夠大的空間
圖解:
例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main()
{
int *p =(int*)malloc(20);
if(p == NULL)
{
printf("%s\n", strerror(errno));
}
else
{
int i = 0;
for(i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}
}
//上方僅僅只是在使用malloc開辟的20個位元組空間
//假設這里,20個位元組空間不能滿足我們的需求了
//希望能夠有40個位元組的空間
//這里就可以使用realloc來調整動態開辟的記憶體
int *ptr = realloc(p, INT_MAX);
if(ptr != NULL)
{
int i = 0;
for(i = 5; i < 10; i++)
{
*(p+i) = i;
}
for(i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}
}
//釋放動態開辟的記憶體空間
free(p);
p = NULL;
return 0;
}
realloc 函式的注意事項:
1.如果 p 指向的空間有足夠的的記憶體空間可以追加,則直接追加,后回傳 p
2.如果 p 指向的空間之后沒有足夠的記憶體空間可以追加,則 realloc 函式會重新找一個新的記憶體區域,開辟一塊滿足需求的空間,并且把原來記憶體中的資料拷貝回來,釋放舊的記憶體空間,最后回傳新開辟的記憶體空間地址,而舊的那塊記憶體空間需要賦空指標,不然會形成野指標,造成非法訪問,
3.得用一個新的變數去接收 realloc 函式的回傳值
注:以上四種函式頭檔案均使用 stdlib.h 頭檔案!
三、常見的動態記憶體錯誤
-
對NULL指標的解參考操作
#include <stdio.h>
#include <stdlib.h>
int main()
{
int *p = (int*)malloc(40);
//萬一malloc失敗了,p就會被賦值為NULL
//*p = 0;//error
int i = 0;
for(i = 0; i < 10; i++)
{
*(p+i) = i;//非法訪問
}
free(p);
p = NULL;
return 0;
}
-
對動態開辟的空間越界訪問
#include <stdio.h>
#include <stdlib.h>
int main()
{
int *p = (int*)malloc(5 * sizeof(int));//只有5個元素
if( p == NULL)
{
return 0;
}
else
{
int i = 0;
for(i = 0; i < 10; i++)//只有5個元素,回圈10次,會造成越界訪問
{
*(p+i) = i;
}
}
free(p);
p = NULL;
return 0;
}
-
對非動態開辟記憶體使用 free 釋放
#include <stdio.h>
#include <stdlib.h>
int main()
{
int a = 10;
int *p = &a;
*p = 20;
free(p);
p = NULL;
return 0;
}
-
使用 free 釋放一塊動態開辟記憶體的一部分
#include <stdio.h>
#include <stdlib.h>
int main()
{
int *p = (int*)malloc(40);
if(p = NULL)
{
return 0;
}
int i = 0;
for(i = 0; i < 10; i++)
{
*p++ = i;
}
//此時p指向的不是動態開辟出的起始位置了
//回收空間,free只能釋放動態開辟出的起始位置
free(p);
p = NULL;
return 0;
}
-
對同一塊動態記憶體的多次釋放
#include <stdio.h>
#include <stdlib.h>
int main()
{
int *p = (int*)malloc(40);
if(p == NULL)
{
return 0;
}
//使用
free(p);
//p = NULL 需要定義為空指標才能參考下面的free
free(p);//重復釋放
return 0;
}
-
動態開辟記憶體忘記釋放(導致記憶體泄露)
#include <stdio.h>
#include <stdlib.h>
int main()
{
while(1)
{
malloc(1);//開辟完空間后一直沒有釋放
}
return 0;
}
注:忘記釋放不再使用的動態開辟的空間會造成記憶體泄漏,動態開辟的空間一定要釋放,并且正確釋放
四、C/C++程式的記憶體開辟

C/C++程式記憶體分配的幾個區域:
- 堆疊區(stack):在執行函式時,函式內區域變數的存盤單元都可以在堆疊上創建,函式執行結束時這些存盤單元自動被釋放,堆疊記憶體分配運算內置于處理器的指令集中,效率很高,但是分配的記憶體容量有限, 堆疊區主要存放運行函式而分配的區域變數、函式引數、回傳資料、回傳地址等,
- 堆區(heap):一般由程式員分配釋放, 若程式員不釋放,程式結束時可能由OS回收 ,分配方式類似于鏈表,
- 資料段(靜態區)(static):存放全域變數、靜態資料,程式結束后由系統釋放,
- 代碼段:存放函式體(類成員函式和全域函式)的二進制代碼,
五、柔性陣列
也許你從來沒有聽說過 柔性陣列(flexible array)這個概念,但是它確實是存在的, C99 中,結構中的最后一個元素允許是未知大小的陣列,這就叫做 『柔性陣列』成員,
例:
typedef struct S
{
int n;
int arr[0];//未知大小的-柔性陣列成員-陣列的大小是可以調整的
//int arr[] 同上
}S;
柔性陣列的特點
- 結構中的柔性陣列成員前面必須至少一個其他成員,
- sizeof 回傳的這種結構大小不包括柔性陣列的記憶體,
- 包含柔性陣列成員的結構用 malloc () 函式進行記憶體的動態分配,并且分配的記憶體應該大于結構的大小,以適應柔性陣列的預期大小,
例:
#include <stdio.h>
typedef struct S
{
int n;
int arr[0];//未知大小的-柔性陣列成員-陣列的大小是可以調整的
}S;
int main()
{
struct S s;
printf("%d\n", sizeof(s));
return 0;
}
執行結果:

柔性陣列的使用
例:
#include <stdio.h>
#include <stdlib.h>
typedef struct S
{
int n;
int arr[0];//未知大小的-柔性陣列成員-陣列的大小是可以調整的
}S;
int main()
{
struct S *ps = (struct S*)malloc(sizeof(struct S)+5*sizeof(int));
ps->n = 100;
int i = 0;
for(i = 0; i <5; i++)
{
ps->arr[i] = i;//0 1 2 3 4
}
struct S *ptr = realloc(ps, 44);
if(ptr != NULL)
{
ps = ptr;
}
for(i = 5; i < 10; i++)
{
ps->arr[i] = i;
}
for(i = 0; i < 10; i++)
{
printf("%d ", ps->arr[i]);
}
free(ps);
ps = NULL;
return 0;
}
執行結果 :

圖解:

柔性陣列的優勢
我們來看一下這段代碼比起上一段代碼的優勢
優勢一:方面記憶體釋放
- 如果我們的代碼是在一個給別人用的函式中,你在里面做了二次記憶體分配,并把整個結構體回傳給用戶,
- 用戶呼叫 free 可以釋放結構體,但是用戶并不知道這個結構體內的成員也需要 free ,所以你不能指望用戶來發現這個事,
- 所以,如果我們把結構體的記憶體以及其成員要的記憶體一次性分配好了,并回傳給用戶一個結構體指標,用戶做一次 free 就可以把所有的記憶體也給釋放掉,
優勢二 : 這樣有利于訪問速度
- 連續的記憶體有益于提高訪問速度,也有益于減少記憶體碎片,(其實,我個人覺得也沒多高了,反正你跑不了要用做偏移量的加法來尋址)
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/298365.html
標籤:其他
上一篇:[C語言]結構體
