让C++对象只能分配到堆/栈和静态区上并判断分配位置
對象分配到堆上的過程:三個形式的new
要把對象分配到棧上,需要使用到new operator,而new operator會調用operator new和placement new。
- operator new用于調用malloc申請堆空間,如果申請失敗會拋出bad_alloc異常
- placement new用于“定向構造”,即在指定的內存上(這里就是指operator new申請的空間)調用構造函數以構造出新對象
根據侯捷先生的書所說,STL(書中的版本)對于某些對象做了統一的分配和統一的構造,而不把這兩個步驟通過直接調用new operator合二為一。
實際上,placement new也有其他作用,例如可以將一個對象構造在已知的棧空間,或者全局靜態區空間上。(當然那樣就不能對那個對象調用delete啦,會core dump的),代碼就像這樣:
char buf[100]{};new (buf) HeapOnly();HeapOnly *h = static_cast<HeapOnly *>((void*)buf);delete h; //!!! crash可見C++給了程序員足夠多的自由。
如何限制只能在堆上
一個很自然的想法是,既然只能在堆上,那么分配的方法就只有new了,那把構造函數private掉就可以了。事實上這是不對的,因為如上文所講,new operator會隱式調用placement new,placement new會隱式調用構造函數,造成了在類外訪問構造函數,所以不能把構造函數設置為private。如果這么干,只要在類外,堆和棧上都分配不了。
解決方案如下:
那么為什么要把析構函數聲明為private呢?這里是兩個問題,依次解決。
為什么要在析構函數上動手腳?根據前文,把構造函數private掉是不行了,但是如果對析構函數這么做,就會形成能在棧上分配,但是卻無法在函數結束時釋放的情況,編譯器會在編譯期發現這種錯誤,報出編譯錯誤。
但是請注意,這樣做的話,就無法顯式的delete掉HeapOnly及其派生類了。和new operator類似,delete operator也會先調用對象的析構函數再釋放內存,這樣又造成了在類外訪問private成員的情況,所以必須要提供一個銷毀該對象的接口Destroy()。
為什么是聲明為private而不是protected呢?眾所周知,protected的意思是子類可見,private是只有自己可見。寫這樣一個HeapOnly的類的意義類似于boost::noncopyable,要讓所有繼承該類的子類都只能被分配到堆上。
- HeapOnly的析構函數protected時, 假設派生類叫Derive,HeapOnly的析構函數對于Derive是可見的,Derive如果沒有再把析構函數對外隱藏,那么在堆上析構Derive就有了可能:因為Derive的析構函數會轉而調用基類的析構函數,Derive也就可以在棧上分配了!所以聲明為protected是不行的。
- HeapOnly的析構函數private時, 假設派生類叫Derive,HeapOnly的析構函數對于Derive是不可見的,如果要析構棧上對象Derive,就必須在Derive的析構函數中保證基類析構函數的可見性,HeapOnly的析構函數是private時,Derive的析構函數是無法調用基類析構函數的(因為private對子類不可見)。編譯器會報這個這樣的錯誤:
可見,編譯器認為既然Derive無法調用基類的析構函數,那就認為Derive的析構函數也是隱式的被刪除了吧。所以Derive就無法在棧上分配了。
題外話:什么時候析構函數應該是virtual的?就算析構函數不是virtual的,顯式的刪除一個棧上的派生對象,也會根據繼承層次一路從派生類析構到基類。但是如果是根據一個基類指針刪除堆上的派生對象,如果析構函數不是virtual的,那么派生類的析構函數就不會被調用……一句話總結,就是:這樣做是為了當用一個基類的指針刪除一個派生類的對象時,派生類的析構函數會被調用。
如何限制只能在棧上(當然這里也可以是靜態區)
這個就比只能在堆上分配要簡單一些,只需要把類中operator new[]和operator new[]聲明為protected就可以了。
class NoHeap {protected:static void *operator new(std::size_t);static void *operator new [] (std::size_t);};需要注意的有兩點:
如何運行時判斷在堆還是在棧上
首先需要明確的是,前面所講的都是在編譯期對于程序員使用一個類型的方法的約束#,當然在運行期是沒辦法做這個約束了(畢竟程序都跑起來了),不過有沒有辦法判斷一個對象到底是在堆上還是在棧上呢?可以試試下面這個類:
class Detect {public:Detect(){int i;Check(&i);}void Check(int *i) {int j;if ((i < &j) == ((void*)this < (void*)&j)) {std::cout << "Stack" << std::endl;return;}else{std::cout << "Heap" << std::endl;return;}}};在Detect的構造函數中分配一個局部變量i,再在調用的Check函數中分配局部變量j,如果Detect在棧上分配,那么典型的情況應該像這樣:
this ----> Detect() ---> int i ---> Check() ---> int j高地址------------------------------------------>低地址當然堆棧也有可能是從低地址向高地址生長的,視機器而定。Check里通過判斷&i/&j/this三者的相對位置,使得這兩種情況都可以應對。當然并不保證所有機器都適用,在我的x86_64電腦上測試通過。
總結
以上是生活随笔為你收集整理的让C++对象只能分配到堆/栈和静态区上并判断分配位置的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 大并发服务器框架设计
- 下一篇: 基于C++11的线程池