深度探索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;