前言
現代計算機基本都是基于馮諾伊曼結構體系設計出來的,馮諾伊曼結構體系的核心就是“存盤程式”,將程式(指令集)和資料以同等地位存盤在記憶體中,但是我們的記憶體空間并不是無限大的,所以為了高效的利用好記憶體空間,作業系統會對這些記憶體空間進行相應的磁區,不同區域的記憶體有其對應的功能和使用方式,

比如區域變數、函式形參通常是存盤在堆疊區的,這部分記憶體空間的特點就是臨時使用,用完即釋放(當然這個都是由作業系統自動完成的,不需要程式員的干預);
再比如全域變數通常存放在靜態區,此外由static修飾的區域變數也會放到靜態區(所以static修飾區域變數,本質上是改變了其存盤的位置,從堆疊區-- > 靜態區),這部分記憶體空間就是生命周期很長,長到整個程式運行結束;
再例如我們使用的常量字串,會被保存到常量區,這部分記憶體區域的特點就是類似于“常量”,不可被修改,相當于添加了一個“const”的buff,
(我的廢話:在學習的時候如果可以多多思考,聯系不同的知識點,當將這些內容串起來時,可以形成一個比較宏觀、整體的角度,更進一步,如果我們能夠從中找到樂趣就更好了!現在我們進入正題吧!)
全文結構:

文章目錄
- 前言
- 一、尋根問底
- 什么是動態記憶體分配 / 管理?
- 為什么需要動態記憶體分配?
- 怎么建立動態記憶體分配?
- 二、動態記憶體函式
- malloc
- free
- calloc
- realloc
- 三、常見的動態記憶體錯誤
- 1)對空指標NULL的解參考操作
- 2)對動態開辟空間的越界訪問
- 3)對非動態開辟記憶體使用 free 釋放
- 4)使用 free 釋放一塊動態開辟記憶體的一部分
- 5)對同一塊動態記憶體的多次釋放
- 6)動態開辟記憶體忘記釋放(導致記憶體泄露)
- 四、柔性陣列
一、尋根問底
什么是動態記憶體分配 / 管理?
由程式員根據實際編程需要向作業系統申請,在堆區上開辟的,供程式員操作使用和維護的記憶體空間,程式員的游樂園!通常是一些臨時用到的資料或者變數,隨時開辟,用完隨時釋放,而不必等到函式結束后由作業系統回收!
為什么需要動態記憶體分配?
實際編程中,不僅需要大小固定的記憶體空間,往往還需要大小可變的記憶體空間,
比如說如果我們要建立一個通訊錄,用來存放相關資訊(姓名、性別、年齡、電話等),這種復雜的資料,我們知道要用結構體型別來存盤,而且要用結構體陣列來存,但是問題是“這個陣列的大小應該多大呢?”
這個問題確實不好回答,如果長度給小了,那么就會導致資料溢位,進而引發程式崩潰,
如果給大了,又會導致存盤空間大量浪費,空間利用率低,
為了解決這一類問題,就出現了動態記憶體分配,記憶體空間按需索取,要多少給多少!
怎么建立動態記憶體分配?
通過系統提供的4個庫函式實作,malloc\calloc\realloc\free,這四個函式后面我們會詳細介紹,
二、動態記憶體函式
注意:以下說的四個函式的頭檔案均為:stdlib.h
malloc
函式原型:void * malloc(size_t size);
size_t就是unsigned int(無符號整型)
這個函式的作用就是在動態存盤區中分配一個長度為size個位元組的連續空間,并回傳指向該空間的指標,
1)如果開辟成功,則回傳一個指向開辟好空間的指標,
2)如果開辟失敗,則回傳一個NULL指標,因此malloc的回傳值一定要做檢查,
3)回傳值的型別是void * ,所以malloc函式并不知道開辟空間的型別,具體在使用的時候使用者自己來決定,
4)如果引數size為0,malloc的行為是標準是未定義的,取決于編譯器,
動態開辟的空間如何釋放和回收呢?
C語言提供了一個專門完成這個功能的庫函式-- - free
free
函式原型:void free(void* p)
free的作用就是釋放指標變數p所指向的動態空間,使這部分空間能夠重新被利用,
1)如果引數ptr指向的空間不是動態開辟的,那free函式的行為是未定義的,
2)如果引數 ptr是NULL指標,則函式什么事都不做,
現在來看一下實際的使用:
#include<stdio.h>
#include<stdlib.h>
int main()
{
//1.通過動態開辟申請10個int型別的空間
int* ptr = (int*)malloc(10 * sizeof(int));//通常結合sizeof一起使用
//根據實際使用強制型別轉換為想要的型別
//2.malloc有可能申請空間失敗,所以需要判斷一下
if (ptr == NULL)
{
perror("main");//perror是一個報錯函式,實際出錯時列印效果為:main:xxxxxx(錯誤原因)
return 0;//出錯就直接結束函式
}
//3.使用 給這10個整型空間賦值
for (int i = 0; i < 10; i++)
{
*(ptr + i) = i;
}
//列印一下
for (int i = 0; i < 10; i++)
{
printf("%d ", ptr[i]);//這里可以直接使用陣列下標的形式,和指標解參考是一樣的
}
//4.釋放
free(ptr);
ptr = NULL;//需要手動置為NULL,防止非法訪問
return 0;
}

