天天看點

【C語言進階】—— 動态記憶體開辟+柔性數組⛳前言⌛一、尋根問底⌚二、動态記憶體函數⛵三、常見的動态記憶體錯誤✨四、經典筆試題練習⏲五、C/C++程式的記憶體開辟⚾六、柔性數組

@toc

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

【C語言進階】—— 動态記憶體開辟+柔性數組⛳前言⌛一、尋根問底⌚二、動态記憶體函數⛵三、常見的動态記憶體錯誤✨四、經典筆試題練習⏲五、C/C++程式的記憶體開辟⚾六、柔性數組

比如局部變量、函數形參通常是存儲在棧區的,這部分記憶體空間的特點就是臨時使用,用完即釋放(當然這個都是由作業系統自動完成的,不需要程式員的幹預);

再比如全局變量通常存放在靜态區,此外由static修飾的局部變量也會放到靜态區(是以static修飾局部變量,本質上是改變了其存儲的位置,從棧區-- > 靜态區),這部分記憶體空間就是生命周期很長,長到整個程式運作結束;

再例如我們使用的常量字元串,會被儲存到常量區,這部分記憶體區域的特點就是類似于“常量”,不可被修改,相當于添加了一個“const”的buff。

進入正題

思維導圖:

【C語言進階】—— 動态記憶體開辟+柔性數組⛳前言⌛一、尋根問底⌚二、動态記憶體函數⛵三、常見的動态記憶體錯誤✨四、經典筆試題練習⏲五、C/C++程式的記憶體開辟⚾六、柔性數組

由程式員根據實際程式設計需要向作業系統申請,在堆區上開辟的,供程式員操作使用和維護的記憶體空間,程式員的遊樂園!通常是一些臨時用到的資料或者變量,随時開辟,用完随時釋放,而不必等到函數結束後由作業系統回收!

我們已經掌握的記憶體開辟方式有:

但是上述的開辟空間的方式有兩個特點:

空間開辟大小是固定的。

數組在申明的時候,必須指定數組的長度,它所需要的記憶體在編譯時配置設定。

但是對于空間的需求,不僅僅是上述的情況。有時候我們需要的空間大小在程式運作的時候才能知道,那數組的編 譯時開辟空間的方式就不能滿足了。 這時候就隻能試試動态存開辟了。

<code>通過系統提供的4個庫函數實作,malloc\calloc\realloc\free</code>,這四個函數後面我們會詳細介紹。

注意:<code>以下說的四個函數的頭檔案均為:stdlib.h</code>

c語言提供了一個<code>動态記憶體開辟的函數</code>:

【C語言進階】—— 動态記憶體開辟+柔性數組⛳前言⌛一、尋根問底⌚二、動态記憶體函數⛵三、常見的動态記憶體錯誤✨四、經典筆試題練習⏲五、C/C++程式的記憶體開辟⚾六、柔性數組

size_t就是unsigned int(無符号整型)

這個函數的作用就是在動态存儲區中配置設定一個長度為size個位元組的<code>連續空間</code>,并傳回指向該空間的指針。

1)如果開辟成功,則傳回一個指向開辟好空間的指針。 2)如果開辟失敗,則傳回一個null指針,是以malloc的傳回值一定要做檢查。 3)<code>傳回值的類型是void *</code>,是以malloc函數并不知道開辟空間的類型,具體在使用的時候使用者自己來決定。 4)如果參數size為0,malloc的行為是标準是未定義的,取決于編譯器。

示例

動态開辟的空間如何釋放和回收呢?

c語言提供了一個專門完成這個功能的庫函數-- - free

c語言提供了另外一個<code>函數free</code>,專門是用來<code>做動态記憶體的釋放和回收</code>的。

函數原型:

【C語言進階】—— 動态記憶體開辟+柔性數組⛳前言⌛一、尋根問底⌚二、動态記憶體函數⛵三、常見的動态記憶體錯誤✨四、經典筆試題練習⏲五、C/C++程式的記憶體開辟⚾六、柔性數組

<code>free的作用就是釋放指針變量p所指向的動态空間,使這部分空間能夠重新被利用。</code>

1)如果參數ptr指向的空間不是動态開辟的,那free函數的行為是未定義的。 2)如果參數 ptr是null指針,則函數什麼事都不做。

看一下實際的使用:

