1. 程式人生 > >C++動態記憶體

C++動態記憶體

1、C++ 動態記憶體

動態記憶體在 C++ 中是必不可少的。C++ 程式中的記憶體主要分為兩個部分

(注:在c或C++語言中,編譯器將記憶體分為四區,稱之為“記憶體四區”模型,即:棧區、堆區、全域性區和程式碼區。在這裡,我們主要關注與使用者密切相關的堆區和棧區。)

  • 棧區:函式內部宣告的所有變數都將佔用棧記憶體。(“棧區”不是“棧”,而是編譯器和作業系統依照類似於棧的操作方式來對部分記憶體空間進行操作和管理。)
  • 堆區:這是程式中未使用的記憶體,在程式執行時可用於動態分配記憶體。(堆區一般有程式設計師分配釋放,分配方式類似於連結串列。)
  • 棧(資料結構):一種先進後出的資料結構。
  • 堆(資料結構)
    :隊可以被看做是一棵樹,如:堆排序等

    很多時候,無法提前預知需要多少記憶體來儲存某個定義變數中的特定資訊,所需記憶體的大小需要在執行時才能確定。

在 C++ 中,可以使用特殊的運算子為給定型別的變數在執行時分配堆內的記憶體,這會返回所分配的空間地址。這種運算子即 new 運算子。

如果不再需要動態分配的記憶體空間,可以使用 delete 運算子,刪除之前由 new 運算子分配的記憶體。

2、new 和 delete 運算子

下面是使用 new 運算子來為任意的資料型別動態分配記憶體的通用語法:

new data-type;

在這裡,data-type 可以是包括陣列在內的任意內建的資料型別,也可以是包括類或結構在內的使用者自定義的任何資料型別。讓我們先來看下內建的資料型別。例如,我們可以定義一個指向 double 型別的指標,然後請求記憶體,該記憶體在執行時被分配。我們可以按照下面的語句使用 new

運算子來完成這點:

double* pvalue = NULL; // 初始化為 null 的指標 pvalue = new double; // 為變數請求記憶體

如果自由儲存區已被用完,可能無法成功分配記憶體。所以建議檢查 new 運算子是否返回 NULL 指標,並採取以下適當的操作:

double* pvalue = NULL; 
if( !(pvalue = new double )) { 
    cout << "Error: out of memory." <<endl; exit(1); 
}

2.1 malloc() 函式:

在 C 語言中就出現了,在 C++ 中仍然存在,但建議儘量不要使用 malloc() 函式。new 與 malloc() 函式相比,其主要的優點
是,new 不只是分配了記憶體,它還建立了物件。

在任何時候,當您覺得某個已經動態分配記憶體的變數不再需要使用時,您可以使用 delete 操作符釋放它所佔用的記憶體,如下所示:

delete pvalue;        // 釋放 pvalue 所指向的記憶體

下面的例項中使用了上面的概念,演示瞭如何使用 new 和 delete 運算子:

例項

#include <iostream>
using namespace std;
 
int main ()
{
   double* pvalue  = NULL; // 初始化為 null 的指標
   pvalue  = new double;   // 為變數請求記憶體
 
   *pvalue = 29494.99;     // 在分配的地址儲存值
   cout << "Value of pvalue : " << *pvalue << endl;
 
   delete pvalue;         // 釋放記憶體
 
   return 0;
}

當上面的程式碼被編譯和執行時,它會產生下列結果:

Value of pvalue : 29495

2.3 陣列的動態記憶體分配

假設我們要為一個字元陣列(一個有 20 個字元的字串)分配記憶體,我們可以使用上面例項中的語法來為陣列動態地分配記憶體,如下所示:

char* pvalue  = NULL;   // 初始化為 null 的指標
pvalue  = new char[20]; // 為變數請求記憶體

要刪除我們剛才建立的陣列,語句如下:

delete [] pvalue;        // 刪除 pvalue 所指向的陣列

下面是 new 操作符的通用語法,可以為多維陣列分配記憶體,如下所示:

2.4 一維陣列

// 動態分配,陣列長度為 m 
int *array=new int [m]; 
//釋放記憶體 
delete [] array;

2.5 二維陣列

二維陣列例項測試

int **array
// 假定陣列第一維長度為 m, 第二維長度為 n
// 動態分配空間
array = new int *[m];
for( int i=0; i<m; i++ )
{
    array[i] = new int [n]  ;
}
//釋放
for( int i=0; i<m; i++ )
{
    delete [] arrar[i];
}
delete [] array;
#include <iostream>
using namespace std;
 
int main()
{
    int **p;   
    int i,j;   //p[4][8] 
    //開始分配4行8列的二維資料   
    p = new int *[4];
    for(i=0;i<4;i++){
        p[i]=new int [8];
    }
 
    for(i=0; i<4; i++){
        for(j=0; j<8; j++){
            p[i][j] = j*i;
        }
    }   
    //列印資料   
    for(i=0; i<4; i++){
        for(j=0; j<8; j++)     
        {   
            if(j==0) cout<<endl;   
            cout<<p[i][j]<<"\t";   
        }
    }   
    //開始釋放申請的堆   
    for(i=0; i<4; i++){
        delete [] p[i];   
    }
    delete [] p;   
    return 0;
}

例項

三維陣列

int ***array;
// 假定陣列第一維為 m, 第二維為 n, 第三維為h
// 動態分配空間
array = new int **[m];
for( int i=0; i<m; i++ )
{
    array[i] = new int *[n];
    for( int j=0; j<n; j++ )
    {
        array[i][j] = new int [h];
    }
}
//釋放
for( int i=0; i<m; i++ )
{
    for( int j=0; j<n; j++ )
    {
        delete array[i][j];
    }
    delete array[i];
}
delete [] array;

三維陣列測試例項:

#include <iostream>
using namespace std;
 
int main()
{   
    int i,j,k;   // p[2][3][4]
    
    int ***p;
    p = new int **[2]; 
    for(i=0; i<2; i++) 
    { 
        p[i]=new int *[3]; 
        for(j=0; j<3; j++) 
            p[i][j]=new int[4]; 
    }
    
    //輸出 p[i][j][k] 三維資料
    for(i=0; i<2; i++)   
    {
        for(j=0; j<3; j++)   
        { 
            for(k=0;k<4;k++)
            { 
                p[i][j][k]=i+j+k;
                cout<<p[i][j][k]<<" ";
            }
            cout<<endl;
        }
        cout<<endl;
    }
    
    // 釋放記憶體
    for(i=0; i<2; i++) 
    {
        for(j=0; j<3; j++) 
        {   
            delete [] p[i][j];   
        }   
    }       
    for(i=0; i<2; i++)   
    {       
        delete [] p[i];   
    }   
    delete [] p;  
    return 0;
}

2.6 物件的動態記憶體分配

物件與簡單的資料型別沒有什麼不同。例如,使用一個物件陣列來理清這一概念:

例項

#include <iostream>
using namespace std;
 
class Box
{
   public:
      Box() { 
         cout << "呼叫建構函式!" <<endl; 
      }
      ~Box() { 
         cout << "呼叫解構函式!" <<endl; 
      }
};
 
int main( )
{
   Box* myBoxArray = new Box[4];
 
   delete [] myBoxArray; // 刪除陣列
   return 0;
}

如果要為一個包含四個 Box 物件的陣列分配記憶體,建構函式將被呼叫 4 次,同樣地,當刪除這些物件時,解構函式也將被呼叫相同的次數(4次)。

當上面的程式碼被編譯和執行時,它會產生下列結果:

呼叫建構函式!
呼叫建構函式!
呼叫建構函式!
呼叫建構函式!
呼叫解構函式!
呼叫解構函式!
呼叫解構函式!
呼叫解構函式!

3. 總結

3. 1 delete 與 delete[] 區別:

1、針對簡單型別 ,使用 new 分配後的不管是陣列還是非陣列形式記憶體空間用兩種方式均可 如:

int *a = new int[10];   
delete a;   
delete [] a; 

此種情況中的釋放效果相同,原因在於:分配簡單型別記憶體時,記憶體大小已經確定,系統可以記憶並且進行管理,在析構時,系統並不會呼叫解構函式, 它直接通過指標可以獲取實際分配的記憶體空間,哪怕是一個數組記憶體空間(在分配過程中 系統會記錄分配記憶體的大小等資訊,此資訊儲存在結構體_CrtMemBlockHeader中, 具體情況可參看VC安裝目錄下CRT\SRC\DBGDEL.cpp)

2、針對類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[10];