注意: 用malloc申請的空間,里面的內容是隨機值,如果不初始化的話,可能就會得到一些意想不到的值;

理解:如果引數ptr指向的空間不是動態開辟的,那free函式的行為是未定義的,

為什么要進行動態記憶體的釋放和回收?
記憶體空間是有限的,如果我們每次在使用的時候只是一味的向申請空間,即使空間再大,也會被用完,而且如果使用的空間不釋放會導致電腦越來越卡,程式運行越來越慢!
那釋放之后為什么要手動將指標賦值為NULL(空指標)呢?
舉一個生活中的例子吧,假設有一個男生跟他女朋友分手了,如果這個男生還一直保留這個女生的電話、微信,更有甚者,還有這個女生家里面的鑰匙,如果你是這個女生的話,你希望他仍然保留這些資訊和物品嗎?你肯定是不想對吧,指不定哪一天他不高興或者其它原因就來騷擾你,(所以才會有一句話叫做情侶分手千萬不要藕斷絲連,當然如果你是這個男生的話,你可能還想著以后和好如初,念念不忘,必有回響~haha),
言歸正傳,編程中如果指標指向的空間已經被釋放了,如果不將其置為NULL,那么其仍然保留這個地方的地址,之后仍然有可能訪問到這片空間,這個生活就是非法訪問了!
那有沒有動態分配函式在申請空間的同時就進行初始化呢 ?
答案當然是有,接下來要結束的calloc就是這樣的一個函式
calloc
函式原型:void * calloc(size_t num, size_t size);
1)函式的功能是為num 個大小為size的元素開辟一塊空間,并且把空間的每個位元組初始化為O
2)與函式ma1loc的區別只在于calloc會在回傳地址之前把申請的空間的每個位元組初始化為全0,
比如剛剛的上面的代碼,如果我們將malloc換成calloc,不進行手動初始化:
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* ptr = (int*)calloc(10, sizeof(int));
if (ptr == NULL)
{
perror("main");
//perror是一個報錯函式,實際出錯時列印效果為:main:xxxxxx(錯誤原因)
}
for (int i = 0; i < 10; i++)
{
printf("%d ", ptr[i]);
}
free(ptr);
ptr = NULL;
return 0;
}

光從上面這三個函式的介紹,我們可能并沒有深刻體會到“動態記憶體分配”的動態體現在哪,接下來要介紹的函式才是動態記憶體分配的“靈魂”-- - realloc
realloc
realloc函式的出現讓動態記憶體管理更加靈活,
有時會我們發現過去申請的空間太小了,有時候我們又會覺得申請的空間過大了,那為了合理的使用記憶體,我們一定會對記憶體的大小做靈活的調整,那rea1lloc函式就可以做到對動態開辟記憶體大小的調整,
函式原型:void * realloc(void* ptr, size_t size);
1)ptr是要調整的記憶體地址.size是調整之后新大小
2)回傳值為調整之后的記憶體起始位置
3)這個函式調整原記憶體空間大小的基礎上,還會將原來記憶體中的資料移動到新的空間,(這種移動的方式實際上就是復制拷貝,會將原內容復制拷貝到新記憶體中)
4)realloc在調整記憶體空間的是存在兩種情況︰
情況1∶原有空間之后有足夠大的空間
情況2︰原有空間之后沒有足夠大的空間

當是情況1的時候,要擴展記憶體就直接原有記憶體之后直接追加空間,原來空間的資料不發生變化,
當是情況2的時候,原有空間之后沒有足夠多的空間時,擴展的方法是∶在堆空間上另找一個合適大小的連續空間來使用,這樣函式回傳的是一個新的記憶體地址,
由于上述的兩種情況,realloc函式的使用就要注意一些,
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* ptr = (int*)malloc(10 * sizeof(int));
if (ptr == NULL)
{
perror("main");
return 0;
}
//業務處理
//進行擴容操作
int* p = (int*)realloc(ptr, 100 * sizeof(int));
//注意:不能直接將擴容之后的地址給ptr,因為存在擴容失敗的可能,會導致ptr地址丟失
if (p == NULL)
{
printf("raalloc failed!\n");
return 0;
}
ptr = p;
//業務處理
free(ptr);
ptr = NULL;
return 0;
}
三、常見的動態記憶體錯誤
1)對空指標NULL的解參考操作
void test()
{
//int* p = NULL;
int* p = (int*)malloc(INT_MAX);
*p = 20;//如果p的值為NULL,就會出問題
free(p);
}
2)對動態開辟空間的越界訪問

