1. 程式人生 > >深度探索c++物件模型第六章筆記

深度探索c++物件模型第六章筆記

執行期語意學(Runtime Semantics)

有下面的程式碼

if(yy==xx.getValue())...........

其中xx 和yy定義為:

X xx;
Y yy;

class Y定義為:

class Y
{
public: 
	Y();
	~Y();
	bool operator==(const Y&) const;
	//...........
};

Class X的定義為:

class X
{
public:
	X();
	~X();
	operator Y()const;
	X getValue();
	//...........
};

上面第一行程式碼的第一轉換為:

//resolution of intended operator
if(yy.operator==(xx.getValue()))

Y的equality(等號)運算子需要一個型別為Y的引數,然後getValue()傳回的卻是一個型別為X的object。本例中,X提供了一個conversion運算子,把一個X object轉換為一個Y object。它必須施行於getValue()的返回值身上。
所以需要第二次轉換:

//conversion of getVlaue()'s return value
if(yy.operator==(xx.getValue().operator Y())

以上的兩次轉換都是編譯器根據class的隱含語意代替我們程式設計師所做的操作。

接下來我們利用一個臨時物件來放置函式呼叫所傳回的值:

  • 產生一個臨時的class X object,放置getValue()的返回值:
X temp1=xx.getValue();
  • 產生一個臨時的class Y obejct ,放置operator Y()的返回值:
X temp2=temp1.operator Y();
  • 產生一個臨時的int object,放置equality(等號)運算子的返回值:
int  temp3=yy.operator==(temp2);

最後,適當的destructor將被施行於每一個臨時性的class object身上,所以有虛擬碼:

//c++  虛擬碼
//以下是條件語句 if(yy==xx.getValue())....的轉換
{
	X temp1=xx.getValue();
	Y temp2=temp2.operator Y();
	int temp3=yy.operator==(temp2);

	if(temp3)........
	
	temp2.Y::~Y();
	temp1.X::~X();
}

6.1 物件的構造和解構(Object Construction and Destruction)

一般而言,Constructor和destructor的安插都如你所預期:

//c++ 虛擬碼
{
	Point point;
	//point.Point::Point()  一般而言會被安插在這裡  (宣告 之後)
	.....
	//point.Point::~Point()	一般而言會被安插在這裡(結束之前)
}

destructor必須放在每一個離開點(當時object還存活);

全域性物件

有以下程式片段:

Matrix identity;
main()
{
	//identity 必須在此處被初始化
	Matrix m1=identity;
	...
	return 0;
}

c++保證,一定會在main()函式中第一次用到identity之前,把identity構造出來,而在main()函式結束之前把identity摧毀掉。
像identity這樣的global object如果有constructor和destructor,我們需要為它提供靜態的初始化操作和記憶體釋放操作
c++程式中所有的global objects 都被放置在程式的data segment中,如果明確指定給他一個值,object將以該值為初值。否則object所配置到的記憶體內容為0。

cfront編譯提供一個可移植但成本頗高的靜態初始化方法(以及記憶體釋放)方法,稱為munch。
這些munch策略稱為:

  • 1、為每一個需要靜態初始化的檔案產生一個_sti()函式,內帶必要的constructor呼叫操作或inline expansions。
  • 2、類似情況,在每一個需要靜態的記憶體釋放操作的檔案中,產生一個__std()函式,內帶必要的destructor呼叫操作,或是其inline expansions。
  • 3、提供一組runtime library “munch”函式:一個_main()函式(用以呼叫可執行檔案中的所有_sti函式),以及一個exit()函式(以類似方式呼叫所有的_std函式)。
  • 在這裡插入圖片描述

支援“nonclass objects的靜態初始化”,在某種程度上是支援virtual base classes的一個副產品。以一個derived class的pointer或reference來存取virtual base class subobject,是一種nonconstant expression,必須在執行期才能加以評估求值。

區域性靜態物件

假設有以下程式片段:

const Matrix &
indenity()
{
	static Matrix mat_idnetity;
	//.......
	return mat_identity;
}

local static class object保證了怎樣的語意呢?

  • mat_identity的constructor必須只能施行一次,雖然上述程式碼可能會被呼叫多次。
  • mat_identity的destructor必須只能施行一次,雖然上述程式碼可能會被呼叫多次。

物件陣列

假設有下面陣列的定義:

Point knots[10];

如果Point既沒有定義一個constructor也沒有定義一個destructor,那麼我們需要配置足夠的記憶體以儲存10個連續的Point元素。
然而Point的確定義了一個default destructor,所以這個destructor必須輪流施行於每一個元素之上。一般而言這是經由一個或多個runtime library函式達成。在cfont編譯器中,我們使用一個被命名為vec_new()的函式,產生出以class objects構造而成的陣列。而有的編譯器則提供兩個函式:一個用來處理“沒有virtual base class”的class,另一個用來處理“內帶virtual base class”的class。後一個函式通常被稱為vec_vnew().而函式的型別通常如下:

void * 
vec_new
{
	void *array,         //陣列起始地址
	size_t elem_size,   //每一個class object的大小
	int  elem_count,     //陣列中元素數目
	void (*constructor) (void *),
	void (*destructor)(void *,char)
}

其中的constructor和destructor引數是這個class的default constructor和default destructor的函式指標。引數array帶有的若不是具名陣列的地址,就是0。如果是0,那麼資料將經由應用程式的new運算子,被動態配置與heap中。(Sun編譯器將“由class objects所組成的具名陣列”和“動態配置而來的陣列”的處理操作分為兩個library函式:_vector_new2和_vec_con,它們各自擁有一個virtual base class函式實體)。引數elem_size表示陣列中的元素數目。在vec_new()中,constructor施行於elem_count個元素之上。
在vec_new()中,constructor施行於elem_count個元素之上,下面是編譯器可能針對我們的10個Point元素所做的vec_new()呼叫操作:

Point knots[10];
vec_new (&knots,sizeof(Point),10,&Point::Point,0);

如果Point也定義了一個destructor,當knots的生命結束時,該destructor也必須施行於那10個Point元素身上。這是經由一個類似的vec_delete()(或是一個vec_delete()------如果classes擁有virtual base classes的話)的runtime library函式完成,其函式型別如下:

void*
vec_delete(
	void *array,                 //陣列起始地址
	size_t elem_size,     //每一個class object的大小
	int  elem_count ,        //陣列中的元素數目
	void (*destructor)(void*,char)
)

有的編譯器會另外增加一些引數,用以傳遞其他數值,以便能夠有條件地導引vec_delete()的邏輯。在vec_delete()中,destructor被施行於elem_count個元素身上。

如果提供一個或多個明顯初值給一個由class objects組成的陣列,

Point  knots[10]={
	Point(),
	Point(1.0,1.0,0.5),
	-1.0
};

對於那些明顯獲得初值的元素,vec_new()不再有必要,對於那些尚未被初始化的元素,vec_new()的施行方式就像面對“由class elements組成的陣列,而該陣列沒有explicit initialization list ”一樣,所以上面的定義可能會被轉化為:

Point knots[10];

//c++ 虛擬碼
// 明確地初始化前3個元素
Point::Point(&knots[0]);
Point::Point(&knots[1],1.0,1.0,0.5);
Point::Point(&knots[2],-1.0,0.0,0.0,);

//以vec_new初始化後7個元素
vec_new(&knots+3,sizeof(Point),7,&Point::Point,0);

new 和delete運算子

運算子new的使用,看起來似乎是一個單一運算,像這樣:

int *pi=new int(5);

但事實上它是由兩個步驟完成:

  • 1、通過適當的new運算子函式實體,配置所需的記憶體:
// 呼叫函式庫中的new 運算子
int *pi=__new(sizeof(int));
  • 2、給配置得來的物件設立初值:
*pi=5;

更加需要注意的是,初始化操作應該在記憶體配置成功後才執行.
delete運算子的情況類似:

delete pi;

如果pi的值為0(為空),c++會要求delete運算子不要進行任何操作。
所以我們需要對此加上一層保護膜:

if(pi!=0)
	__delete(pi);

pi所指物件之生命會因delete而結束,所以後繼任何對pi的參考操作就不能再保證有良好的行為,並因此會被視為是一種不好的程式風格。然而,把pi繼續當做一個指標來使用,仍然是可以的。

//ok :pi仍然指向合法空間
//  甚至即使儲存於其中的object已經不合法
if(pi==sentinel)........

使用指標pi和使用pi所指物件,其差別在於哪一個的生命已經結束了。雖然該地址上的物件不再合法,但地址本身卻仍然代表一個合法的程式空間。因此pi能夠被繼續使用,但只能在受限的情況下。

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

Point3d  *origin=new Point3d();

上面的程式碼會被轉換為:

Point3d *origin;
// c++虛擬碼
if(origin=__new(sizeof(Point3d)))
	origin=Point3d::Point3d(origin);

如果出現excepting handling(異常處理),那麼轉換結果會更加複雜:

//c++虛擬碼
if(origin==__new(sizeof(Point3d))){
	try{
	origin=Point3d::Point3d(origin);
	}
	catch(....){
	//呼叫delete library function 以
	//釋放因new 而配置的記憶體
	__delete(origin);
	//將原來的exception上傳
	throw;
	}
}

在這裡,如果以new運算子配置object,而其constructor丟出一個exception,配置得來的記憶體就會被釋放掉,然後exception在被丟出去(上傳)。
Destructor的應用極為類似:

delete origin;

就會變成:

if(origin!=0)
	//c++虛擬碼
	Point3d::~Point3d(origin);
	__delete(origin);

一般的library對於new運算子的實現操作都很直接了當,但有兩個精巧之處:

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的呼叫都必須傳回一個獨一無二的指標,解決該問題的傳統方法是傳回一個指標,指向一個預設為1-byte的記憶體區塊。這個實現技術的另一個有趣之處是,它允許使用者提供一個屬於自己的_new_handler()函式,所以才每一次迴圈都呼叫_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陣列的操作,只是單純地獲得記憶體和釋放記憶體而已。這些操作由new和delete運算子來完成就綽綽有餘。
然而如果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);

