天天看點

C++:記憶體管理:C++記憶體管理詳解一:記憶體二:malloc和free使用三:new 和delete 

C++語言記憶體管理是指:對系統的配置設定、建立、使用這一系列操作。在記憶體管理中,由于是作業系統記憶體,使用不當會造成很麻煩的後果。本文将從系統記憶體的配置設定、建立出發,并且結合例子來說明記憶體管理不當會造成的結果以及解決方案。

一:記憶體

在計算機中,每個應用程式之間的記憶體是互相獨立的,通常情況下應用程式A并不能通路應用程式B,當然我們可以使用一種特殊技術,讓他們可以互相通路,但這個不是本文的重點。比如在計算機中,一個視訊播放程式與一個浏覽器程式,他們的記憶體并不能通路,每個程式所擁有的記憶體是分區進行管理的。

在計算機系統中,運作程式A将會在記憶體中開啟程式A的記憶體區域1,運作程式B将會在記憶體中開辟程式B的記憶體區域2,記憶體1和記憶體2之間邏輯分離。

C++:記憶體管理:C++記憶體管理詳解一:記憶體二:malloc和free使用三:new 和delete 

 1.1 記憶體四個區

在程式A開辟的記憶體區域1會被分為幾個區域,這就是記憶體四區,記憶體四區分為棧區、堆區、資料區和代碼區。

C++:記憶體管理:C++記憶體管理詳解一:記憶體二:malloc和free使用三:new 和delete 

棧區:指的是存儲一些臨時變量的區域,臨時變量包括了局部變量、傳回值、參數、傳回位址等,當這些變量超出了目前作用域時将會自動彈出。該棧的最大存儲是有大小的且固定的,超過該代銷将會造成棧溢出。

堆區:是一個比較大的記憶體空間,主要用于對動态記憶體的配置設定,在程式開發中一般是開發人員進行配置設定和釋放,若在程式結束時都還未釋放,系統将自動進行回收。

資料區:主要存放全局變量、常量、靜态變量,資料區又可以劃分為:全局區和靜态區,全局變量和靜态變量将會存放到該區域。

代碼區:比較簡單,主要存儲可執行代碼,該區域的屬性是隻讀的。

1.2 通過代碼證明記憶體四區的底層結構

棧區

由于棧區和堆區的底層結構比較直覺的表現,我們在此使用代碼隻示範這兩個概念來檢視代碼觀察棧區的記憶體位址配置設定情況。

// main.cpp
#include<iostream>
int main() {
	int a = 0;
	int b = 0;
	int c = 0;
	printf("變量a的位址: %d", &a);
	printf("\n變量b的位址: %d", &b);
	printf("\n變量c的位址: %d", &c);

	// 列印結果

}
           
C++:記憶體管理:C++記憶體管理詳解一:記憶體二:malloc和free使用三:new 和delete 

 為什麼它們之間的位址不是增加而是減少呢?那是因為棧區的一種資料存儲結構為先進後出,如圖:

C++:記憶體管理:C++記憶體管理詳解一:記憶體二:malloc和free使用三:new 和delete 
  1. 首先棧的頂部為位址的“最小”索引,随後往下依次增大,但是由于堆棧的特殊結果,我們将變量a先進行存儲,那麼它的一個索引位址将會是最大的,随後依次減少。(即 6422300)
  2. 第二次存儲的變量時b,該值得位址索引比a小4個位元組(因為int類型在32位系統中占4個位元組),是以在a位址的基礎上減少4個位元組(即:6422296)
  3. 第三次存儲變量c,該值得位址索引比b小4個位元組(即:6422292)
  4. 第三次存儲變量d,該值得位址索引比c小1個位元組(即:6422291),因為char字元占一個位元組
  5. 由于a、b、c、d這四個變量同屬一一個棧内,是以他們位址的索引是連續性的,但是如果我們建立一個靜态變量将會是怎樣的了 ?

思考:在配置設定記憶體的時候,為什麼 定義int 類型,記憶體大小相差4位元組,而定義char類型記憶體相差1位元組

要回答上面這個問題,我們首先要了解的就是:“跳躍力”。