當I = 10時越界訪問
3)對非動態開辟記憶體使用 free 釋放

4)使用 free 釋放一塊動態開辟記憶體的一部分

5)對同一塊動態記憶體的多次釋放

6)動態開辟記憶體忘記釋放(導致記憶體泄露)
#include<stdio.h>
#include<stdlib.h>
void test()
{
int* p = (int*)malloc(100);
if (NULL != p)
{
*p = 20;
}
}
int main()
{
while (1)
{
test();
}
return 0;
}

四、柔性陣列
也許你從來沒有聽說過柔性陣列(flexible array)這個概念,但是它確實是存在的,C99中,結構中的最后一個元素允許是未知大小的陣列,這就叫做『柔性陣列』成員,
其實從名字我們也可以大概知道其含義,“柔性”指柔軟的,可變動的,flexible 本就具有靈活的,可變的含義,
舉例:
struct S
{
int n;
int arr[];//還可以寫成這樣 int arr[0];
};
柔性陣列的特點∶
1)結構中的柔性陣列成員前面必須至少一個其他成員,(也就是說柔性陣列成員不能單獨存在)
2)sizeof回傳的這種結構大小不包括柔性陣列的記憶體,(計算大小的時候,不考慮柔性陣列成員的大小)

3)包含柔性陣列成員的結構用malloc()函式進行記憶體的動態分配,并且分配的記憶體應該大于結構的大小,以適應柔性陣列的預期大小,(也就是包含柔性陣列成員的結構體型別在創建變數的時候,需要用動態記憶體開辟的方式來創建,原因是:柔性陣列的大小可變,那么其創建出來的結構體變數大小也是可變的,所以需要動態開辟的方式來創建!)
舉例:
#include<stdio.h>
struct S
{
int n;
int arr[];//還可以寫成這樣 int arr[0];
};
int main()
{
//struct S s1;這種方式創建的變數無法正常使用
//printf("%d\n", sizeof(s1));
//假設我們期望arr的大小是10個int型別
struct S* ps = (struct S*)malloc(sizeof(struct S) + 10 * sizeof(int));
return 0;
}
動態開辟記憶體分布情況如圖:

正因為空間是動態開辟出來的,如果后續使用的時候,陣列arr的空間大小不夠了,可以通過realloc去動態調整,體現了其“柔性”的特點,

完整的代碼:
#include<stdio.h>
struct S
{
int n;
int arr[];//還可以寫成這樣 int arr[0];
};
int main()
{
//假設我們期望arr的大小是10個int型別
struct S* ps = (struct S*)malloc(sizeof(struct S) + 10 * sizeof(int));
ps->n = 10;
int i = 0;
for (i = 0; i < 10; i++)
{
ps->arr[i] = i;
}
//調整
struct S* ptr = (struct S*)realloc(ps, sizeof(struct S) + 20 * sizeof(int));
if (ptr == NULL)
{
perror("main");
return 1;
}
ps = ptr;
//使用
//.....
//釋放
free(ps);
ps = NULL;
return 0;
}
柔性陣列功能的替代方法
用一個指標代替柔性陣列成員
struct S
{
int n;
//int arr[];//還可以寫成這樣 int arr[0];
int* arr;//替換柔性陣列
};
柔性陣列與非柔性陣列比較
好處一:方面記憶體釋放
如果我們的代碼是在一個給別人用的函式中,你在里面做了二次記憶體分配,并把整個結構體回傳給用戶,用戶呼叫free可以釋放結構體,但是用戶并不知道這個結構體內的成員也需要free,所以你不能指望用戶來發現這個事,所以,如果我們把結構體的記憶體以及其成員要的記憶體一次性分配好了,并回傳給用戶一個結構體指標,用戶做一次free就可以把所有的記憶體也給釋放掉,
好處二 : 這樣有利于訪問速度.
連續的記憶體有益于提高訪問速度,也有益于減少記憶體碎片,(其實,我個人覺得也沒多高了,反正你跑不了要用做偏移量的加法來尋址)
(涉及到記憶體池,區域性原理:空間區域性原理、時間區域性原理,有興趣的同學可自行查找資料!)
回顧:

轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/290324.html
標籤:其他