// 僅釋放了a指標指向的全部記憶體空間 但是隻呼叫了a[0]物件的解構函式 剩下的從a[1]到a[9]這9個使用者自行分配的m_cBuffer對應記憶體空間將不能釋放 從而造成記憶體洩漏
delete a;

// 呼叫使用類物件的解構函式釋放使用者自己分配記憶體空間並且   釋放了a指標指向的全部記憶體空間
delete [] a;

所以總結下就是,如果pointer代表一個用new申請的記憶體返回的記憶體空間地址,即所謂的指標,那麼:

  •  delete pointer -- 代表用來釋放記憶體,且只用來釋放p指向的記憶體。
  •  delete[] pointer -- 用來釋放pointer指向的記憶體,!!還逐一呼叫陣列中每個物件的 destructor!!

對於像 int/char/long/int*/struct 等等簡單資料型別,由於物件沒有 destructor,所以用 delete 和 delete [] 是一樣的!但是如果是C++ 物件陣列就不同了!

3.2 利用動態記憶體, 也可以做出連結串列, 以及不斷增長的陣列:

#include <iostream>
#include <cstdio>

using namespace std;

struct node()
{//連結串列的節點
      int data = 0;//資料
      int num = 0;//節點編號
      struct node *next = NULL;//指向下一個節點
};

int main()
{
    struct node *head/*頭節點*/, *p, *q;
    head=NULL;
    p=NULL;
    q=new node;
    q->next=NULL;
    q->num=1;
    int a=-1;
    cout<<"請輸入第1個數字:";
    cin>>a;
    q->data=a;
    head=q;
    while (a!=0)
    {
         p=q;
         q=new node;
         q->next=NULL;
         p->next=q;
         q->num=p->num+1;
         cout<<"請輸入第"<<q->num<<"個數字:";
         cin>>a;
         q->data=a;
    }

    //前面都是輸入,以下都是輸出

    q=head;
    p=NULL;
    while (q->data!=0)
    {
         printf("%d %d\n",q->num,q->data);
         q=q->next;
    }
    
    //釋放記憶體

    q=head;
    p=q;
    while(q->next!=NULL)
    {
        p=q->next;
        delete []q;
        q = p;  
    }
    return 0;
}

用指標訪問結構體內的變數。

在連結串列中插入、刪除節點也很簡單, 先給next賦下一個節點地址,再加資料即可。

3.3 new 和 malloc 內部的實現方式有什麼區別?

new 的功能是在堆區新建一個物件,並返回該物件的指標。

所謂的【新建物件】的意思就是,將呼叫該類的建構函式,因為如果不構造的話,就不能稱之為一個物件。

malloc 只是機械的分配一塊記憶體,如果用 malloc 在堆區建立一個物件的話,是不會呼叫建構函式的。

嚴格說來用 malloc 不能算是新建了一個物件,只能說是分配了一塊與該類物件匹配的記憶體而已,然後強行把它解釋為【這是一個物件】,按這個邏輯來,也不存在建構函式什麼事。

同樣的,用 delete 釋放一個堆區的物件,會呼叫該物件的解構函式

free 去釋放一個堆區的物件,不會呼叫該物件的解構函式。

做個簡單的實驗即可明瞭:

#include <iostream>
#include <malloc.h>

class TEST
{
private:
    int num1;
    int num2;
public:
    TEST()
    {
        num1 = 10;
        num2 = 20;
    }
    void Print()
    {
        std::cout << num1 << " " << num2 << std::endl;
    }
};

int main(void)
{
    // 用malloc()函式在堆區分配一塊記憶體空間,然後用強制型別轉換將該塊記憶體空間
    // 解釋為是一個TEST類物件,這不會呼叫TEST的預設建構函式
    TEST * pObj1 = (TEST *)malloc(sizeof(TEST));
    pObj1->Print();

    // 用new在堆區建立一個TEST類的物件,這會呼叫TEST類的預設建構函式
    TEST * pObj2 = new TEST;
    pObj2->Print();

    return 0;
}
/*
執行結果:

-----------------------------
-842150451 -842150451       |
10 20                       |
請按任意鍵繼續. . .         |
-----------------------------

我們可以看到pObj1所指的物件中,欄位num1與num2都是垃圾值
而pObj2所指的物件中,欄位num1與num2顯然是經過了構造後的值
*/