最近被問到了C++內存池的問題,其中不免涉及到在指定內存地址調用對象構造函數以及顯示調用對象析構函數的情況。
C++中new的用法
new是C++中用於動態內存分配的運算符,在C語言中一般使用malloc函數。
(1)plain new顧名思義就是普通的new,就是我們慣常使用的new。分配內存,調用構造函數,在C++中是這樣定義的:
1 void* operator new(std::size_t) throw(std::bad_alloc); 2 void operator delete(void *) throw();
plain new在分配失敗的情況下,拋出異常std::bad_alloc而不是返回NULL,因此通過判斷返回值是否為NULL是徒勞的。
1 #include "stdafx.h" 2 #include <iostream> 3 using namespace std; 4 char *GetMemory(unsigned long size) 5 { 6 char *p=new char[size];//分配失敗,不是返回NULL 7 return p; 8 } 9 10 int main() 11 { 12 try 13 { 14 char *p=GetMemory(10e11);// 分配失敗拋出異常std::bad_alloc 15 //........... 16 if(!p)//徒勞 17 cout<<"failure"<<endl; 18 delete [] p; 19 } 20 catch(const std::bad_alloc &ex) 21 { 22 cout<<ex.what()<<endl; 23 } 24 25 return 0; 26 }
(2)nothrow new是不拋出異常的運算符new的形式。nothrow new在失敗時,返回NULL。定義如下:
1 void * operator new(std::size_t,const std::nothrow_t&) throw(); 2 void operator delete(void*) throw();
1 #include "stdafx.h" 2 #include <iostream> 3 #include <new> 4 using namespace std; 5 char *GetMemory(unsigned long size) 6 { 7 char *p=new(nothrow) char[size];//分配失敗,是返回NULL 8 if(NULL==p) 9 cout<<"alloc failure!"<<endl; 10 return p; 11 } 12 13 int main() 14 { 15 try 16 { 17 char *p=GetMemory(10e11); 18 //........... 19 if(p==NULL) 20 cout<<"failure"<<endl; 21 delete [] p; 22 } 23 catch(const std::bad_alloc &ex) 24 { 25 cout<<ex.what()<<endl; 26 } 27 return 0; 28 }
(3)placement new意即“放置”,這種new允許在一塊已經分配成功的內存上重新構造對象或對象數組。placement new不用擔心內存分配失敗,因為它根本不分配內存,它做的唯一一件事情就是調用對象的構造函數。定義如下:
1 void* operator new(size_t,void*); 2 void operator delete(void*,void*);
palcement new的主要用途就是反復使用一塊較大的動態分配的內存來構造不同類型的對象或者他們的數組。placement new構造起來的對象或其數組,要顯示的調用他們的析構函數來銷毀,千萬不要使用delete。
1 #include "stdafx.h" 2 #include <iostream> 3 #include <new> 4 using namespace std; 5 class ADT 6 { 7 int i; 8 int j; 9 public: 10 ADT() 11 { 12 } 13 ~ADT() 14 { 15 } 16 }; 17 18 int main() 19 { 20 char *p=new(nothrow) char[sizeof(ADT)+2]; 21 if(p==NULL) 22 cout<<"failure"<<endl; 23 ADT *q=new(p) ADT; //placement new:不必擔心失敗 24 // delete q;//錯誤!不能在此處調用delete q; 25 q->ADT::~ADT();//顯示調用析構函數 26 delete []p; 27 return 0; 28 }
使用placement new構造起來的對象或數組,要顯式調用它們的析構函數來銷毀(析構函數並不釋放對象的內存),千萬不要使用delete.這是因為placement new構造起來的對象或數組大小並不一定等於原來分配的內存大小,
另:
當使用new運算符定義一個多維數組變量或數組對象時,它產生一個指向數組第一個元素的指針,返回的類型保持了除最左邊維數外的所有維數。例如:
int *p1 = new int[10];
返回的是一個指向int的指針int*
int (*p2)[10] = new int[2][10];
new了一個二維數組, 去掉最左邊那一維[2], 剩下int[10], 所以返回的是一個指向int[10]這種一維數組的指針int (*)[10].
int (*p3)[2][10] = new int[5][2][10]; new了一個三維數組, 去掉最左邊那一維[5], 還有int[2][10], 所以返回的是一個指向二維數組int[2][10]這種類型的指針int (*)[2][10].
#include<iostream> #include <typeinfo> using namespace std; int main() { int *a = new int[34]; int *b = new int[]; int (*c)[2] = new int[34][2]; int (*d)[2] = new int[][2]; int (*e)[2][3] = new int[34][2][3]; int (*f)[2][3] = new int[][2][3]; a[0] = 1; b[0] = 1; //運行時錯誤,無分配的內存,b只起指針的作用,用來指向相應的數據 c[0][0] = 1; d[0][0] = 1;//運行時錯誤,無分配的內存,d只起指針的作用,用來指向相應的數據 e[0][0][0] = 1; f[0][0][0] = 1;//運行時錯誤,無分配的內存,f只起指針的作用,用來指向相應的數據 cout<<typeid(a).name()<<endl; cout<<typeid(b).name()<<endl; cout<<typeid(c).name()<<endl; cout<<typeid(d).name()<<endl; cout<<typeid(e).name()<<endl; cout<<typeid(f).name()<<endl; delete[] a; delete[] b; delete[] c; delete[] d; delete[] e; delete[] f; } 輸出結果: int * int * int (*)[2] int (*)[2] int (*)[2][3] int (*)[2][3]
深入學習文獻:C++new用法深層剖析
C++顯示調用析構函數
一、文章來由
現在在寫一個項目,需要用到多叉樹存儲結構,但是在某個時候,我需要銷毀這棵樹,這意味着如果我新建了一個樹對象,我很可能在某處希望將這個對象的聲明周期終結,自然會想到顯示調用析構函數,但是就扯出來這么大個陷阱。
二、原因
在了解為什么不要輕易顯示調用析構函數之前,先來看看預備知識。
為了理解這個問題,我們必須首先弄明白“堆”和“棧”的概念。
1)堆區(heap) —— 一般由程序員分配釋放, 若程序員不釋放,程序結束時可能由OS回收。注意它與數據結構中的堆是兩回事,分配方式倒是類似於鏈表。
2)棧區(stack) —— 由編譯器自動分配釋放,存放函數的參數值,局部變量的值等。其操作方式類似於數據結構中的棧。
我們構造對象,往往都是在一段語句體中,比如函數,判斷,循環,還有就直接被一對“{}”包含的語句體。這個對象在語句體中被創建,在語句體結束的時候被銷毀。問題就在於,這樣的對象在生命周期中是存在於棧上的。也就是說,如何管理,是系統完成而程序員不能控制的。所以,即使我們調用了析構,在對象生命周期結束后,系統仍然會再調用一次析構函數,將其在棧上銷毀,實現真正的析構。
所以,如果我們在析構函數中有清除堆數據的語句,調用兩次意味着第二次會試圖清理已經被清理過了的,根本不再存在的數據!這是件會導致運行時錯誤的問題,並且在編譯的時候不會告訴你!
三、顯示調用帶來的后果
如果硬要顯示調用析構函數,不是不可以,但是會有如下3條后果:
1)顯式調用的時候,析構函數相當於的一個普通的成員函數;
2)編譯器隱式調用析構函數,如分配了對內存,顯式調用析構的話引起重復釋放堆內存的異常;
3)把一個對象看作占用了部分棧內存,占用了部分堆內存(如果申請了的話),這樣便於理解這個問題,系統隱式調用析構函數的時候,會加入釋放棧內存的動作(而堆內存則由用戶手工的釋放);用戶顯式調用析構函數的時候,只是單純執行析構函數內的語句,不會釋放棧內存,也不會摧毀對象。
用如下代碼表示:
例1:
class aaa
{
public: aaa(){} ~aaa(){cout<<"deconstructor"<<endl; } //析構函數 void disp(){cout<<"disp"<<endl;} private: char *p; }; void main() { aaa a; a.~aaa(); a.disp(); }
分析:
這樣的話,顯式兩次destructor,第一次析構相當於調用一個普通的成員函數,執行函數內語句,顯示第二次析構是編譯器隱式的調用,增加了釋放棧內存的動作,這個類未申請堆內存,所以對象干凈地摧毀了,顯式+對象摧毀
例2:
class aaa { public: aaa(){p = new char[1024];} //申請堆內存 ~aaa(){cout<<"deconstructor"<<endl; delete []p;} void disp(){cout<<"disp"<<endl;} private: char *p; }; void main() { aaa a; a.~aaa(); a.disp(); }
分析:
這樣的話,第一次顯式調用析構函數,相當於調用一個普通成員函數,執行函數語句,釋放了堆內存,但是並未釋放棧內存,對象還存在(但已殘缺,存在不安全因素);第二次調用析構函數,再次釋放堆內存(此時報異常),然后釋放棧內存,對象銷毀
四、奇葩的錯誤
系統在什么情況下不會自動調用析構函數呢?顯然,如果對象被建立在堆上,系統就不會自動調用。一個常見的例子是new…delete組合。但是好在調用delete的時候,析構函數還是被自動調用了。很罕見的例外在於使用布局new的時候,在delete設置的緩存之前,需要顯式調用的析構函數,這實在是很少見的情況。
我在棧上建樹之后,顯示調用析構函數,對象地址任然存在,甚至還可以往里面插入節點。。。
其實析構之前最好先看看堆上的數據是不是已經被釋放過了。
////////////////a.hpp #ifndef A_HPP #define A_HPP #include <iostream> using namespace std; class A { private: int a; int* temp; bool heap_deleted; public: A(int _a); A(const A& _a); ~A(); void change(int x); void show() const; }; #endif ////////////a.cpp #include "a.hpp" A::A(int _a): heap_deleted(false) { temp = new int; *temp = _a; a = *temp; cout<< "A Constructor!" << endl; } A::A(const A& _a): heap_deleted(false) { temp = new int; *temp = _a.a; a = *temp; cout << "A Copy Constructor" << endl; } A::~A() { if ( heap_deleted == false){ cout << "temp at: " << temp << endl; delete temp; heap_deleted = true; cout << "Heap Deleted!\n"; } else { cout << "Heap already Deleted!\n"; } cout << "A Destroyed!" << endl; } void A::change(int x) { a = x; } void A::show() const { cout << "a = " << a << endl; } //////////////main.cpp #include "a.hpp" int main(int argc, char* argv[]) { A a(1); a.~A(); a.show(); cout << "main() end\n"; a.change(2); a.show(); return 0; }
五、小結
所以,一般不要自作聰明的去顯示調用析構函數。