目錄
一、記憶體劃分
二、四大函式
①malloc
②free
③calloc
④realloc
三、易錯分析
問題一:
問題二:
問題三:
問題四:
問題五:
問題六:
四、經典面試題
面試題一:
面試題二:
面試題三:
五、柔性陣列
1.前言
2.特點
3.優勢
一、記憶體劃分
要理解動態記憶體管理,首先要了解C程式對記憶體劃分的主要形式:
堆疊區 ①在執行函式時,函式內區域變數的存盤單元都可以在堆疊上創建,函式執行結束時這些存盤單元自動被釋放,
②堆疊記憶體分配運算內置于處理器的指令集中,效率很高,但是分配的記憶體容量有限,(堆疊溢位問題)
③ 堆疊區主要存放運行函式而分配的區域變數、函式引數、回傳資料、回傳地址等,
④向下增長指從堆疊依次申請的地址在減小
堆區 ①一般由程式員釋放, 若程式員不釋放,程式結束時可能由OS(operate system)回收 ,分配方式類似于鏈表,
②動態記憶體開辟在堆區上,
③向上增長指從堆依次申請的地址在增加
資料段(靜態區) ①存放全域變數、靜態資料,
②程式結束后由系統釋放,
代碼段 ①存放函式體(類成員函式和全域函式)的二進制代碼,
②其中的資料只可被讀取,不可被修改
(上圖來源位元科技
二、四大函式
①malloc

作用:在堆區申請一塊size_t大小(單位位元組)的空間,成功回傳動態開辟空間的地址,失敗回傳空指標NULL ,(比如開辟的空間太大了就會失敗)
注意點:1.size大小為0的情況是未定義的,其結果取決于編譯器的處理
2.由于malloc回傳值為void*型別,在用指標接收時最好先強制型別轉換
3.小心記憶體開辟失敗回傳空指標
#include<stdlib.h>
int main()
{
int*p = (int *)malloc(40);
return 0;
}
看!我們很輕松的動態開辟了一塊指定大小的空間,但不知道你是不是有這樣的疑惑:一直開辟空間那電腦的記憶體不會越來越小嗎? 其實當我們程式結束時,動態開辟的空間會被自動回收,當然我們也可以選擇主動出擊——使用free函式,
②free

作用:釋放動態開辟的空間(memblock為指向動態開辟空間的指標)
注意點:1.free(NULL),函式不執行任何操作
2.不能用free函式釋放非動態開辟的空間
3.free只是釋放空間,并沒有清除指標,也就是說你和女朋友分手了,但你仍然牢 牢記著人家的電話號碼(空間地址),若此時對指標解參考,就犯了非法訪問記憶體 的錯誤,所以要及時將指標賦值為空,對人家徹底死心,
#include<stdlib.h>
#include<assert.h>
int main()
{
int i = 0;
int*p = (int *)malloc(10*sizeof(int));
assert(p);//監測是否開辟成功
for (i = 0; i < 10; i++)
{
p[i] = i;//或者為*(p+i),但不可以是p++,
}
for (i = 0; i < 10; i++)
{
printf("%d ",p[i]);
}
free(p);
p = NULL;//最好置為NULL
return 0;
}
malloc和free的組合拳,完美的完成了動態記憶體開辟的程序,但其實動態開辟記憶體不只是malloc的特權,calloc也具有,只不過二者的作用有所不同
③calloc

作用:向堆區申請一塊空間,存放num個size大小的元素,成功回傳指向開辟空間的指標,失敗則回傳NULL,
區別:calloc開辟空間后會將每個元素初始化為0,所以malloc開辟空間效率更高,calloc會自動賦值為0,各有所長,
calloc開辟——初始化為0
malloc開辟——未初始化

認識了malloc,calloc這類動態開辟空間的函式,聰明的你或許會想到如果開辟的空間不夠怎么辦,別擔心,C語言給出了realloc函式調整動態開辟空間的大小,
④realloc

作用:將動態開辟的記憶體大小調整為size(單位位元組)
注意要深刻理解realloc調整大小的兩種情況:
情況一:

若原有空間充足,則就直接在原有記憶體之后直接追加空間,原來空間的資料不發生變化,
情況二:

原有空間之后沒有足夠多的空間時,擴展的方法是:在堆空間上另找一個合適大小的連續空間來使用,復制原來的內容再追加,這樣函式回傳的是一個新的記憶體地址,
malloc創建的p所指向的空間

realloc后p1指向的空間

realloc之后p指向的空間
我們根據上面的分析可以得出以下結論(空間不足時):
1.回傳一個新的地址指向新開辟的空間
2.原空間的內容會被復制到新開辟的空間
3.原空間的內容被釋放
當傳入的指標為空指標時,此時realloc的功能相當于malloc的,
三、易錯分析
試著分析一下以下代碼有什么缺陷或者不足
問題一:
#include<stdlib.h>
#include<assert.h>
//缺陷版
int main()
{
int i = 0;
int*p = (int *)malloc(5*sizeof(int));
for (i = 0; i < 5; i++)
{
p[i] = i;
}
p = realloc(p, 1000000);//ok?
free(p);
p = NULL;
}
//完善版
int main()
{
int i = 0;
int*p = (int *)malloc(5*sizeof(int));
assert(p);
for (i = 0; i < 5; i++)
{
p[i] = i;
}
int *p1 = (int *)realloc(p, 1000);
if(p1!=NULL)
{
p=p1;//為了程式的連貫性,最好都賦值給最開始使用的p
}
free(p);
p = NULL;
}
1.無論是malloc或者是realloc都要小心記憶體開辟失敗回傳NULL的情況,所以檢驗必不可少
2.“p = realloc(p, 1000000);”的寫法如果失敗那原來的地址也找不到了,可謂是賠了夫人又折兵,
問題二:
void test()
{
int i = 0;
int *p = (int *)malloc(10 * sizeof(int));
if (NULL == p)
{
exit(EXIT_FAILURE);
}
for (i = 0; i <= 10; i++)//ok?
{
*(p + i) = i;
}
free(p);
}
1.注意越界訪問的問題,"i<10"才不會讓程式崩潰
問題三:
void test()
{
int a = 10;
int *p = &a;
free(p);//ok?
}
1.不能用free釋放不是動態開辟的記憶體
問題四:
void test()
{
int *p = (int *)malloc(100);
p++;//ok?
free(p);
}
1.p的地址不能改變,否則在free時會產生釋放部分空間錯誤
問題五:
void test()
{
int *p = (int *)malloc(100);
free(p);
free(p);
}
1.很顯然該程式犯了重復釋放的錯誤
2.在這段簡短的代碼中可以輕而易舉的發現問題,但如果在很長的代碼中我們如何避免?
①做到誰開辟誰釋放空間
②養成將指標置為NULL的習慣,這樣即使被free也是free(NULL),如前文所說無事發生
問題六:
void test()
{
int *p = (int *)malloc(100);
if (NULL != p)
{
*p = 20;
}
}
int main()
{
test();
while (1);
}
1.執行上述程式時,打開任務管理器可以發現CPU被占用的空間一直增大(電腦有保護機制,到一定程度就停止增長),一直開辟空間但是不釋放使我們空余的記憶體越來越少,這也是就我們所說的記憶體泄漏問題
2.所以動態開辟的記憶體一定要釋放并且正確釋放,
四、經典面試題
面試題一:
void GetMemory(char *p)
{
p = (char *)malloc(100);
}
int main()
{
char *str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
return 0;
}
請問運行的結果是什么? 程式崩潰
分析:初學者往往很容易進入這個誤區,認為str成功指向了開辟的空間,可仔細一想,str真的如我們所想嗎?在這里我們混淆了傳址操作和傳參操作,圖中的操作屬于傳參操作,p不過是str的一份臨時拷貝,改變p對str沒有一點影響,當然上面的代碼也存在沒有檢查回傳值,沒有主動釋放記憶體的錯誤,
我們如何糾正呢,在接下來的面試題里做詳細解答,
面試題二:
char *GetMemory(void)
{
char p[] = "hello world";
return p;
}
int main()
{
char *str = NULL;
str = (char*)GetMemory();
printf(str);
return 0;
}
請問上述程式的結果是什么? 列印隨機值,
分析:如果你認為GetMemory函式回傳的是p的地址,沒錯,你想的很對,但此時的p所指向的內容不再是“"hello world"”,為什么呢?p[ ]陣列在函式中創建,正如文章開頭提到的記憶體布局,函式申請的區域變數開辟在堆疊區上,而開辟在堆疊區上變數的特點是當這個函式結束,變數也隨即銷毀,所以p只是記住當時的地址,里面的內容不再是"hello world",如果此時對str進行解參考操作,則會出現非法訪問記憶體的錯誤,
面試題三:
int main(void)
{
char *str = (char *)malloc(100);
strcpy(str, "hello");
free(str);
if (str != NULL)
{
strcpy(str, "world");
printf(str);
}
return 0;
}
請問上述程式的結果是什么? 列印“world”
分析:不知道有沒有小伙伴們認為沒有列印結果的,如果有,我猜想應該是對free的作用產生誤解,free只是對堆區的記憶體進行釋放,并不會將str指向的地址賦值為空指標,
如何修改面試一的程式:
分析完上面三個面試題后相信你對動態記憶體開辟有了更深的理解,我們再回到第一個問題,如何將他修改正確呢?其實面試題一中代碼之所以失敗的原因在于沒有將p與str建立起練習,以此為突破口,我們可以有以下兩個思路:1.傳址操作 2.回傳p的地址,
//采用傳址操作
void GetMemory(char **p)
{
*p = (char *)malloc(100);
}
int main()
{
char *str = NULL;
GetMemory(&str);
if (str != NULL)
{
strcpy(str, "hello world");
printf(str);
}
free(str);
str = NULL;
return 0;
}
//回傳p的地址
char* GetMemory(char *p)
{
p = (char *)malloc(100);
return p;
}
int main()
{
char *str = NULL;
str=(char *)GetMemory(str);
if (str != NULL)
{
strcpy(str, "hello world");
printf(str);
}
free(str);
str = NULL;
return 0;
}
五、柔性陣列
1.前言
我們要向實作陣列長度的動態變化,以下代碼可以嗎?
const int n = 0;
int main()
{
int arr[n] = { 0 };
return 0;
}
顯然不可以,[ ]里必須是常量,即使加const修飾,也還是變數,只不過這個變數不可以被修改罷了,那我們是否可以使用一個指向動態開辟的空間的指標呢?這樣的方法確實是可以的,但在這里會介紹一種更優的方法——柔性陣列(C99中新增),我們會在最后比較兩者的優劣,
2.特點
1.結構中的柔性陣列成員必須在最后一個,
2.sizeof(struct s) 回傳的這種結構大小不包括柔性陣列的大小,
3.包含柔性陣列成員的結構用malloc ()函式進行記憶體的動態分配,并且分配的記憶體應該大于結構的大小,以適應柔性陣列的預期大小,
使用示范:
struct s
{
int n;
int arr[0];
};
int main()
{
int i = 0;
struct s*p = (struct s*)malloc(sizeof(struct s)+40);//注意struct的大小不包括動態開辟的陣列
if (p != NULL)
{
for (i = 0; i < 10; i++)
{
p->arr[i] = i;
}
}
free(p);
p = NULL;
return 0;
}
3.優勢
我們先試著用指標實作上面的功能
#include<stdlib.h>
struct s
{
int n;
int *arr;
};
int main()
{
struct s*ps =(struct s*) malloc(sizeof(struct s));
ps->n=10;
if (ps != NULL)
{
ps->arr = (struct s*)malloc(ps->n*sizeof(int));//用arr接收開辟空間的地址
if (ps->arr != NULL)
{
int i = 0;
for (i = 0; i < 10; i++)
{
ps->arr[i] = i;
}
}
}
free(ps->arr);
ps->arr = NULL;
free(ps);
ps = NULL;
return 0;
}
相比于用指標動態開辟陣列空間,柔性陣列有以下三個優勢:
1.釋放空間更加方便
如果我們的代碼是在一個給別人用的函式中,你在里面做了二次記憶體分配,并把整個結構體回傳給用戶,用戶呼叫free可以釋放結構體,但是用戶并不知道這個結構體內的成員也需要free,所以你不能指望用戶來發現這個事,所以,如果我們把結構體的記憶體以及其成員要的記憶體一次性分配好了,并回傳給用戶一個結構體指標,用戶做一次free就可以把所有的記憶體也給釋放掉,
2.減少記憶體碎片
使用柔性陣列創建的n與arr是一起創建的,在空間上連續,而使用指標創建的結構體和結構體內指標指向空間是不連續的,其間往往會有被浪費的記憶體碎片
3.提高訪問速度
在計算機中CPU讀取速度 硬碟<記憶體<高速快取<暫存器
計算機在讀取資料時遵循“區域性原理”,即接下來訪問的記憶體80%的概率在當前記憶體附近,所以暫存器會預先讀入周圍的資料,如果資料不連續,那意味著暫存器命中失敗,則要從高速快取到記憶體甚至到硬碟搜索,直到找到為止,速度自然慢了,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/304518.html
標籤:java


