[UE4]C++ 動態內存分配(6種情況,好幾個例子)


1.堆內存分配 :

C/C++定義了4個內存區間:

    代碼區,全局變量與靜態變量區,局部變量區即棧區,動態存儲區,即堆(heap)區或自由存儲區(free store)。

堆的概念:

通常定義變量(或對象),編譯器在編譯時都可以根據該變量(或對象)的類型知道所需內存空間的大小,從而系統在適當的時候為他們分配確定的存儲空間。這種內存分配稱為靜態存儲分配;

    有些操作對象只在程序運行時才能確定,這樣編譯時就無法為他們預定存儲空間,只能在程序運行時,系統根據運行時的要求進行內存分配,這種方法稱為動態存儲分配。所有動態存儲分配都在堆區中進行。

當程序運行到需要一個動態分配的變量或對象時,必須向系統申請取得堆中的一塊所需大小的存貯空間,用於存貯該變量或對象。當不再使用該變量或對象時,也就是它的生命結束時,要顯式釋放它所占用的存貯空間,這樣系統就能對該堆空間進行再次分配,做到重復使用有限的資源。

 

2.堆內存的分配與釋放

堆空間申請、釋放的方法:

在C++中,申請和釋放堆中分配的存貯空間,分別使用new和delete的兩個運算符來完成:   


指針變量名=new 類型名(初始化式);

         delete 指針名;

例如:

  1、 int *pi=new int(0);

      它與下列代碼序列大體等價:

      2、int ival=0, *pi=&ival;

區別:pi所指向的變量是由庫操作符new()分配的,位於程序的堆區中,並且該對象未命名。  

 

堆空間申請、釋放說明:

⑴.new運算符返回的是一個指向所分配類型變量(對象)的指針。對所創建的變量或對象,都是通過該指針來間接操作的,而且動態創建的對象本身沒有名字。

⑵.一般定義變量和對象時要用標識符命名,稱命名對象,而動態的稱無名對象(請注意與棧區中的臨時對象的區別,兩者完全不同:生命期不同,操作方法不同,臨時變量對程序員是透明的)。

⑶.堆區是不會在分配時做自動初始化的(包括清零),所以必須用初始化式(initializer)來顯式初始化。new表達式的操作序列如下:從堆區分配對象,然后用括號中的值初始化該對象。

 

3.堆空間申請、釋放演示:

⑴.用初始化式(initializer)來顯式初始化

  int *pi=new int(0);

⑵.當pi生命周期結束時,必須釋放pi所指向的目標:

         delete pi;

注意這時釋放了pi所指的目標的內存空間,也就是撤銷了該目標,稱動態內存釋放(dynamic memory deallocation),但指針pi本身並沒有撤銷,它自己仍然存在,該指針所占內存空間並未釋放。

下面是關於new 操作的說明

⑴.new運算符返回的是一個指向所分配類型變量(對象)的指針。對所創建的變量或對象,都是通過該指針來間接操作的,而動態創建的對象本身沒有名字。
   ⑵.一般定義變量和對象時要用標識符命名,稱命名對象,而動態的稱無名對象(請注意與棧區中的臨時對象的區別,兩者完全不同:生命期不同,操作方法不同,臨時變量對程序員是透明的)。

⑶.堆區是不會在分配時做自動初始化的(包括清零),所以必須用初始化式(initializer)來顯式初始化。new表達式的操作序列如下:從堆區分配對象,然后用括號中的值初始化該對象。

 
4. 在堆中建立動態一維數組

①申請數組空間:

指針變量名=new 類型名[下標表達式];

注意:“下標表達式”不是常量表達式,即它的值不必在編譯時確定,可以在運行時確定。

②釋放數組空間:

delete [ ]指向該數組的指針變量名;

注意:方括號非常重要的,如果delete語句中少了方括號,因編譯器認為該指針是指向數組第一個元素的,會產生回收不徹底的問題(只回收了第一個元素所占空間),加了方括號后就轉化為指向數組的指針,回收整個數組。delete [ ]的方括號中不需要填數組元素數,系統自知。即使寫了,編譯器也忽略。