只有已經構造妥當的元素才需要destructor的施行,因為他們的記憶體已經被配置出來了,vec_new()有責任在exception發生的時候把那些記憶體釋放掉。
當我們寫下:

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

那麼當我們需要刪除陣列時,可以這樣寫:

delete[ ] p_array;

只有在中括號出現時,編譯器才尋找陣列的維度,否則它便假設只有單獨一個objects要被刪除,如果沒有提供中括號:

delete p_array;

那麼只有第一個元素會被解構,其他的元素仍然存在。

那麼編譯器如何記錄元素數目呢?一個明顯的方法就是為vec_new()所傳回的每一個記憶體區塊配置一個額外的word,然後把元素數目包藏在那個word之中。通過這種被包藏的數值被稱為所謂的cookie。cookie策略有一個普遍引起憂慮的話題就是,如果一個壞指標應該被交給delete_vec(),那麼取出來的cookie自然是不合法的。一個不合法的元素數目和一個錯誤的起始地址,會導致destructor以非預期的次數被施行於一段非預期的區域。

如果我們配置一個數組,內帶有10個Point3d objects,我們會預期Point和Point3d的constructor被呼叫各10次,每次作用於陣列的一個元素:

//完全不是一個好主意
Point *ptr=new Point3d[10];

而當我們delete“由ptr所指向的10個Point3d元素時”,很明顯的是,我們需要虛擬機制的幫助,以獲得預期的Point destructor和Point3d destructor,每一次作用於陣列中的每一個元素:

//   超出預期,只有Point::~Point被呼叫。。。
delete[ ] ptr;

施行於陣列上的destructor,是根據交給vec_delete()函式之“被刪除的指標型別的destructor”-----本例中正式Point destructor,這很明顯並非我們希望的。此外,每一個元素的大小也一併被傳遞過去,這就是vec_delete()如何迭代走過每一個數組元素的方式。本例中被傳遞過去的是Point class object的大小而不是Point3d class object的大小。
我們應該避免以一個base class 指標指向一個derived class objects所組成的陣列------如果derived class object 比其base 大的話,那麼就只能程式設計師手動來處理:

for (int ix=0;ix<elem_count;++ix)
{
		Point3d *p=&((Point3d*)ptr)[ix];
		delete p;
}

程式設計師必須迭代走過整個陣列,把delete運算子實施於每一個元素身上,呼叫操作將是virtual ,所以,Point3d和Point的destructor都會施行於陣列中的每一個objects身上。

Placement Operator new的語意

有一個預先定義好的過載的new運算子,稱為placement operator new,它需要第二個引數,型別為void*,呼叫方式如下:

Point2w *ptw=new (arena) Point2w;

其中arena指向記憶體中的一個區塊,用以放置新產生出來的Point2w object,這個預先定義好的placement operator new的實現方法簡直出乎意料的平凡,它只要將“獲得的指標”所指的地址傳回即可:

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