【C語言進階】—— 動态記憶體開辟+柔性數組⛳前言⌛一、尋根問底⌚二、動态記憶體函數⛵三、常見的動态記憶體錯誤✨四、經典筆試題練習⏲五、C/C++程式的記憶體開辟⚾六、柔性數組
注意: 用malloc申請的空間,裡面的内容是随機值,如果不初始化的話,可能就會得到一些意想不到的值;
【C語言進階】—— 動态記憶體開辟+柔性數組⛳前言⌛一、尋根問底⌚二、動态記憶體函數⛵三、常見的動态記憶體錯誤✨四、經典筆試題練習⏲五、C/C++程式的記憶體開辟⚾六、柔性數組
了解:如果參數ptr指向的空間不是動态開辟的,那free函數的行為是未定義的。
【C語言進階】—— 動态記憶體開辟+柔性數組⛳前言⌛一、尋根問底⌚二、動态記憶體函數⛵三、常見的動态記憶體錯誤✨四、經典筆試題練習⏲五、C/C++程式的記憶體開辟⚾六、柔性數組

為什麼要進行動态記憶體的釋放和回收?

記憶體空間是有限的,如果我們每次在使用的時候隻是一味的向申請空間,即使空間再大,也會被用完,而且如果使用的空間不釋放會導緻電腦越來越卡,程式運作越來越慢!

那釋放之後為什麼要手動将指針指派為null(空指針)呢?

舉一個生活中的例子吧,假設有一個男生跟他女朋友分手了,如果這個男生還一直保留這個女生的電話、微信,更有甚者,還有這個女生家裡面的鑰匙。如果你是這個女生的話,你希望他仍然保留這些資訊和物品嗎?你肯定是不想對吧,指不定哪一天他不高興或者其它原因就來騷擾你。(是以才會有一句話叫做情侶分手千萬不要藕斷絲連,當然如果你是這個男生的話,你可能還想着以後和好如初,念念不忘,必有回響~haha)。 言歸正傳,程式設計中如果指針指向的空間已經被釋放了,如果不将其置為null,那麼其仍然保留這個地方的位址,之後仍然有可能通路到這片空間,這個生活就是非法通路了!

那有沒有動态配置設定函數在申請空間的同時就進行初始化呢 ?

答案當然是有,接下來要結束的calloc就是這樣的一個函數

<code>calloc函數也用來動态記憶體配置設定</code>,函數原型:

【C語言進階】—— 動态記憶體開辟+柔性數組⛳前言⌛一、尋根問底⌚二、動态記憶體函數⛵三、常見的動态記憶體錯誤✨四、經典筆試題練習⏲五、C/C++程式的記憶體開辟⚾六、柔性數組
1)函數的功能是為<code>num</code>個大小為<code>size</code>的元素開辟一塊空間,并且把空間的每個位元組初始化為0 2)與函數<code>ma1loc</code>的差別隻在于<code>calloc會在傳回位址之前把申請的空間的每個位元組初始化為全0。</code>

比如剛剛的上面的代碼,如果我們将malloc換成calloc,不進行手動初始化:

【C語言進階】—— 動态記憶體開辟+柔性數組⛳前言⌛一、尋根問底⌚二、動态記憶體函數⛵三、常見的動态記憶體錯誤✨四、經典筆試題練習⏲五、C/C++程式的記憶體開辟⚾六、柔性數組

在記憶體儲存如下:

【C語言進階】—— 動态記憶體開辟+柔性數組⛳前言⌛一、尋根問底⌚二、動态記憶體函數⛵三、常見的動态記憶體錯誤✨四、經典筆試題練習⏲五、C/C++程式的記憶體開辟⚾六、柔性數組

是以如何我們對申請的記憶體空間的内容要求初始化,那麼可以很友善的使用calloc函數來完成任務。

光從上面這三個函數的介紹,我們可能并沒有深刻體會到“動态記憶體配置設定”的動态展現在哪,接下來要介紹的函數才是動态記憶體配置設定的“靈魂”-- - realloc

<code>realloc函數</code>的出現讓動态記憶體管理更加靈活

有時會我們發現過去申請的空間太小了,有時候我們又會覺得申請的空間過大了,那為了合理的使用記憶體,我們一定會<code>對記憶體的大小做靈活的調整</code>。那<code>rea1lloc函數就可以做到對動态開辟記憶體大小的調整</code>

【C語言進階】—— 動态記憶體開辟+柔性數組⛳前言⌛一、尋根問底⌚二、動态記憶體函數⛵三、常見的動态記憶體錯誤✨四、經典筆試題練習⏲五、C/C++程式的記憶體開辟⚾六、柔性數組
1)ptr是要調整的記憶體位址.size是調整之後新大小 2)傳回值為調整之後的記憶體起始位置 3)這個函數調整原記憶體空間大小的基礎上,還會将原來記憶體中的資料移動到新的空間。(這種移動的方式實際上就是複制拷貝,會将原内容複制拷貝到新記憶體中) 4)realloc在調整記憶體空間的是存在兩種情況︰ 情況1∶原有空間之後有足夠大的空間 情況2︰原有空間之後沒有足夠大的空間
【C語言進階】—— 動态記憶體開辟+柔性數組⛳前言⌛一、尋根問底⌚二、動态記憶體函數⛵三、常見的動态記憶體錯誤✨四、經典筆試題練習⏲五、C/C++程式的記憶體開辟⚾六、柔性數組
【C語言進階】—— 動态記憶體開辟+柔性數組⛳前言⌛一、尋根問底⌚二、動态記憶體函數⛵三、常見的動态記憶體錯誤✨四、經典筆試題練習⏲五、C/C++程式的記憶體開辟⚾六、柔性數組
當是情況1的時候,要擴充記憶體就直接原有記憶體之後直接追加空間,原來空間的資料不發生變化。 當是情況2的時候,原有空間之後沒有足夠多的空間時,擴充的方法是∶在堆空間上另找一個合适大小的連續空間來使用。這樣函數傳回的是一個新的記憶體位址。