#include <iostream.h>

#include <string.h>

void main(){
     int n;
     char *pc;
     cout<<"請輸入動態數組的元素個數"<<endl;
     cin>>n; //n在運行時確定,可輸入17
     pc=new char[n]; //申請17個字符(可裝8個漢字和一個結束符)的內存空間
     strcpy(pc,“堆內存的動態分配”);//
     cout<<pc<<endl;
     delete []pc;//釋放pc所指向的n個字符的內存空間
     return ;
} 

5. 動態一維數組的說明

① 變量n在編譯時沒有確定的值,而是在運行中輸入,按運行時所需分配堆空間,這一點是動態分配的優點,可克服數組“大開小用”的弊端,在表、排序與查找中的算法,若用動態數組,通用性更佳。一定注意:delete []pc是將n個字符的空間釋放,而用delete pc則只釋放了一個字符的空間;

② 如果有一個char *pc1,令pc1=p,同樣可用delete [] pc1來釋放該空間。盡管C++不對數組作邊界檢查,但在堆空間分配時,對數組分配空間大小是紀錄在案的。

③ 沒有初始化式(initializer),不可對數組初始化。


6.指針數組和數組指針

指針類型:

(1)int*ptr;    //指針所指向的類型是int
(2)char*ptr;    //指針所指向的的類型是char
(3)int**ptr;    //指針所指向的的類型是int* (也就是一個int * 型指針)
(4)int(*ptr)[3];  //指針所指向的的類型是int()[3] //二維指針的聲明

指針數組:
一個數組里存放的都是同一個類型的指針,通常我們把他叫做指針數組。

比如 int * a[2];它里邊放了2個int * 型變量 .

int * a[2];
a[0]= new int[3];
a[1]=new int[3];
delete a[0];
delete a[1];

注意這里 是一個數組,不能delete [] ;

 

數組指針:

 一個指向一維或者多維數組的指針.

int * b=new int[10];

指向一維數組的指針b ;

注意,這個時候釋放空間一定要delete [] ,否則會造成內存泄露, b 就成為了空懸指針

int (*b2)[10]=new int[10][10];

注意,這里的b2指向了一個二維int型數組的首地址.

注意:在這里,b2等效於二維數組名,但沒有指出其邊界,即最高維的元素數量,但是它的最低維數的元素數量必須要指定!就像指向字符的指針,即等效一個字符串,不要把指向字符的指針說成指向字符串的指針。

int(*b3) [30] [20];  //三級指針――>指向三維數組的指針;
int (*b2) [20];     //二級指針;――>指向二維數組的指針;
b3=new int [1] [20] [30];
b2=new int [30] [20];

刪除這兩個動態數組可用下式:

delete [] b3;  //刪除(釋放)三維數組;
delete [] b2;  //刪除(釋放)二維數組;

在堆中建立動態多維數組

new 類型名[下標表達式1] [下標表達式2]……;

例如:建立一個動態三維數組

float (*cp)[30][20] ;  //指向一個30行20列數組
                             //的指針,指向二維數組的指針
cp=new float [15] [30] [20];  //建立由15個30*20數組組成的數組;

注意:cp等效於三維數組名,但沒有指出其邊界,即最高維的元素數量,就像指向字符的指針即等效一個字符串,不要把指向字符的指針,說成指向字符串的指針。這與數組的嵌套定義相一致。

float(*cp) [30] [20];  //三級指針;
float (*bp) [20];     //二級指針;
xcp=new float [1] [20] [30];
bp=new float [30] [20];

兩個數組都是由600個浮點數組成,前者是只有一個元素的三維數組,每個元素為30行20列的二維數組,而另一個是有30個元素的二維數組,每個元素為20個元素的一維數組。

刪除這兩個動態數組可用下式:

delete [] cp;  //刪除(釋放)三維數組;

1、先看二維數組的動態創建:

