前言
大家在編寫C程序的時候,是否會遇到數(shù)組空間不夠大,或者一次性就把空間開大了,
又怕造成空間開銷浪費等內(nèi)存大小相關(guān)問題不得其解的時候呢?
那么這一章內(nèi)容將會給你帶來解決這類問題的好辦法——動態(tài)內(nèi)存開辟
目錄
-
為什么要動態(tài)內(nèi)存分配
-
動態(tài)內(nèi)存函數(shù)的介紹
- malloc與free
- calloc
- realloc
-
常見的動態(tài)內(nèi)存錯誤
-
一些動態(tài)內(nèi)存的筆試題
-
內(nèi)容補充說明(C/C++程序內(nèi)存分配的幾個區(qū)域)
-
特加內(nèi)容(柔性數(shù)組)
正文
為什么要動態(tài)內(nèi)存分配
其實我們平時類似于下面這樣的開辟內(nèi)存空間都是在棧上開辟的!至于棧是什么?——這里不用過多理解,先知道!
int a=20;//在??臻g上開辟四個字節(jié)
int arr[20];//在??臻g上開辟10個字節(jié)的連續(xù)空間
上面的開辟方式有以下幾個特點:
- 空間開辟大小是固定的
- 數(shù)組在申明的時候,必須指定數(shù)組的長度,它所需要的內(nèi)存在編譯時分配
但是在實現(xiàn)的情況下:
對于空間的需求,不僅僅是上述的情況。有時候我們需要的空間大小在程序運行的時候才能知道,
那數(shù)組的編譯時開辟空間的方式就不能滿足了。
這時候就只能試試動態(tài)存開辟了。
所以動態(tài)內(nèi)存的開辟是很有必要
而且動態(tài)內(nèi)存開辟的空間是在堆區(qū)上開辟的,下面是C語言內(nèi)存的劃分區(qū)域圖
動態(tài)內(nèi)存函數(shù)的介紹
想使用動態(tài)內(nèi)存開辟空間,其實只需掌握以下幾個函數(shù)即可!
接下來就是函數(shù)的介紹與使用,當你理解后再去使用將會比較容易上手!
1.malloc與free
其實C語言提供了一個動態(tài)內(nèi)存開辟的函數(shù):
void* malloc (size_t size);//size_t本質(zhì)上是unsigned int類型
這個函數(shù)向內(nèi)存申請一塊連續(xù)可用的空間,并返回指向這塊空間的指針。
它的特點:
- 如果開辟成功,則返回一個指向開辟好空間的指針。
- 如果開辟失敗,則返回一個NULL指針,因此malloc的返回值一定要做檢查。
- 返回值的類型是 void* ,所以malloc函數(shù)并不知道開辟空間的類型,具體在使用的時候使用者自己來決定
- 如果參數(shù) size 為0,malloc的行為是標準是未定義的,取決于編譯器
可能單看文字你會迷迷糊糊的,說了一大堆,那究竟怎么使用呢?
舉個例子: 比如我要開辟一個有10個元素的整型數(shù)組!
#include<stdio.h>
#include<stdlib.h>//malloc所需的頭文件
int main()
{
//int arr[10];//在棧區(qū)開辟10個int類型的空間
int* pa = (int*)malloc(sizeof(int) * 10);//向堆區(qū)申請10個int類型的空間
if (pa == NULL)//判斷是否申請失敗
{
printf("malloc fail!\n");
return 1;
}
free(pa);//釋放內(nèi)存
pa = NULL;
return 0;
}
-
注意一:
不知道大家注意到?jīng)]有,我們malloc動態(tài)開辟內(nèi)存是需要向堆區(qū)申請的,所以有沒有可能會申請失敗呢?
答案是肯定!所以我們每次申請空間后都需要進行判斷是否申請成功,malloc函數(shù)申請失敗它們返回一個NULL。 -
注意二:
是否注意在malloc前面加了一個強制類型轉(zhuǎn)化,上面在講特點的時候提過,malloc返回的類型的void* 類型的指針,所以我們是需要根據(jù)自己所需進行轉(zhuǎn)化的! -
注意三:
在結(jié)尾一定要把自己動態(tài)開辟的內(nèi)存進行釋放!這就要介紹一下free函數(shù)了!
free
C語言提供了另外一個函數(shù)free,專門是用來做動態(tài)內(nèi)存的釋放和回收的,函數(shù)原型如下
void free (void* ptr);//void* 可以接收任意類型的指針
可能有一些小伙伴看到free函數(shù)的參數(shù)類型是void*,就在想我要不要先將原來的類型強轉(zhuǎn)成void *再使用?
沒必要,因為void* 就像一個垃圾桶,可以接收任意類型的指針變量?。?!
特點:
- 如果參數(shù) ptr 指向的空間不是動態(tài)開辟的,那free函數(shù)的行為是未定義的
- 如果參數(shù) ptr 是NULL指針,則函數(shù)什么事都不做
一個特別的提醒:
- 使用完free函數(shù),該函數(shù)只會將空間釋放,不會吧你的指針變量置空,這時候我們應(yīng)該養(yǎng)成良好的習(xí)慣,手動將指針置成NULL
//例如
free(pa);
pa=NULL;//手動置空
2.calloc
calloc函數(shù)它跟malloc函數(shù)的功能其實的大同小異的,只是參數(shù)和初始化不一樣?。?!
函數(shù)原型:
void* calloc (size_t num, size_t size);//num表示要多少個,size表示每一個的大小
特點:
- 函數(shù)的功能是為 num 個大小為 size 的元素開辟一塊空間,并且把空間的每個字節(jié)初始化為0。
- 與函數(shù) malloc 的區(qū)別只在于 calloc 會在返回地址之前把申請的空間的每個字節(jié)初始化為全0。
舉個例子: 還是開辟一個有10個整型的數(shù)組,這次用的是calloc
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* pa = (int*)calloc(10, sizeof(int));
if (pa == NULL)
{
printf("calloc fail!\n");
return 1;
}
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", pa[i]);
}
free(pa);
pa = NULL;
return 0;
}
看看它和malloc函數(shù)打印后的結(jié)果:
3.realloc
realloc 函數(shù)是可以幫我們進行擴容的?。?!挺好用的?。?!
它的出現(xiàn)讓動態(tài)內(nèi)存管理更加靈活
有時會我們發(fā)現(xiàn)過去申請的空間太小了,有時候我們又會覺得申請的空間過大了,那為了合理的時候內(nèi)存,我們一定會對內(nèi)存的大小做靈活的調(diào)整。那 realloc 函數(shù)就可以做到對動態(tài)開辟內(nèi)存大小的調(diào)整。
函數(shù)原型:
void* realloc (void* ptr, size_t size);
使用例子:我要在上面calloc開辟的10的int類型的空間上增加20個int類型的空間
#include<stdio.h>
#include<stdlib.h>
int main()
{
//向堆區(qū)申請10個int類型的空間
int* pa = (int*)calloc(10, sizeof(int));
if (pa == NULL)//判斷是否申請失敗
{
printf("calloc fail!\n");
return 1;
}
//......
//假設(shè)內(nèi)存不夠了,需要擴容20個int類型的空間
int* tem = (int*)realloc(pa, sizeof(int) * 30);//用臨時變量存起來
if (tem != NULL)//擴容成功才能把地址給pa
{
pa = tem;
}
free(pa);
pa = NULL;
return 0;
}
注意點:
- ptr 是要調(diào)整的內(nèi)存地址
- size 調(diào)整之后新大小
- 返回值為調(diào)整之后的內(nèi)存起始位置。
- 這個函數(shù)調(diào)整原內(nèi)存空間大小的基礎(chǔ)上,還會將原來內(nèi)存中的數(shù)據(jù)移動到 新 的空間
-
realloc在調(diào)整內(nèi)存空間的是存在兩種情況:
- 情況1:原有空間之后有足夠大的空間
- 情況2:原有空間之后沒有足夠大的空間
- 不要直接把擴容后的地址賦值給原地址,如果沒有擴容成功,則函數(shù)會返回NULL,這時又直接就把NULL賦值給了原來地址,會導(dǎo)致無法再找回原有的地址,只需創(chuàng)建個臨時變量判斷即可
對于上述注意點的第五條兩個情況的解釋
情況1
當是情況1 的時候,要擴展內(nèi)存就直接原有內(nèi)存之后直接追加空間,原來空間的數(shù)據(jù)不發(fā)生變化
情況2
當是情況2 的時候,原有空間之后沒有足夠多的空間時,擴展的方法是:在堆空間上另找一個合適大小的連續(xù)空間來使用。這樣函數(shù)返回的是一個新的內(nèi)存地址。由于上述的兩種情況,realloc函數(shù)的使用就要注意一些
常見的動態(tài)內(nèi)存錯誤
通過上面的學(xué)習(xí),你可能已經(jīng)會使用動態(tài)內(nèi)存開辟了,
但是新手在使用的時候難免會出現(xiàn)一些自己并不知道的bug!!!
下面給大家演示一些幾種常見的錯誤示范,讓你少走彎路?。?!
- 對NULL指針的解引用操作
void test()
{
int *p = (int *)malloc(INT_MAX/4);
*p = 20;//如果p的值是NULL,就會有問題
free(p);
}
結(jié)論:申請空間后一定要判斷是否申請成功了
- 對動態(tài)開辟空間的越界訪問
void test()
{
int i = 0;
int* p = (int*)malloc(10 * sizeof(int));
if (p==NULL)
{
return 1;
}
for (i = 0; i <= 10; i++)
{
p[i] = i;//當i是10的時候越界訪問
}
free(p);
}
結(jié)論:無論是動態(tài)開辟的,還是非動態(tài)開辟的內(nèi)存都要注意邊界問題,避免越界
- 對非動態(tài)開辟內(nèi)存使用free釋放
void test()
{
int a = 10;
int *p = &a;
free(p);//典型的對非動態(tài)開辟的內(nèi)存錯誤釋放
}
結(jié)論:不能對非動態(tài)開辟的內(nèi)存進行釋放(malloc,ralloc,realloc都是動態(tài)開辟)
- 使用free釋放一塊動態(tài)開辟內(nèi)存的一部分
void test()
{
int *p = (int *)malloc(100);
p=p+50;//有些人突然心血來潮覺得100太了,想釋放后半部分空間
free(p);//p不再指向動態(tài)內(nèi)存的起始位置
}
結(jié)論:使用free時,要注意傳過去的是動態(tài)內(nèi)存的起始地址,不能說釋放一點,只能一次性全部釋放
- 對同一塊動態(tài)內(nèi)存多次釋放
void test()
{
int *p = (int *)malloc(100);
free(p);
free(p);//重復(fù)釋放
}
結(jié)論:重復(fù)釋放同一個空間,程序會崩潰,但是我們可以養(yǎng)成良好習(xí)慣,free完后手動把指針置空,比如下面的程序就不會崩潰
void test()
{
int *p = (int *)malloc(100);
free(p);
p=NULL;
free(p);//重復(fù)釋放,p已經(jīng)為空,所以free不會做任何事情
}
因為把NULL傳給free它是不會干任何事情的?。。?/p>
- 動態(tài)開辟內(nèi)存忘記釋放(內(nèi)存泄漏,很嚴重)
void test()
{
int *p = (int *)malloc(100);
if(NULL != p)
{
*p = 20;
}
}
int main()
{
test();
while(1);
}
上面這個程序是隨便舉個例子的,你運行起來的話,在任務(wù)管理器可以看到它的內(nèi)存占比會一直不斷增加
結(jié)論:動態(tài)開辟的內(nèi)存一定要記得釋放,一定要養(yǎng)成malloc,calloc,realloc這些函數(shù)與free一定是搭配使用的?。。?/p>
動態(tài)開辟的空間一定要釋放,并且正確釋放?。?! |
一些動態(tài)內(nèi)存的筆試題
紙上得來終覺淺,絕知此事要躬行 |
既然我們學(xué)會了如何使用動態(tài)內(nèi)存函數(shù)了,那么想要深刻理解它,就要刷題了!!
只要做題才知道自己理解得怎么樣了,才知道自己有沒有掌握。
題目1
//請問運行Test 函數(shù)會有什么樣的結(jié)果?
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
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 函數(shù)會有什么樣的結(jié)果?
答案:什么也不會輸出,程序會崩潰。
原因:
- 首先它沒有可能存在內(nèi)存泄漏(次要原因,但不是導(dǎo)致它程序崩潰的原因)
- 傳參的時候傳的是形參,是無法修改str的,所以str一直的NULL
- 使用strcpy的時候,傳過去的是NULL空指針,strcpy會對NULL進行解引用(*),就是非法訪問的(崩潰的原因)
題目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 函數(shù)會有什么樣的結(jié)果?
答案:
原因:
- 野指針問題, 本質(zhì)就是在調(diào)用GetMemory這個函數(shù)的時候,它確實是把p的地址帶回來給了str,但是,數(shù)組p只是一個在GetMemory函數(shù)里的一個局部變量,只要出了這個函數(shù),它就被銷毀了,所以p已經(jīng)是一個野指針了,打印的是隨機值。
題目三
下面程序輸出什么,有沒有什么錯誤?
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;
}
答案:輸出:hello,有錯誤。
原因:
- 我們上面講過,申請內(nèi)存是不是可能出現(xiàn)失敗的情況,所以是需要判斷是否申請成功的,但是這里并沒有!
- 還記得我們說過使用malloc這樣的函數(shù)的時候,要搭配free使用嗎!這里雖然可以正常打印,但是呢,卻存在內(nèi)存泄漏的隱患,因為它沒有釋放內(nèi)存!
題目四
下面程序有什么錯誤?
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;
}
錯誤:
- 還是沒有判斷是否開辟成功,可能會出現(xiàn)開辟失敗,造成程序崩潰的錯誤隱患
- 釋放完沒有對str置空,我們說過,free之后要養(yǎng)成手動置空,因為free是不會幫我們把str置空的,所以str被free之后,已經(jīng)變成了野指針了,它就能進到下面的if語句,然后又會非法訪問?。?!
內(nèi)容補充說明(C/C++程序內(nèi)存分配的幾個區(qū)域)
- 棧區(qū)(stack):在執(zhí)行函數(shù)時,函數(shù)內(nèi)局部變量的存儲單元都可以在棧上創(chuàng)建,函數(shù)執(zhí)行結(jié)束時這些存儲單元自動被釋放。棧內(nèi)存分配運算內(nèi)置于處理器的指令集中,效率很高,但是分配的內(nèi)存容量有限。 棧區(qū)主要存放運行函數(shù)而分配的局部變量、函數(shù)參數(shù)、返回數(shù)據(jù)、返回地址等。
- 堆區(qū)(heap):一般由程序員分配釋放, 若程序員不釋放,程序結(jié)束時可能由OS回收 。分配方式類似于鏈表。
- 數(shù)據(jù)段(靜態(tài)區(qū))(static)存放全局變量、靜態(tài)數(shù)據(jù)。程序結(jié)束后由系統(tǒng)釋放。
- 代碼段:存放函數(shù)體(類成員函數(shù)和全局函數(shù))的二進制代碼。
特加內(nèi)容(柔性數(shù)組)
也許你從來沒有聽說過柔性數(shù)組(flexible array)這個概念,但是它確實是存在的。
C99 中,結(jié)構(gòu)中的最后一個元素允許是未知大小的數(shù)組,這就叫做『柔性數(shù)組』成員
比如像下面這樣的定義就是柔性數(shù)組
struct S
{
int i;
int a[0];//柔性數(shù)組成員
};
可能上面的定義有的編譯器可能編譯不過,下面這個就可以
struct S
{
int i;
int a[];//柔性數(shù)組成員
};
特點:
- 結(jié)構(gòu)中的柔性數(shù)組成員前面必須至少一個其他成員
- sizeof 返回的這種結(jié)構(gòu)大小不包括柔性數(shù)組的內(nèi)存
- 包含柔性數(shù)組成員的結(jié)構(gòu)用malloc ()函數(shù)進行內(nèi)存的動態(tài)分配,并且分配的內(nèi)存應(yīng)該大 于結(jié)構(gòu)的大小,以適應(yīng)柔性數(shù)組的預(yù)期大小
例如:當我們計算它的大小的時候
至于它的使用,本質(zhì)上就是當我們需要多大空間的時候,我們對柔性數(shù)組中的int a[]進行動態(tài)內(nèi)存開辟即可!
這里不打算過多介紹!
只是單單交代它的存在性!
它的好處有以下幾點文章來源:http://www.zghlxwxcb.cn/news/detail-404654.html
- 方便內(nèi)存釋放
- 這樣有利于訪問速度(連續(xù)的內(nèi)存有益于提高訪問速度,也有益于減少內(nèi)存碎片)
本章完!文章來源地址http://www.zghlxwxcb.cn/news/detail-404654.html
到了這里,關(guān)于【深入理解C】動態(tài)內(nèi)存管理的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!