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”,10,118000); //調用三參數構造函數 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;}
堆內存是最常用的需要自定義拷貝構造函數的資源,但不是唯一的,如打開文件等也需要。
如果類需要析構函數來釋放某些資源,則類也需要一個自定義的拷貝構造函數。此時,對象的拷貝就是深拷貝了。