C\C++語言3|動態內存(堆內存)使用和管理以及動態數組、向量

C語言 操作系統 程序員 電腦 小智雅匯 2019-05-29

在C或C++中,每個程序都需要用到幾個變量在寫程序前就應該知道,每個數組有幾個元素也必須在寫程序時就決定。有時在編程序時,我們並不知道需要多大的數組或需要多少變量,直到程序開始運行,根據某一個當前運行值才能決定。(或者通過交互確定。)

在定義數組時,我們建議按最大的可能值定義數組,每次運行時使用數組的一部分元素,當元素個數變化不是很大時,這個方案是可行的;但如果元素個數的變化範圍很大時,就太浪費空間了。

這個問題一個更好的解決方案就是動態變量機制。所謂動態變量是指:在寫程序時無法確定它們的存在,只有當程序運行起來時,隨著程序的運行,根據程序的需求動態產生和消亡的變量。由於動態變量不能在程序中定義,也就無法給它們取名字,因此動態變量的訪問需要通過指向動態變量的指針變量來進行間接訪問。

計算機內存除了操作系統、各進程佔用的空間以外,剩餘的空間(加上硬盤的虛擬內存)都可以用作程序的動態內存(堆內存)。

要使用動態變量,必須定義一個相應類型的指針變量(存放一個內存單元地址的變量),然後通過動態變量申請的功能向系統申請一塊空間,將空間的地址存入該指針變量。這樣就可以間接訪問動態變量了。當程序運行結束時,系統會自動回收指針佔用的空間,但並不回收指針指向的動態變量的空間,動態變量的空間需要程序員在程序中地釋放。因此要實現等實現動態內存分配,系統必須提供以下3個功能。

定義指針變量;
動態申請空間;
動態回收空間;

1 C語言的動態內存申請

在C語言中,malloc()函數的功能是在內存的動態存儲區中分配size個字節的連續空間,它的返回值指向所分配的那一段空間的起始地址,若分配失敗,則返回一個空指針(0)。

void *malloc(unsigned int size);
void *calloc(unsigned int n, unsigned int size);
void *realloc(void *mem_address, unsigned int newsize);

malloc的全稱是memory allocation,中文叫動態內存分配,用於申請一塊連續的指定大小的內存塊區域以void*類型返回分配的內存區域地址,當無法知道內存具體位置的時候,想要綁定真正的內存空間,就需要用到動態的分配內存。

malloc()函數接受一個參數:所需的內存字節數。malloc()函數會找到合適的空閒內存塊,這樣的內存塊是匿名的。也就是說,malloc()分配內存,但是不會為其賦名。然而,它確實返回動態分配內存塊的首字節地址。因此,可以把該地址賦給一個指針變量,並使用這個指針變量訪問這塊內存。

malloc()函數返回的是一個void通用指針,可以通過強制類型轉換來轉換為特定的指針類型。

void* 類型表示未確定類型的指針。C、C++規定,void* 類型可以通過類型轉換強制轉換為任何其它類型的指針。

類型未確定,即無法確定對應內存空間的長度和解碼方案,強制類型轉換,即重新確定長度和解碼方案。

malloc()一般需和free()函數配對使用,free()用於釋放內存。

malloc 只管分配內存,並不能對所得的內存進行初始化,所以得到的一片新內存中,其值將是隨機的。

calloc()函數接受兩個參數,第一個參數是所需的存儲單元數量,第二個參數是存儲單元的大小(以字節為單位)。另外,callo()函數會把塊中的所有位都設置為0.

calloc()的功能是在內存的動態存儲區中分配n個長度為size個字節的連續空間,它的返回值是指向所分配空間的起始地址,若分配失敗,則返回一個空指針(0)。

realloc()可以確保一塊足夠大的連續空間,當空間或大或小時,可以重新分配,目的就是確保空間夠用並連續。

void *realloc(void *mem_address, unsigned int newsize);

realloc()先判斷當前的指針是否有足夠的連續空間,如果有,擴大mem_address指向的地址,並且將mem_address返回,如果空間不夠,先按照newsize指定的大小分配空間,將原有數據從頭到尾拷貝到新分配的內存區域,而後釋放原來mem_address所指內存區域(注意:原來指針是自動釋放,不需要使用free),同時返回新分配的內存區域的首地址。即重新分配存儲器塊的地址。

當今的操作系統都會給應用程序的每一個進程分配獨立的“虛擬地址空間”。

程序中訪問的內存地址不再是實際的物理內存地址,而是一個虛擬地址,然後由操作系統將這個虛擬地址映射到合適的物理內存地址上。這樣一來,只要操作系統處理好虛擬地址到物理地址的映射關係,就可以保證不同的程序最終訪問不同的區域,從而達到內存地址空間的隔離。

物理地址存在於物理內存中,同理,虛擬地址存在於虛擬地址空間中。要進行數據訪問,必須由操作系統將虛擬地址轉化為物理地址。我們在C語言和C++中看到的地址都是虛擬地址。用戶是看不到物理地址的,物理地址由操作系統統一管理。

不其它的編程語言,C語言沒有內存回收機制,為了避免內存洩露,C++使用了智能指針的機制。

2 C++的動態內存申請

C++new(delete)與malloc(free)的主要區別一是可以自動確認數據類型的內存大小,二是除了分配與釋放內存,還會自動調用構造函數與析構函數。對於數組的操作,需要使用如delete [] stringParr;

double *p, *q, *t;
p new double;
q = new double(1.0); // q指向的單元被初始化為1.0
t = new double[10]; // 為t分配一個長度為10的一維數組;

如果分配失敗,則返回一個空指針;

符號[]是告訴編譯器,我這裡需要釋放的是一個整個數組的指針,這樣編譯器就會逐個釋放每個元素的內存佔用。

使用malloc函數需要指定內存分配的字節數並且不能初始化對象,New會自動調用對象的構造函數。delete會調用對象的destructor,而free不會調用對象的destructor.

同時,malloc需要計算類型的字節數,而new可以自動推算類型的字節數。

new、delete返回的是所分配對象(變量)的指針,而malloc、free返回的是void指針,所以需要做強制類型轉換;

//為簡單變量動態分配內存,並作初始化
#include <iostream.h>
int main()
{
int *p;
p = new int(99);
//動態分配內存,並將99作為初始化值賦給它
cout<< *p;
delete p;
return 0;
}//new 不能初始化動態數組
p = new int(5);//申請單個空間,並賦默認值為5
q = new char[10]; //申請10個空間

用new(或malloc)開闢的內存單元沒有名字,指向其首地址的指針是引用其的唯一途徑,若指針變量重新賦值,則用new(或malloc)開闢的內存單元就在內存中“丟失”了,別的程序也不能佔用這段單元。

當我們在程序中使用new運算符(或malloc)在堆中開闢一個空間之後,在數據使用完後,一定要釋放在堆中開闢的空間,否則會出現內存洩露。需要注意的是,釋放以後並沒有完事,此時的指針並沒有自動置NULL,此時指針指向的就是“垃圾”內存,是一個“野指針”。如果繼續使用,會出現不可預料的錯誤。

因為沒有明確的所屬關係,一些在多個模塊中共亨的資源指針很容易成為“野指針”,從而導致多種內存汸問的問題。首先,因為共享某個內存塊指針的多個模塊並不擁有這個指針,它們只是使用這些指針,並不負責釋放這些指針所指向的內存資源,這可能會導致程序的內存洩露。其次,因為這些指針被多個模塊所共享,這可能會導致某個指針所指向的內存資源巳經被釋放,但是另外的模塊又試圖訪問這個指針所指向的內存資源,最終導致內存訪問錯誤。

為此,C++使用了智能指針的辦法,詳細介紹可見:

C++|智能指針為何智能?

3 動態數組

動態數組是寫程序時不指定長度的數組,它的長度在程序運行時確定。

普通數組聲明時必須指定長度。但有的時候,除非程序實際運行,否則不好確定長度。例如,數組可能容納了一個學生ID 列表,但程序每次運行時,班級的學生數都可能發生變化。沿用傳統做法,就必須預估數組可能的最大長度,並希望那個長度足夠大,能適應所有情況。但這樣做有兩個問題。首先,你估計的長度可能還是太小,造成程序不能適應所有情況。其次,由於數組可能包含許多未使用的位置,所以會浪費計算機內存。動態數組避免了所有這些問題。用動態數組容納學生ID,可在程序運行時輸入班級的學生數。然後,程序會創建剛好那麼大的動態數組。動態數組用操作符new 創建。和許多人想象的不同,動態數組的創建和使用其實非常簡單。由於數組變量其實就是指針變量,所以可以用操作符new 創建被用作數組的動態變量,並像使用普通數組那樣使用動態數組變量。例如,以下語句創建一個動態數組變量,其中含有10 個double 類型的數組元素:

double* p;
p = new double[10];

由於程序馬上就要終止,所以並不是真的需要這個delete 語句。但如果程序準備用動態變量做其他事情,就應該包括這個delete 語句,將動態數組佔用的內存還給自由存儲。為動態數組執行的delete 語句類似於以前介紹過的delete 語句,只是在動態數組的情況下,必須包括一對空的方括號,如下所示:

delete [] a;

方括號告訴C++要銷燬的是動態數組變量,所以系統會檢查數組的長度,並刪除剛好那麼多的索引變量。如省略方括號,相當於告訴計算機只銷毀一個int 類型的變量。例如:

delete a;

雖然上述語句不合法,但大多數編譯器都檢測不到這個錯誤。ANSI C++標準規定,這種情況下發生的事情是“未定義”的,意味著編譯器的作者可以做他覺得方便的任何事情——

相對於上述的靜態數組,動態數組在運行時使用malloc()或new來申請需求數量的內存空間。

首先是聲明一個指針變量,然後用這個指針變量指向內存中的動態數組,並被用作動態數組名稱:double * a;

調用new 使用操作符new 創建一個動態數組:a = new double[arraySize];動態數組長度在方括號內給出。可用int 變量或其他int 表達式給出長度。arraySize 可以是值在運行才確定的int 變量。

動態數組可以和普通數組一樣使用,指針變量(比如a)可以像普通數組那樣使用。例如,可採用標準方式書寫索引變量,比如a[0],a[1],等等。不能再為指針變量賦其他任何指針值。相反,它應該像數組變量那樣使用。

調用delete [] 用完動態數組後使用delete、一對空的方括號和指針變量來銷燬動態數組,將其佔用的內存還給自由存儲以便重用。例如:delete [] a;

一個數組是一段連續的內存空間的命名,一個按上述流程在堆中申請的也是一段連續的內存空間,並也被賦值給了一個指針變量。
指針是內存地址,變量可以對計算機內存中的地址進行命名,而指針提供了一種間接的變量命名方式。
動態變量是程序運行時創建(和銷燬)的變量。動態數組是其長度在程序運行時確定的數組。動態數組被實現為數組類型的動態變量。
動態變量要佔用計算機內存的一個特殊區域,這個區域稱為自由存儲。程序結束動態變量的使用後,應該將動態變量佔用的內存還給自由存儲,以便重用;這是用delete 語句來完成的。

4 向量

向量用法類似於數組,但向量長度不固定。如需更大容量來存儲更多元素,它的容量就會自動擴充。向量在庫中定義,位於std 命名空間。所以,在程序中使用向量需包括以下語句(或其他類似語句):

#include <vector>
using namespace std;

給定了Base_Type(基類型)的向量類要寫成vector。下面是兩個示範向量聲明:

vector v; // 默認構造函數生成一個空向量
vector record(20); //用AClass 類的默認構造函數初始化20 個元素

在向量添加元素要用成員函數push_back,如下例所示:

v.push_back(42);

一個元素位置獲得了它的第一個值之後(無論是通過push_back,還是通過構造函數來初始化),以後就可使用方括號記號法來訪問那個元素位置,和訪問普通數組元素一樣。

向量在任何時候都有一個容量,即當前分配了內存的元素的數量。成員函數capacity()返回向量的容量。不要混淆向量的容量和長度。長度是向量中元素的個數,容量是實際分配了內存的元素的個數。容量通常大於長度,且肯定大於或等於長度。

一旦向量容量不夠,並需要空間來容納一個附加成員,容量就會自動增加。不同C++實現對每次增加的容量有不同規定,但通常都會超過馬上就要用到的容量。一個常見的方案是倍增當前需要的容量。由於增加容量是一項複雜的任務,所以相較於頻繁分配許多小的內存塊,一次性分配一個較大的內存塊顯得更有效率。

向量的長度是指向量中的元素個數,容量是當前實際分配了內存的元素的個數。對於向量v,可分別用成員函數v.size()和v.capacity()判斷其長度和容量。

一般可忽略向量的容量,這不會對程序行為產生任何影響。但如果必須考慮效率問題,可考慮自己管理容量,而不是接受每次都使容量倍增的默認行為。可用成員函數reserve來顯式增大向量的容量。例如以下語句將容量設為至少32 個元素:

v.reserve(32);

以下語句將容量設為向量當前元素個數加10:

v.reserve(v.size() + 10);

-End-

相關推薦

推薦中...