void main(){
    double **data;
    data = new double*[m]; //申請行
    if ((data ) == 0)
     { cout << "Could not allocate. bye ...";
        exit(-1);}
    for(int j=0;j<m;j++)
     { data[j] = new double[n]; //設置列
        if (data[j] == 0)
        { cout << "Could not allocate. Bye ...";
           exit(-1);}  } //空間申請結束,下為初始化
    for (int i=0;i<m;i++) 
       for (int j=0;j<n;j++)  data[i][j]=i*n+j;
      display(data); //2、二維數組的輸出,此處略。

//3、再看二維數組的撤銷與內存釋放:

   for (int i=0;i<m;i++)
         delete[] data[i];
      //注意撤銷次序,先列后行,與設置相反
    delete[] data;
    return;
}

二維數組的內存釋放可以做成函數,

調用語句de_allocate(data);

void de_allocate(double **data){
       for (int i=0;i<m;i++)    delete[] data[i];
       delete[] data;
       return; 
}

通過指針使堆空間,編程中的幾個可能問題

⑴.動態分配失敗

返回一個空指針(NULL),表示發生了異常,堆資源不足,分配失敗。

          data = new double*[m]; //申請行

          if ((data ) == 0)……

⑵.指針刪除與堆空間釋放

刪除一個指針p(delete p;)實際意思是刪除了p所指的目標(變量或對象等),釋放了它所占的堆空間,而不是刪除p本身,釋放堆空間后,p成了空懸指針,不能再通過p使用該空間,在重新給p賦值前,也不能再直接使用p。

⑶.內存泄漏(memory leak)和重復釋放

new與delete 是配對使用的, delete只能釋放堆空間。如果new返回的指針值丟失,則所分配的堆空間無法回收,稱內存泄漏,同一空間重復釋放也是危險的,因為該空間可能已另分配,所以必須妥善保存new返回的指針,以保證不發生內存泄漏,也必須保證不會重復釋放堆內存空間。

⑷.動態分配的變量或對象的生命期。

無名對象的生命期並不依賴於建立它的作用域,比如在函數中建立的動態對象在函數返回后仍可使用。我們也稱堆空間為自由空間(free store)就是這個原因。但必須記住釋放該對象所占堆空間,並只能釋放一次,在函數內建立,而在函數外釋放是一件很容易失控的事,往往會出錯。

 

編程學習-動態內存分配-基於C++類

堆對象與構造函數

 通過new建立的對象要調用構造函數,通過deletee刪除對象也要調用析構函數。

CGoods *pc;
pc=new CGoods;  //分配堆空間,並構造一個無名
                               //的CGoods對象;
…….
delete pc;  //先析構,然后將內存空間返回給堆;

    堆對象的生命期並不依賴於建立它的作用域,所以除非程序結束,堆對象(無名對象)的生命期不會到期,並且需要顯式地用delete語句析構堆對象,上面的堆對象在執行delete語句時,C++自動調用其析構函數。

正因為構造函數可以有參數,所以new后面類(class)類型也可以有參數。這些參數即構造函數的參數。
但對創建數組,則無參數,並只調用缺省的構造函數。見下例類說明:

class CGoods{
           char Name[21];
           int  Amount;
           float Price;
           float Total value;
public:
  CGoods(){}; //缺省構造函數。因已有其他構造函數,系統不會再自動生成缺省構造,必須顯式說明。
  CGoods(char* name,int amount ,float price){
            strcpy(Name,name);
            Amount=amount;
            Price=price;
            Total_value=price*amount;  }
            ……

};//類聲明結束

//下面注意如何使用:

void main(){
  int n;
  CGoods *pc,*pc1,*pc2;
  pc=new CGoods(“夏利2000”,10118000);

  //調用三參數構造函數
  pc1=new CGoods();  //調用缺省構造函數
  cout<<’輸入商品類數組元素數’<<endl;
  cin>>n;
  pc2=new CGoods[n];
 //動態建立數組,不能初始化,調用n次缺省構造函數 
  ……
  delete pc;
  delete pc1;
  delete []pc2;  
}