對于一個指針 而言,如果前面規定了它的類型,那就相當于決定了它的“跳躍力”。“跳躍力”就比如說上圖中  int類型跳了 4個位元組,char類型跳了1位元組,double類型跳了8位元組。

C++:記憶體管理:C++記憶體管理詳解一:記憶體二:malloc和free使用三:new 和delete 

資料區

我們定義一個全局變量和 靜态變量看看效果

#include<iostream>
int e = 0;
int main() {
	static int f = 0;
	printf("\n var e address: %d", &e);
	printf("\nvar f address: %d", &f);

}

// 列印結果
 var e address: 8430692
 var f address: 8430696
           

從這個例子可以看出,資料區的位址不是棧結構,它是由低向高配置設定的,這裡還是和棧區有點差別的。

以上例子其實也說明了棧區的特性,就是容量具有固定大小,超過最大容量将會造成棧溢出

#include<iostream>

int main() {
	char char_arr[1024 * 1000000000];
	char_arr[0] = 'a';
}

// 編譯之後直接會報錯
           
C++:記憶體管理:C++記憶體管理詳解一:記憶體二:malloc和free使用三:new 和delete 

 1.3 :為了解決上面這個問題(開辟大容量記憶體)

堆并沒有棧一樣的結構,也沒有棧一樣的先進後出。我們需要人為的對記憶體進行配置設定使用。

#include<iostream>
#include<string>

int main() {

	//定義一個指針:指向一個大容量的記憶體位址
	char* p1 = (char*)malloc(1024 * 1000);

	// 把字元串"here is heap"指派到指針 p1指向的空間中
	std::string s = "here is heap";
	strcpy_s(p1, 1024,s.c_str());

	//列印記憶體空間的位址
	printf("p1 storage address: %d", p1);
	printf("\np1 storage: %s", p1);
}


//列印結果

p1 storage address: 19443776
p1 storage: here is heap
           

多說一點

char *p=new char[10];

p="hello";

delete p[];//此步釋放時将報錯❌

這是因為:在C++中,字元串常量"hello"被儲存在常量存儲區,而p="hello"操作是改變了指針的指向,使得指針p指向了常量存儲區的"hello",造成了初始在堆上開辟的記憶體洩露,而delete無法釋放常量存儲區的記憶體,導緻出錯。

二:malloc和free使用

在 C 語言(不是 C++)中,malloc 和 free 是系統提供的函數,成對使用,用于從堆中配置設定和釋放記憶體。malloc 的全稱是 memory allocation 譯為“動态記憶體配置設定”。

2.1 malloc和free

在開辟堆空間時我們使用的函數為 malloc,malloc 在 C 語言中是用于申請記憶體空間,malloc 函數的原型如下:

void *malloc(size_t size);
  •  在 malloc 函數中,size 是表示需要申請的記憶體空間大小,申請成功将會傳回該記憶體空間的位址;申請失敗則會傳回 NULL,并且申請成功也不會自動進行初始化。
  • 細心的同學可能會發現,該函數的傳回值說明為 void *,在這裡 void * 并不指代某一種特定的類型,而是說明該類型不确定,通過接收的指針變量進而進行類型的轉換。
  • 在配置設定記憶體時需要注意,即時在程式關閉時系統會自動回收該手動申請的記憶體 ,但也要進行手動的釋放,保證記憶體能夠在不需要時傳回至堆空間,使記憶體能夠合理的配置設定使用。

釋放空間使用free函數原型如下: 

void  free(void *ptr); 

free函數的傳回值是void ,沒有傳回值,接收的參數為使用malloc配置設定的記憶體空間指針

下面看一下一個完整的堆記憶體申請與釋放的例子:

// main.cpp

int main(){

    // 下面是一個完整申請,釋放記憶體的例子,可以參考一下。
	int n, *p, i;
	printf("請輸入一個任意長度的數字來配置設定空間::");
	scanf_s("%d", &n);
	p = (int*)malloc(n * sizeof(int));
	if (p ==NULL)
	{
		printf("記憶體申請失敗");
		return 0;
	}
	else
	{
		printf("記憶體申請成功");
	}
	// 使用 memset對記憶體空間進行填充
	memset(p, 0, n * sizeof(int));

	// 檢視剛剛填充的内容
	for (int i = 0; i < n; i++)
	{
		printf("\n%d", p[i]);
	}
	// 釋放空間
	free(p);
	p = NULL;
	return 0;
}
           

分析上述代碼可知:

  •         使用malloc建立了一個由使用者輸入建立指定大小的記憶體,判斷了記憶體位址是否建立成功,且使用 memset函數對該記憶體空間進行了填充,然後通過for循環來檢視
  •        最後使用 free()函數釋放記憶體,并且将 p指派為 NULL ,這一點要注意不能讓指針指向未知的位址,要至于NULL(否則就是野指針)

2.2 記憶體洩漏與安全使用執行個體講解

  1. 記憶體洩漏是指在動态配置設定的記憶體中,并沒有釋放記憶體或者一些其他原因造成了記憶體無法釋放,輕則會造成系統記憶體資源浪費,嚴重會導緻整個系統崩潰等情況的發生。
  2. 記憶體洩漏通常比較隐蔽,且少量的記憶體洩漏發生也不一定會有無法承受的後果,但由于該錯誤的積累将會造成整體系統的性能下降或系統崩潰,特别是在大型的系統中,如何有效的防止記憶體洩漏等問題的出現就變得尤其重要,例如一些長時間的程式,若在運作之初有少量的記憶體洩漏的問題産生可能并未呈現,但随着運作時間的增長、系統業務處理的增加将會累積出現記憶體洩漏這種情況;這時極大的會造成不可預知的後果,如整個系統的崩潰,造成的損失将會難以承受。由此防止記憶體洩漏對于底層開發人員來說尤為重要。
  3. C 程式員在開發過程中,不可避免的面對記憶體操作的問題,特别是頻繁的申請動态記憶體時會及其容易造成記憶體洩漏事故的發生。如申請了一塊記憶體空間後,未初始化便讀其中的内容、間接申請動态記憶體但并沒有進行釋放、釋放完一塊動态申請的記憶體後繼續引用該記憶體内容;如上所述這種問題都是出現記憶體洩漏的原因,往往這些原因由于過于隐蔽在測試時不一定會完全清楚,将會導緻在項目上線後的長時間運作下,導緻災難性的後果發生。
// main.cpp

int main(){

    char* p1;
	p1 = (char *)malloc(100);
	printf("記憶體開始被洩漏了");

	return 0;

}
           

三:new 和delete 

  • C++中使用new 和delete從堆中配置設定和釋放記憶體,new 和delete是運算符,不是函數,兩者需要成對使用。
  • new/delete 除了配置設定記憶體和釋放記憶體 與malloc/free相比,還做了其他很多事情,是以在C++中不再使用malloc/free ,建議使用 new/delete。

3.1 new和delete使用

new 一般使用格式:

指針變量名 = new 類型辨別符

指針變量名 = new 類型辨別符(初始值)

指針變量名 = new類型辨別符【記憶體單元個數】

// main.cpp

char * getMemory(unsigned long size) {

    char *p = new char[size];
    return p;
}

int main(){

    try {

        // 有可能會發生異常
        char *p = getMemory(1000000);

        // ...........
        delete[] p;
    }
    catch(const std::bad_alloc &amp;ex) {

        cout &lt;&lt; ex.what();
    }
}


plain new 在配置設定失敗的情況下,抛出異常 std::bad_alloc 而不是傳回 NULL,
是以通過判斷傳回值是否為 NULL 是徒勞的。
           

3.2 delete和delete[]的差別

C++中對new申請的記憶體的釋放方式有 delete和delete[]兩種方式,那麼這兩種方式有什麼差別了 ?

  1. delete 釋放new配置設定的單個對象指針指向的記憶體。
  2. delete[] 釋放new配置設定的對象數組指針指向的記憶體
  3. 為基本資料類型配置設定和回收空間;
  4. 為自定義類型配置設定和回收空間;

現在我們針對基本資料類型分析 delete/delete[] 

int *a = new int[10];

delete a;

delete[] a;

  •  此種情況中的釋放效果是相同的,原因在于:配置設定基本資料類型記憶體時,記憶體大小已經确定,系統可以記憶并且進行管理,在析構時,系統并不會調用析構函數。
  • 它直接通過指針可以擷取實際配置設定的記憶體空間,哪怕是一個數組記憶體空間(注意:在配置設定過程中系統會記錄配置設定記憶體的大小資訊,此資訊儲存在結構體 _CrtMemBlockHeader中,具體情況可以參考VC安裝目錄下CRTSRCDBGDEL.cpp)

針對Class類型,他們的差異展現在

class A
{

    private:
        char *m_cBuffer;
        int m_nLen;

    public:
        A(){m_cBuffer = new char[m_nLen];}
       
        ~A{ delete []m_cBuffer ;}

}

A *a = new A[100];

// 僅釋放了a指針指向的全部記憶體空間 但是隻調用了a[0]對象的析構函數 剩下的從a[1]到a[9]這9個使用者自行配置設定的m_cBuffer對應記憶體空間将不能釋放 進而造成記憶體洩漏.
delete a;

//調用使用類對象的析構函數釋放使用者自己配置設定記憶體空間并且   釋放了a指針指向的全部記憶體空間
delete a[]
           

總結:

     如果ptr代表一個用new 申請的記憶體傳回的記憶體空間位址,即所謂的指針,那麼:

     delete ptr 代表用來釋放記憶體,且隻用來釋放ptr指向的記憶體。

     delete[] ptr 用來釋放ptr指向記憶體,同時還逐一調用數組每個對象的 destructor 。

     對于像 int/ char/ long/int */ struct 等簡單的資料類型,由于對象沒有 析構函數(destructor)是以使用 delete或者 delete[] 是一樣的,但是如果是C++對象數組就不同了。

下面我們來證看一個例子:學習delete 和 delete[] 使用方法

#include<iostream>
#include<string>

class Babe
{
public:
	Babe() 
	{
		std::cout<< "Create a Babe" <<std::endl;
	
	}
	~Babe()
	{
		std::cout << "Babe was destructored" << std::endl;
	}

private:

};


int main(){
    Babe* pBabe = new Babe[3];
	delete pBabe;

	Babe* pBabe1 = new Babe[10];
	delete[] pBabe1;
	return 0;
}

// 列印結果:
pBabe:
    Create a Babe
    Create a Babe
    Create a Babe
    Babe was destructored

pBabe1:
    Create a Babe
    Create a Babe
    Create a Babe
    Create a Babe
    Create a Babe
    Create a Babe
    Create a Babe
    Create a Babe
    Create a Babe
    Create a Babe
    Babe was destructored
    Babe was destructored
    Babe was destructored
    Babe was destructored
    Babe was destructored
    Babe was destructored
    Babe was destructored
    Babe was destructored
    Babe was destructored
    Babe was destructored
           

可以看到:

  1.  隻使用delete的時候隻出現了一個 Babe was desstructor ,而使用delete[] 的時候出現了10 個 Babe was desstructor,。
  2. 不過不管是delete 還是delete[]  ,他們的對象在記憶體中均被删除,存儲位置均标記為可寫。
  3. 但是使用delete的時候 隻調用了 pbabe[0] 的析構函數,使用delete[] 則調用了10個 Babe對象的析構函數,那麼調用多少個析構函數有什麼差別了 ?

析構函數特點:

  1.       如果沒有使用作業系統的系統資源(比如:Socket、File、Thread等),不會造成明顯惡果。
  2.       如果使用了作業系統的系統資源,如果單純隻是把類的對象從記憶體中删除是不妥當的,因為沒有調用對象的析構函數會導緻系統資源部被釋放,這些資源的釋放必須依靠這些類的析構函數,是以在這些類生成對象數組的時候,最好使用 delete[]來釋放

未完待續:看完這篇你還能不懂C語言/C++記憶體管理?

 把記憶體管理了解好,C語言真的不難學。今天帶你“攻破”記憶體管理

繼續閱讀