由于上述的兩種情況,realloc函數的使用就要注意一些。

【C語言進階】—— 動态記憶體開辟+柔性數組⛳前言⌛一、尋根問底⌚二、動态記憶體函數⛵三、常見的動态記憶體錯誤✨四、經典筆試題練習⏲五、C/C++程式的記憶體開辟⚾六、柔性數組

當i = 10時越界通路

【C語言進階】—— 動态記憶體開辟+柔性數組⛳前言⌛一、尋根問底⌚二、動态記憶體函數⛵三、常見的動态記憶體錯誤✨四、經典筆試題練習⏲五、C/C++程式的記憶體開辟⚾六、柔性數組

free一定是釋放堆區上的資料

【C語言進階】—— 動态記憶體開辟+柔性數組⛳前言⌛一、尋根問底⌚二、動态記憶體函數⛵三、常見的動态記憶體錯誤✨四、經典筆試題練習⏲五、C/C++程式的記憶體開辟⚾六、柔性數組
【C語言進階】—— 動态記憶體開辟+柔性數組⛳前言⌛一、尋根問底⌚二、動态記憶體函數⛵三、常見的動态記憶體錯誤✨四、經典筆試題練習⏲五、C/C++程式的記憶體開辟⚾六、柔性數組
free要指向開辟好空間的起始位置,防止開辟空間指針的自增自減運算
【C語言進階】—— 動态記憶體開辟+柔性數組⛳前言⌛一、尋根問底⌚二、動态記憶體函數⛵三、常見的動态記憶體錯誤✨四、經典筆試題練習⏲五、C/C++程式的記憶體開辟⚾六、柔性數組

(沒停止運作我電腦當機了!!!……)

忘記釋放不再使用的動态開辟的空間會造成記憶體洩漏。

✨請問運作test函數會有什麼樣的結果?

✨ <code>程式崩潰,無法列印</code>

分析:

【C語言進階】—— 動态記憶體開辟+柔性數組⛳前言⌛一、尋根問底⌚二、動态記憶體函數⛵三、常見的動态記憶體錯誤✨四、經典筆試題練習⏲五、C/C++程式的記憶體開辟⚾六、柔性數組

改進方法一:

【C語言進階】—— 動态記憶體開辟+柔性數組⛳前言⌛一、尋根問底⌚二、動态記憶體函數⛵三、常見的動态記憶體錯誤✨四、經典筆試題練習⏲五、C/C++程式的記憶體開辟⚾六、柔性數組

改進方法二:

【C語言進階】—— 動态記憶體開辟+柔性數組⛳前言⌛一、尋根問底⌚二、動态記憶體函數⛵三、常見的動态記憶體錯誤✨四、經典筆試題練習⏲五、C/C++程式的記憶體開辟⚾六、柔性數組
✨<code>str非法通路,程式出錯,出現傳回棧空間位址的問題</code>

退出函數時被銷毀了,雖然傳回了但是在棧區退出時被銷毀了,重新賦予p随機值

【C語言進階】—— 動态記憶體開辟+柔性數組⛳前言⌛一、尋根問底⌚二、動态記憶體函數⛵三、常見的動态記憶體錯誤✨四、經典筆試題練習⏲五、C/C++程式的記憶體開辟⚾六、柔性數組
✨ 沒有釋放動态開辟的記憶體(free),導緻記憶體洩漏
✨ 提前釋放記憶體,後面strcpy函數非法通路
【C語言進階】—— 動态記憶體開辟+柔性數組⛳前言⌛一、尋根問底⌚二、動态記憶體函數⛵三、常見的動态記憶體錯誤✨四、經典筆試題練習⏲五、C/C++程式的記憶體開辟⚾六、柔性數組

c/c++程式記憶體配置設定的幾個區域:

<code>棧區(stack)</code>:在執行函數時,函數内局部變量的存儲單元都可以在棧上建立,函數執行結束時這些 存儲單元自動被釋放。棧記憶體配置設定運算内置于處理器的指令集中,效率很高,但是配置設定的記憶體容量有 限。 棧區主要存放運作函數而配置設定的局部變量、函數參數、傳回資料、傳回位址等。 <code>堆區(heap)</code>:一般由程式員配置設定釋放, 若程式員不釋放,程式結束時可能由os回收 。配置設定方式類似于連結清單。 <code>資料段(靜态區)(static)</code>:存放全局變量、靜态資料。程式結束後由系統釋放。 <code>代碼段</code>:存放函數體(類成員函數和全局函數)的二進制代碼。 有了這幅圖,我們就可以更好的了解在《c語言初識》中講的static關鍵字修飾局部變量的例子了。 實際上普通的局部變量是在棧區配置設定空間的,<code>棧區的特點是在上面建立的變量出了作用域就銷毀。</code> 但是被static修飾的變量存放在資料段(靜态區),資料段的特點是在上面建立的變量,直到程式結束才銷毀 是以生命周期變長。

也許你從來沒有聽說過柔性數組(flexible array)這個概念,但是它确實是存在的。c99中,<code>結構體中的最後一個元素允許是未知大小的數組,這就叫做『柔性數組』成員</code>。

其實從名字我們也可以大概知道其含義,“柔性”指柔軟的,可變動的,flexible 本就具有靈活的,可變的含義。
柔性數組的特點∶ 1)結構中的<code>柔性數組成員前面必須至少一個其他成員</code>。(也就是說柔性數組成員不能單獨存在) 2)<code>sizeof傳回的這種結構大小不包括柔性數組的記憶體</code>。(計算大小的時候,不考慮柔性數組成員的大小)
【C語言進階】—— 動态記憶體開辟+柔性數組⛳前言⌛一、尋根問底⌚二、動态記憶體函數⛵三、常見的動态記憶體錯誤✨四、經典筆試題練習⏲五、C/C++程式的記憶體開辟⚾六、柔性數組
3)<code>包含柔性數組成員的結構用malloc( )函數進行記憶體的動态配置設定</code>,并且配置設定的記憶體應該大于結構的大小,以适應柔性數組的預期大小。(也就是包含柔性數組成員的結構體類型在建立變量的時候,需要用動态記憶體開辟的方式來建立。 原因是:柔性數組的大小可變,那麼其建立出來的結構體變量大小也是可變的,是以需要動态開辟的方式來建立!)

舉例:

動态開辟記憶體分布情況如圖:

【C語言進階】—— 動态記憶體開辟+柔性數組⛳前言⌛一、尋根問底⌚二、動态記憶體函數⛵三、常見的動态記憶體錯誤✨四、經典筆試題練習⏲五、C/C++程式的記憶體開辟⚾六、柔性數組

正因為空間是動态開辟出來的,如果後續使用的時候,數組arr的空間大小不夠了,可以通過realloc去動态調整,展現了其“柔性”的特點。

【C語言進階】—— 動态記憶體開辟+柔性數組⛳前言⌛一、尋根問底⌚二、動态記憶體函數⛵三、常見的動态記憶體錯誤✨四、經典筆試題練習⏲五、C/C++程式的記憶體開辟⚾六、柔性數組

用一個指針代替柔性數組成員

【C語言進階】—— 動态記憶體開辟+柔性數組⛳前言⌛一、尋根問底⌚二、動态記憶體函數⛵三、常見的動态記憶體錯誤✨四、經典筆試題練習⏲五、C/C++程式的記憶體開辟⚾六、柔性數組

如果記憶體不夠,想再次修改如下:

【C語言進階】—— 動态記憶體開辟+柔性數組⛳前言⌛一、尋根問底⌚二、動态記憶體函數⛵三、常見的動态記憶體錯誤✨四、經典筆試題練習⏲五、C/C++程式的記憶體開辟⚾六、柔性數組

柔性數組與非柔性數組比較

好處一:方面記憶體釋放 如果我們的代碼是在一個給别人用的函數中,你在裡面做了二次記憶體配置設定,并把整個結構體傳回給使用者。使用者調用free可以釋放結構體,但是使用者并不知道這個結構體内的成員也需要free,是以你不能指望使用者來發現這個事。是以,如果我們把結構體的記憶體以及其成員要的記憶體一次性配置設定好了,并傳回給使用者一個結構體指針,使用者做一次free就可以把所有的記憶體也給釋放掉。 好處二 : 這樣有利于通路速度. 連續的記憶體有益于提高通路速度,也有益于減少記憶體碎片。(其實,我個人覺得也沒多高了,反正你跑不了要用做偏移量的加法來尋址) (涉及到記憶體池,局部性原理:空間局部性原理、時間局部性原理。)
【C語言進階】—— 動态記憶體開辟+柔性數組⛳前言⌛一、尋根問底⌚二、動态記憶體函數⛵三、常見的動态記憶體錯誤✨四、經典筆試題練習⏲五、C/C++程式的記憶體開辟⚾六、柔性數組

繼續閱讀