天天看點

6.2 new和delete運算符

運算符new的使用,事實上經過兩個步驟完成:

int* pi = new int(5);

//1.通過new運算符,配置所需要的記憶體
int* pi =_new(sizeof(int));

//2.将配置的對象設立初值
*pi = 5; 
           

更進一步說,初始化操作應該再記憶體成功(經由new運算符)後才執行:

//new運算符的兩個分離步驟
//int* pi = new int(5);

//重寫聲明
int* pi;
if(pi = _new(sizeof(int)))
	*pi = 5;

//delete的分離步走
//delete pi;

//重寫聲明
if(pi != 0)
	_delete(pi);
           

如果pi的值時0,C++語言會要求delete運算符不會執行,pi并不會自動清0,像下面這樣的後繼行為,雖然沒有良好定義,但是可能(也可能不)被評估為真,這是因為對于pi所指之記憶體的變更或再使用,可能(也可能不)會發生。

if(pi && pi == 5) ...
           

pi所指對象的生命會因delete而結束,是以後繼任何對pi的參考操作就不再包良好的行為,并是以被視為一種不好的程式風格。然而,把pi繼續當一個指針來用,仍然可以的(雖然使用受到限制),例如:

//ok,pi仍然指向合法的空間
//甚至幾十存儲其中的object不再合法
if(pi == sentinel) ...
           

在這裡,使用指針pi,和使用pi所指的對象,其差别在于哪一個生命已經結束了。雖然該位址的對象不再合法,位址本身卻仍然代表一個合法的程式空間,是以pi能夠繼續使用,但隻能在受限制的情況下使用,很想一個void*指針的情況。

以constructor來配置一個class object情況類似:

//被轉化為
Point3d origin;
//C++ pseudo code
if(origin = _new(sizeof(Point3d)))
	origin = Point3d::Point3d(origin);
           

如果實作出exception handling,那麼轉換結果可能複雜些:

//C++ pseudo code
if(origin = _new(sizeof(Point3d)))
{
	try{
		origin = Point3d::Point3d(origin);
	}
	catch(...){
		//調用delete library funct釋放記憶體
		_delete(origin);
		//将原來的exception上傳
		throw;
	}
}
           

destructor應用類似:

delete origin;

//被内部轉化
if(origin != 0)
{
	Point3d::~Point3d(origin);
	_delete(origin);
}
           

以下是library中new的實作方法(以下版本并沒考慮exception handing):

extern void*
operator new(size_t size)
{
	if(size == 0)
		size = 1;
	
	void* last_alloc;
	while(!(last_alloc = malloc(size)))
	{
		if(_new_handler)
			(*_new_handler)();
		else
			return 0;
	}
	return last_alloc;
}
           

雖然如下寫法是合法的:

new T[0];
           

但語言要求每一次對new的調用都必須傳回一個獨一無二的指針,解決此問題的方法是傳統的傳回一個指針,指向一個預設為1byte的記憶體空間,另一個有趣之處是,它允許使用者提供一個屬于自的_new_handler()函數。

new運算符實際上總是以标準C malloc完成,雖然并沒有規定一定得這麼做。相同情況下,delete運算符也是以标準C的free()完成:

extern void
operator delete(void* ptr)
{
	if(ptr)
		free((char*)ptr);
}
           

針對數組的new語意

當我們寫:

int* p_array = new int[5];
           

vec_new()不會被真正調用,因為它主要功能是把default constructor施行于class object所組成的數組的每一個元素上。倒是new運算符會被調用:

int* p_array = (int*)_new(5*sizeof(int));
           

相同情況下,如果我們寫:

//struct simple_aggr{float f1,f2;};
simple_aggr* p_aggr = new simple_aggr[5];
           

vec_new()也不會被調用,因為simple_aggr并沒有定義一個constructor或destructor,是以配置數組以及清理p_aggr數組的操作,隻是單純地擷取記憶體和釋放記憶體而已。

然而如果class定義了一個default constructor,某些版本的vec_new()就會被調用,配置并構造class objects所組成的數組:

Point3d* p_array = new Point3d[10];

//内部轉化
Point3d* p_array;
p_array = vec_new(0,sizeof(Point3d),10,
				  &Point3d::Point3d,
				  &Point3d::~Point3d);
           

在個别數組構造過程中,如果發生exception,destructor就會被傳遞給vec_new()。隻要已經構造妥當的元素才施行destructor。

對于寫下如下的代碼,析構函數對應的寫法:

int array_size = 10;
Point3d* p_array = new Point3d[10];

//C++2.0之前必須這麼寫
delete [array_size] p_array;
				  
//C++2.0之後可以寫麼寫
delete [] p_array;
           

為了回溯相容,兩種形式都可以接受。

對于如下的destructor操作而言:

delete p_array;
           

那麼隻有第一個元素被析構,其他元素仍然存在——雖然其相關的記憶體已經歸還了。

Placement Operator new的語意

有一個預先定義好的重載的(overloaded)new運算符,成為placement operator new。它需要第二個參數,類型為void*,調用方式如下:

Poin2w* ptw = new(arena)Poin2w;	
           

其中arena指向記憶體中的一塊區域,用以放置新産生出來的Point2w object。它的實作方法隻要将“獲得的指針(arena)”所指的位址傳回即可:

void* operator new(size_t,void* p)
{
	return p;
}	
           

Placement new operator所擴充的另一半是将Point2w constructor自動實施于arena所指的位址上:

//C++ pseudo code
Poin2w* ptw = (Poin2w*)arena;
if(ptw != 0)
		ptw->Point2w::Point2w();
           

然而卻有一個輕微不良行為:

//讓arena成為全局性定義
vod foobar()
{
	Point2w* p2w = new(arena)Point2w;
	//do it
	//now manipulate a new object
	p2w = new(arena)Point2w;
}
           

如果placement operator在原已經存在的一個object上構造新的object,該object有一個destructor,這個destructor并不會被調用,調用該destructor的方法之一是将那個指針delete。不過在此例中如果這樣做是錯誤的:

delete p2w;
p2w = new(arena)Point2w;

//施行destructor的正确方法
p2w->~Point2w;
p2w = new(arena)Point2w;
           

剩下的問題有一個是設計的問題:在例子中第placement operator第一次調用,會将新object構造于原已存在的object至上,還是會析構于全新位址上?我們如何知道arena所指的這塊區域是否需要先析構?這個問題在語言層面沒有解答,一個合理的習慣是指向new這一段也要負責執行destructor。

另一個問題關系到arena所表現的類型,C++ Standard說它必須指向相同類型的class,要不就是一塊“新鮮”記憶體,足夠容納該類型的object。但是,derived class并不被支援。對于一個derived class,或是其他沒有關聯的類,其行為雖然并非不合法,卻是未定義的。

新的記憶體空間可以這樣配置:

char* arena = new char[sizeof(Point2w)];
           

相同類型的object可以這樣獲得:

Poin2w* arena = new Point2w;
           

不論哪種情況,新的Point2w的存儲空間的确覆寫了arena的位置,此行為良好。然而,placement new operator并不支援多态。對于derived class比其base class大的話,derived class 的constructor會被破壞。

還有一個問題是:

struct Base{int j;virtual void f();};
struct Derived: Base{ void f();};				  

void fooBar()
{
		Base b;
		b.f();			//Base::f()被調用
		b.~Base();
		new(&b)Derived;
		b.f();		   //哪一個f()被調用
}
           

由于上述兩個classes有相同的大小,把derived object放在base class而配置的記憶體中是安全的,然而,要支援這一點,就必須放棄動态的virtual function。盡管大部分使用者以為調用時Derived的,但大部分編譯器調用卻是Base。

繼續閱讀