如果只是傳回其第二個引數,那麼它的價值?

  • 1、什麼是placement new operator能夠有效執行的另一半部擴充(而且是“arena的明確指定操作(explicit assignment)”所沒有提供的)?
  • 2、什麼是arena指標的真正型別?該型別暗示了什麼?
    placement new operator所擴充的另一半邊是將Point2w constructor自動施於arena所指的地址上
//c++  虛擬碼
Point2w *ptw=(Point2w*) arena;
if(ptw!=0)
	ptw->Point2w::Point2w();

這正是使placement operator new威力如此強大的原因,這一份碼決定objects被放置在哪裡,編譯系統保證object的constructor會施行於其上。

臨時性物件

如果我們有一個函式,形式如下:

T operator+(const T&,const T&);
以及兩個T objects ,a和b,那麼:
a+b;

可能會導致一個臨時性物件,以放置傳回的物件。是否產生臨時性物件是根據編譯器和操作發生時的上下文關係。
例如

T a,b;
T c=a+b;

有三種方式:

  • 1、編譯器會產生一個臨時性物件,放置a+b的結果,然後再使用T的copy constructor,把該臨時性物件當做c的初始值。
  • 2、可能會直接以拷貝構造的方式,將a+b的值放到c中,於是不需要臨時性物件,以及對其constructor和destructor的呼叫。
  • 3、視operator+()的定義而定,named return value優化也可能實施起來,這將導致直接在上述c物件中求表示式結果,避免執行copy constructor和具名物件的destructor。
    幾乎所有的c++編譯器保證任何表示式,如果有這種形式:
T c=a+b;

而其中的加法運算子被定義為:

T operator+(const T&,const T&);
或
T T::operator(const T&);

那麼實現時根本不產生一個臨時性物件。
而意義相當的assignment敘述句:

c=a+b;

不能夠忽略臨時性物件。它會導致下面的結果:

//c++虛擬碼
//  T  temp =a+b;
T  temp;
temp.operator(a,b);   (1)
// c=temp
c.operator=(temp);
temp.T::~T();

標記為(1)的那一行,未構造的臨時物件被賦值給operator+(),這意思是要不是“表示式的結果被copy constructed 至臨時物件中”,就是“以臨時物件取代NRV”。
不管哪一種情況,直接傳遞c到運算子函式中是有問題的。由於運算子函式並不為其外加引數呼叫一個destructor,所以必須在此呼叫之前先呼叫destructor。

所以初始化操作:

T c=a+b;
總是比下面的操作更有效率地被編譯器轉換:
c=a+b;