深度探索c++對象模型之placement operator new語意

       首先談談new、delete和operator new、operator delete的區別:new和delete只是c++中的運算符而已!而operator new和operator delete則是c++中的函數,是可以重載的函數【但不能在全局域中重載,一般都是在類內重載】,其中operator new被重載時,第一個參數是是要求分配空間的大小(字節),類型一般是size_t,除此之外,還可以帶其它的參數,但該函數的返回類型必須是void *;此外,它與new不同的是,它只分配所要求的空間,並不調用相關類的constructor(構造函數),如果分配內存失敗,則它會調用new_handler或拋出bad_alloc異常。而operator delete的重載限制爲,返回類型是void型,傳遞參數是void*型。讓我們先來看一個示例:
#include <iostream>
#include <string>
using namespace std;

class A{
public:
	A(){ cout << "這是A的構造器在執行!" << endl; }
	~A(){ cout << "在執行A的析構器!" << endl; }

	void *operator new(size_t size, string str)
	{
		cout << "new函數的尺寸爲" << size << ",字符串是:" << str << endl;
		return ::operator new(size); //在這裏如果我們不調用標準的、全局的operator new,比如直接返回NULL,那麼在後面的main函數中,delete p函數就不會得到執行,因爲編譯器會首先測試p指針是不是NULL值,是就跳過delete的執行
	}

	void operator delete(void *p)
	{
		cout << "delete版的重載函數在執行" << endl;
		::operator delete(p);
	}

private:
	int n;
};

int main()
{
	cout << "A的尺寸是:" << sizeof(A) << endl;
	A *p = new("一個新的A對象") A; //在這裏編譯器會有擴展,先傳遞一個sizeof(A),再傳遞那個漢語字符串
	delete p;

	return 0;
}

在上面的例子中,如果我們能寫下“    A* p = new A; ”【但現實情況下,我們這樣寫會編譯錯誤,因爲我們在A的類中已經給new定義了一個重載函數,而這個函數需要傳遞兩個參數,而我們只傳遞過去一個sizeof(A)】,那麼這裏的new操作符會做以下這些事,首先是調用operator new類分配內存,接着再調用A類的constructor構造器,最後返回對應的指針。而我們這裏的operator new和operator delete和C語言中的malloc和free差不多,只負責內存的分配與回收。事實上,在上面的代碼運行時【我用的vs013】,結果顯示調用了A的構造函數,但這並不是在operator new裏面調用的,而是編譯器的功勞】

      那什麼是placement new呢?首先說說placement new的好處,它適用於那些對效率要求很高【相比而言,單純的new需要查找空間等】,能長時間運行不被打斷的程序。再說說它的使用方法,以上面的自定義類A爲例,如果我們是單個使用,那麼只要預先在堆中開闢好內存【void *buf = new[sizeof(A)];】,然後再分配一下即可【A *p = new(buf) A;】,但是對於數組形式來說就稍微有些不同了:

void *buf = new[X*sizeof(A) + sizeof(int)];//其中,X是我們所需要的類對象數組的大小,而sizeof(int)則是一個多出來的用於存放數組大小的字節內存
然後分配時寫【A *p = new(buf) A;】即可。。。

      所謂的placement new,其實就是operator new的一個全局域的版本【這也是爲什麼我們不能在全局域中重載operator new函數的原因】,但與其它operator new不同的是,它不允許被用戶自定義的版本的代替。對placement new的調用,一定要傳遞兩個參數,首先是對象的大小,其次是void*類型的指針,但placement new會忽略掉第一個參數,既size_type,因爲這個參數對它沒啥用,但它一定會以void*形式返還第二個參數,既new出來的指針所指地址。看到這裏也許有讀者朋友會問,既然這個placement new做的是返回void*指針,那麼我們何必調用它呢?我們在堆中開闢好一塊內存之後,直接把這個內存起始地址賦值給我們的指針不就行了?嗯,對於那些內建類型如int、char等,確實如此,但對於那些自定義的有着constructor構造器的類類型來說,就不行了,因爲我們除了給它們“安家置戶”之外,還需要調用它們自帶的構造器對它們constructor,讓我們來看一下編譯器擴展後的僞代碼:

//經過編譯器處理的僞代碼
A *p = (A *)buf;//同上,buf是我們預先開闢好的起始內存地址
if( p != NULL )
  p->A::A();//需要調用A的constructor
      接下來,讓我們來考慮下面的代碼段:

void fooBar()
{
  A *p = new(buf) A;
  ...//buf沒有改變
  //在這裏該不該寫【delete p;】?
  A *p2 = new(buf) A;
}
如果placement new在原來已經存在的對象上構造一個新對象,而這個對象有一個析構器,那麼這個析構器並不會被調用,調用它的析構器的方法是把那個指針delete掉,然和人如果我們真像註釋裏面那樣寫的話,那這樣同時也會釋放buf所指的內存,這並不是我們希望看到的。所以我們應該先明確調用一下A的析構器,然後再寫【A *p2 = new(buf) A;】,不過據說,現在的c++ standard用一個placement operator delete矯正了這個錯誤:它會對類執行該類的析構器,而且不會釋放對應的內存。

      還有一個問題,關係到buf所表現出的真正類型,c++ standard說它要麼指向相同類型的class,要麼指向一塊新鮮的內存,足夠容納該類型的對象,所以因爲後者,derived class並不在被支持之列,對於derived class,其行爲可能是不可控制的,因爲如果一個派生類的佔用內存如果比它的基類大,比如【A *P3 = new(buf) B;//B是A的derived class】,那麼B的constructor很可能導致嚴重的破壞。

      在placement operator new被引入到cfront2.0時,最隱晦問題是下面這個:

struct Base{int i; virtual void fun();}
struct Derived:Base { void fun(); }

void fooBar()
{
  Base b;
  b.fun();//Base::fun被調用
  b.~Base();
  new(&b) Derived;
  b.fun();//在這裏,哪個版本的fun被調用??
}
由於上述兩個類有着相同的內存大小,所以把Derived對象放在爲Base類中而配置的內存是安全的。但是,要支持這一點,就必須放棄對於“經由對象靜態調用所有virtual函數”通常都會有的優化處理。所以最後結果是上面的placement operator new在c++ standard未能獲得支持,因爲上面代碼中的問題並不能明確回答或定義,你可以說它調用的是base的fun,也可以說它調用的是derived裏的fun。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章