此例告訴我們堆對象的使用方法:

申請堆空間之后構造函數運行;

釋放堆空間之前析構函數運行;

再次強調:由堆區創建對象數組,只能調用缺省的構造函數,不能調用其他任何構造函數。如果沒有缺省的構造函數,則不能創建對象數組。

 

淺拷貝與深拷貝

對象的構造,也可以由拷貝構造函數完成,即用一個對象的內容去初始化另一個對象的內容。

此時,若對象使用了堆空間(注意和“堆對象”區分),就有深、淺拷貝的問題,不清楚則很容易出錯。

1、什么是淺拷貝?

2、淺拷貝可能帶來什么問題?

3、什么是深拷貝?

4、深拷貝的實現方法?

什么是淺拷貝

缺省拷貝構造函數:用一個對象的內容初始化另一個同類對象,也稱為缺省的按成員拷貝,不是對整個類對象的按位拷貝。這種拷貝稱為淺拷貝。

class CGoods{
           char *Name; //不同與char Name[21] ?
           int  Amount;
           float Price; float Total_value;
public: CGoods(){Name=new char[21];}
 CGoods(CGoods & other){ //缺省拷貝構造內容:
            this->Name=other.Name;
            this->Amount=other.Amount;
            this->Price=other.Price;
           this->Total_value="/blog/other.Total_value;}
~CGoods(){delete Name;}//析構函數
};  //類聲明結束

淺拷貝帶來的問題

void main(){
     CGoods pc;  //調用缺省構造函數
   CGoods pc1(pc);   //調用拷貝構造函數
} //程序執行完,對象pc1和pc將被析構,此時出錯。

析構時,如用缺省的析構函數,則動態分配的堆空間不能回收。

如果用有“delete Name;”語句的析構函數,則先析構pc1時,堆空間已經釋放,然后再析構pc時出現了二次釋放的問題。

這時就要重新定義拷貝構造函數,給每個對象獨立分配一個堆字符串,稱深拷貝。

深拷貝——自定義拷貝構造

CGoods(CGoods & other){ //自定義拷貝構造
            this->Name=new char[21];
            strcpy(this->Name,other.Name);
            this->Amount=other.Amount;
            this->Price=other.Price;
            this->Total_value="/blog/other.Total_value;
}

例子:定義copy structor和拷貝賦值操作符(copy Assignment Operator)實現深拷貝。

//學生類定義:
class student{
     char *pName; //指針成員
public:
     student();
     student(char *pname);
     student(student &s); //拷貝構造函數
     ~student();
     student & operator=(student &s);
                                    //拷貝賦值操作符
};

//缺省構造函數:
student::student()
 {   pName=NULL; cout<<“Constructor缺省/n";  }
//帶參數構造函數:
student::student(char *pname){
       if(pName=new char[strlen(pname)+1])
              strcpy(pName,pname);
       cout <<"Constructor" <<pName<<endl;}
//拷貝構造函數:
student::student(student &s){
    if(s.pName!=NULL){
        if(pName=new char[strlen(s.pName)+1])
               strcpy(pName,s.pName); }

  //加1不可少,否則串結束符沖了其他信息,析構會出錯!
    else pName=NULL;
    cout <<"Copy Constructor" <<pName<<endl;}

//析構函數:
student::~student(){
    cout<<"Destructor"<<pName<<endl;
    if(pName) delete[ ]pName;} //釋放字符串

//拷貝賦值操作符:
student & student::operator=(student &s){
    if(pName) delete[ ]pName;
    if(s.pName){
       if(pName=new char[strlen(s.pName)+1])  
             strcpy(pName,s.pName);}
    else pName=NULL;
cout <<"Copy Assign operator" <<pName<<‘/n’;
    return *this;}

堆內存是最常用的需要自定義拷貝構造函數的資源,但不是唯一的,如打開文件等也需要。

   如果類需要析構函數來釋放某些資源,則類也需要一個自定義的拷貝構造函數。此時,對象的拷貝就是深拷貝了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM