想象一下咱們有下面這個簡單的式子:node
if( yy == xx.getValue() ) ...
其中xx和yy定義爲:程序員
X xx; Y yy;
class Y定義爲:express
class Y { public: Y(); ~Y(); bool operator==( const Y& ) const; // ... };
class X定義爲:數組
class X { public: X(); ~X(); operator Y() const; // conversion運算符 // ... };
先看看開始那個表達式該如何處理。安全
首先,讓咱們決定equality(等號)運算符所參考到的真正實例。在這個例子中,它將被決議cookie
爲「被overloaded的Y成員實例」。下面是該式子的第一次轉換:ide
// resolution of intended operator if( yy.operator==( xx.getValue() ) )
Y的equality(等號)運算符須要一個類型爲Y的參數,然而getValue()傳回的倒是一個類型函數
爲X的object。若非有什麼辦法能夠把一個X object轉換爲一個Y object,那麼這個式子就算錯!測試
本例中X提供一個conversion運算符,把一個X object轉換爲一個Y object。它必須施行於優化
getValue()的返回值上。下面是該式子的第二次轉換:
// conversion of getValue()'s return value if( yy.operator==( xx.getValue().operator Y() ) )
到目前爲止所發生的一切都是編譯器根據class的隱含語意,對咱們的程序代碼所作的增胖
操做。若是咱們須要,咱們也能夠明確地寫出那樣的式子。不過不建議,這樣作會使編譯速度
稍微快一些。
雖然程序的語意是正確的,其教育性卻尚不能說是正確的。接下來咱們必須產生一個臨時對
象,用來放置函數調用所傳回的值:
1)產生一個臨時的class X object,放置getValue()的返回值:
X temp1 = xx.getValue();
2)產生一個臨時的class Y object,放置operator Y()的返回值:
Y temp2 = temp1.operator Y();
3)產生一個臨時的int object,放置equality(等號)運算符的返回值:
int temp3 = yy.operator==( temp2 );
最後,適當的destructor將被施行於每個臨時性的class object身上。這致使咱們的式子被
轉換爲如下形式:
// C++僞碼 // 如下是條件句if( yy == xx.getValue() ) ... 的轉換 { X temp1 = xx.getValue(); Y temp2 = temp1.operator Y(); int temp3 = yy.operator==( temp2 ); if( temp3 ) ... temp2.Y::~Y(); temp1.X::~X(); }
這是C++的一件困難事情:不太容易從程序源碼看出表達式的複雜度。下面就是執行期所發
生的一些轉換。
通常而言,constructor和destructor的安插都如你所預期的那樣:
// C++僞碼 { Point point; // point.Point::Point() 通常而言會安插在這裏 ... // point.Point::~Point() 通常而言會被安插在這裏 }
若是一個區段({}括起來的區域)或函數中有一個以上的離開點,狀況會稍微混亂一些。
Destructor必須被放在每個離開點(當時object還存活)以前,例如:
{ Point point; // constructor 在這裏行動 switch( int( Point.x() ) ) { case -1: // mumble; // destructor 在這裏行動 return; case 0: // mumble; // destructor 在這裏行動 return; case 1: // mumble; // destructor 在這裏行動 return; default: // mumble; // destructor 在這裏行動 return; } // destructor 在這裏行動 }
在這個例子中,point的destructor必須在switch指令4個出口的return操做前被生成出來。另
外也頗有可能在這個區段的結束符號(右大括號)以前被生成出來——即便程序分析的結果發
現毫不會進行到那裏。
一樣的道理,goto指令也可能須要許多個destructor調用操做。例以下面的程序片斷:
{ if( cache ) // 檢查cache;若是吻合就傳回1 return 1; Point xx; // xx的constructor 在這裏行動 while( cvs.iter( xx ) ) if( xx == value ) goto found; // xx的destructor 在這裏行動 return 0; found: // cache item // xx的destructor 在這裏行動 return 1; }
Destructor調用操做必須被放在最後兩個return指令以前,可是卻沒必要被放在最初的return之
前,那固然是由於那時object還沒有被定義出來!
通常而言咱們會把object儘量放置在使用它的那個程序區段附近,這麼作能夠節省非必要
的對象產生操做和摧毀操做。以本例而言,若是咱們在檢查cache以前就定義了Point object,
那就不夠理想。這個道理彷佛很是明顯,但許多Pascal或C程序員使用C++的時候,仍然習慣把
全部的objects放在函數或某個區段的起始處。
若是咱們有如下程序片斷:
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。所以在下面這段代碼中:
int v1 = 1024; int v2;
v1和v2都被配置於程序的data segment,v1值爲1024,v2值爲0(這和C略有不一樣,C並不
自動設定初值)。在C語言中一個global object只可以被一個常量表達式(可在編譯時期求其值
的那種)設定初值。固然,constructor並非常量表達式。雖然class object在編譯時期能夠被
放置於data segment中而且內容爲0,但constructor一直要到程序啓動(startup)時纔會實施。
必須對一個「放置於program data segment中的object的初始化表達式」作評估,這正式爲何一
個object須要靜態初始化的緣由。
當cfront仍是惟一的C++編譯器,並且跨平臺移植性比效率的考慮更重要的時候,有一個可
移植但成本頗高的靜態初始化(以及內存釋放)方法,稱爲munch。cfront的束縛是,它的解決
方案必須在每個UNIX平臺上——從Cray到VAX,再從Sun到UNIX PC——都有效。所以不論
是相關的linker或object-file format,都不能預先作任何假設。因爲這樣的限制,下面這些munch
策略就浮現出來了:
1)爲每個須要靜態初始化的文件產生一個_sti()函數,內含必要的constructor調用操做或
inline expansions。例如前面所說的identity對象會在matrix.c中產生出下面的_sti()函數(sti就是
static initialization的縮寫):
_sti_matrix_c_identity() { // C++代碼 identity.Matrix::Matrix(); // 這就是static initialization }
其中matrix_c是文件名編碼,_identity表示文件中所定義的一個static object。在_sti以後附
加上這兩個名稱,能夠爲可執行文件提供一個獨一無二的標識符號。
2)相似狀況,在每個須要靜態的內存釋放操做(static deallocation)的文件中,產生一
個_std()函數(std就是static deallocation的縮寫),內含必要的destructor調用操做,或是其
inline expansions。在咱們的例子中會有一個_std()函數被產生出來,針對identity對象調用
Matrix destructor。
3)提供一組runtime library 「munch」函數:一個_main函數(用以調用可執行文件中的全部
_sti()函數),以及一個exit()函數(用相似的方式調用全部的_std()函數)。
cfront在程序中安插一個_main()函數調用操做,做爲main()函數的第一個指令。這裏的exit
和C library的exit()不一樣,爲了連接前者,在cfront的CC命令中必須先指定C++ standard
library。通常而言這樣就能夠了,但有些編譯系統拒絕munch exit()函數。
最後一個須要解決的問題是,如何收集一個程序中各個object files的_sti()函數和_std()函
數。它必須是可移植的——雖然移植性限制在UNIX平臺。
解決辦法是使用nm命令。nm會傾印(dump)出object file的符號表格項目(symbol table
entries)。一個可執行文件是由.o文件產生出來的,nm將施行於可執行文件身上。其輸出被導
入(「piped into」)munch程序中。munch程序會分析符號表格中的名稱,搜尋以_sti或_std開頭
的名稱,而後把函數名稱加到一個sti()函數和std()函數的跳離表格(jump table)中。接下來它
把這個表格寫到一個小的program text文件中,而後,CC命令被從新激活,將這個內含表格的
文件加以編譯。整個可執行文件而後被從新連接。_main()和exit因而在各個表格上走訪一遍,
輪流調用每個項目(表明一個函數地址)。
這個作法能夠解決問題,但彷佛離正統的計算機科學遠了一些。其修補版(patch)假設
可執行文件是System V COFF(Common Object File Format)格式,因而它檢驗可執行文件並
找出那些「有着_link node」並內含一個指針,指向_sti()和_std()函數「的文件,將它們通通串鏈在
一塊兒。接下來它把鏈表的根源設爲一個全局性的_head object(定義於新的patch runtime library
中)。這個patch library內含另外一個不一樣的_main()函數和exit()函數,將以_head爲起始的鏈表走
訪一遍。最後針對Sun、BSD以及ELF的其餘patch libraries終於也有各個使用者團體捐贈出
來,用以和各式各樣的cfront版本搭配。
當特定平臺上的C++編譯器開始出現,更有效率的方法也就有可能隨之出現,由於各平臺有
可能擴充連接器和目的文件格式(object file format),以求直接支持靜態初始化和內存釋放操
做。例如,System V的Executable and Linking Format(ELF)被擴充以增長支持.init和.fini兩
個section,這兩個sections內含對象所須要的信息,分別對應於靜態初始化和釋放操做。編譯
器特定(Implementation-specific)的startup函數(一般名爲crt0.o)會完成平臺特定
(platform-specific)的支持(分別針對靜態初始化和釋放操做的支持)。
cfront 2.0版以前並不支持nonclass object的靜態初始化操做:也就是說C語言的限制仍然殘
留着。因此,像下面這樣的例子,每個初始化操做都被標示爲不合法:
extern int i; // 所有都要求靜態初始化(static initialization) // 在2.0版之前的C和C++中,這些都是不合法的 int j = i; int *pi = new int( i ); double sal = compute_sal( get_emplopyee( i ) );
支持「nonclass objects的靜態初始化」,在某種程度上,是支持virtual base classes的一個副
產品。virtual base classes怎麼會扯進這個主題呢?以一個derived class的pointer或reference
來存取virtual base class subobject,是一種nonconstant expression,必須在執行期才能加以
評估求值。例如,儘管下列程序片斷在編譯器時期可知:
// constant expression Vertex3d *pv = new PVertex; Point3d *p3d = pv;
其virtual base class Point的subobject在每個derived class中位置卻可能會變更,所以不
可以在編譯時期設定下來。下面的初始化操做:
// Point是Point3d的一個virtual base class // pt的初始化操做須要 // 某種形式的執行期評估(runtime evaluation) Point *pt = p3d;
須要編譯器提供內部擴充,以支持class object的靜態初始化(至少涵蓋class objects的指針
和reference)。例如:
// Initial support of virtual base class conversion // requires non-constant initialization support Point *pt = p3d->vbcPoint;
提供必要的支持以涵蓋全部的nonclass objects,並不須要走太遠的路。
使用被靜態初始化的objects,有一些缺點。例如,若是exception handling被支持,那些
objects將不能被放置於try區段以內。這對於被靜態調用的constructors多是特別沒法接受的,
由於任何的throw操做將必然觸發exception handling library默認的terminate()函數。另外一個缺點
是爲了控制「須要跨越模塊作靜態初始化」之objects的相依順序,而扯出來的複雜度。建議根本
不要用那些須要靜態初始化的global objects。
假設咱們有如下程序片斷:
const Matrix& identity() { static Matrix mat_identity; // ... return mat_identity; }
Local static class object保證了什麼樣的語意?
1)mat_identity的constructor必須只能施行一次,雖然上述函數可能會被調用屢次。
2)mat_identity的destructor必須只能施行一次,雖然上述函數可能會被調用屢次。
編譯器的策略之一就是,無條件地在程序起始(startup)時構造出對象來。然而這會致使所
有的local static class objects都在程序起始時被初始化,即便它們所在的那個函數從未曾被調用
過。所以,只在identity()被調用時才把mat_identity構造起來,是比較好的作法(如今的C++
Standard已經強制要求這一點)。咱們應該怎麼作呢?
如下是在cfront之中的作法。首先,導入一個臨時性對象以保護mat_identity的初始化操做。
第一次處理identity()時,這個臨時對象被評估爲false,因而constructor會被調用,而後臨時對
象被改成true。這樣就解決了構造的問題。而在相反的那一端,destructor也須要有條件地施於
mat_identity身上,但只有在mat_identity是否被構造起來,很簡單,若是那個臨時對象爲true,
就表示構造好了。困難的是,因爲cfront產生C碼,mat_identity對函數而言仍然是local,所以我
沒辦法在靜態的內存釋放函數(static deallocation function)中存取它。解決辦法優勢詭異,結
構化語言避之惟恐不及:取出local object的地址。(因爲object是static,其地址在downstream
component中將會被轉換到程序用來放置global object的data segment中)。下面是cfront的輸
出:
// 被產生出來的臨時對象,做爲戒護之用 static struct Matrix *_0_F3 = 0; // C++的reference在C中是以pointer來代替的 // identity()的名稱會被mangled struct Matrix* identity_Fv() { // _1 反映出語彙層面的設計, // 使得C++得以支持這樣的代碼: // int val; // int f() { int val; // return val + ::val; } // 最後一行會變成: // ... return _lval + val; static struct Matrix _lmat_identity; // 若是臨時性的保護對象已被設立,那就什麼也別作,不然 // (a) 調用constructor:_ct_6MatrixFv // (b) 設定保護對象,使它指向目標對象 _0_F3 ? 0 : ( _ct_6MatrixFv( &_lmat_identity ), ( _0_F3 = ( &_lmat_identity ) ) ); ... }
最後,destructor必須在「與text program file(也就是本例中的stat_0.c)有關聯的靜態內存內
存釋放函數 (static deallocation function)「中被有條件地調用:
char _std_stat_0_c_j() { _0_F3 ? _dt_6MatrixFv( _0_F3, 2 ) : 0; ... }
請記住,指針的使用是cfront所特有的:然而條件式析夠則是全部編譯器都須要的。C++標
準委員會新的規則要求編譯單位中的static local class objects必須被摧毀——以構造的的相反順
序摧毀。因爲這些objects是在須要時才被構造(例如每個含有static local class objects的函
數第一次被進入時),因此編譯時期沒法預期其集合以及順序。爲了支持新的規則,可能須要
對被產生的static class objects保持一個執行期鏈表。
假設咱們有下列的數組定義:
Point knots[ 10 ];
若是Point既沒有定義一個constructor也沒有定義一個destructor,那麼咱們的工做不會比建
立一個」內建(build-in)類型所組成的數組「更多,也就是說咱們只要配置足夠內存以存儲10個
連續的Point元素便可。
然而Point的肯定義了一個default destructor,因此這個destructor必須輪流施行於每個元
素之上。通常而言這是經由一個或多個runtime library函數達成的。在cfront中,咱們使用一個被
命名爲vec_new()的函數,產生出以class objects構造而成的數組。比較新近的編譯器,包括
Borland、Microsoft和Sun,則是提供兩個函數,一個用來處理」沒有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持有的若不是具名數組(本例爲knots)的地址,就是0。若是是0,那麼數組
將經由應用程序的new運算符,被動態配置於heap中。Sun把」由class objects所組成的具名數
組「和」動態配置而來的數組「的處理操做分爲兩個library函數:_vector_new2和_vector_con,它
們各自擁有一個virtual base class函數實例。
參數elem_size表示數組中的元素個數。在vec_new()中,constructor施行於elem_count個元
素上,對於支持exception handling的編譯器而言,destructor的提供是必要的。下面下面是編
譯器可能針對咱們的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_vdelete()——若是classes
擁有virtual base classes的話)的runtime library函數完成(Sun對於」具名數組「和」動態配置而
來的數組「,處理方式不一樣)的,其函數類型以下:
void* vec_delete( void *array, // 數組起始地址 size_t elem_size, // 每個class object的大小 int elem_count, // 數組的元素個數 void ( *destuctor )( 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 );
四、Default Constructors和數組
若是想要在程序中取出一個constructor的地址,是不能夠的。固然,這是編譯器在支持vec_new()時該作的事情。然而, 經由一個指針來啓動constructor,將沒法(不被容許)存取default argument values。
例如,在cfront2.0以前,聲明一個由class objects所組成的數組,意味着這個class必須沒有
聲明constructors或一個default constructor(沒有參數那種)。一個constructor不能夠取一個或
一個以上的默認參數值。這是違反直覺的,會致使如下的大錯。下面是cfront 1.0中對於負數函
數庫(complex library)的聲明,能看出其中的錯誤?
class complex { complex( double = 0.0, double = 0.0 ); ... }
在當時的語言規則下,此複數函數庫的使用者沒有辦法聲明一個由complex class objects組
成的數組。顯然咱們在語言的一個陷阱上被絆倒了。在1.1版,修改的是class library;然而在2.0
版,修改是語言自己。
再一次,如何支持如下句子:
complex::complex( double = 0.0, double = 0.0 );
當程序員寫出:
complex c_array[ 10 ];
時,而編譯器最終須要調用:
vec_new( &c_array, sizeof( complex ), 10, &complex::complex, 0 );
默認參數如何可以對vec_new()而言有用?
很明顯,有數種可能的實現方法。cfront所採用的方法是產生一個內部的stub constructor,
沒有參數。在其函數內調用由程序員提供的constructor,並將default參數值顯示地指定過去
(因爲constructor的地址已被取得,因此它不可以成一個inline):
// 內部產生的stub constructor // 用以支持數組的構造 complex::complex() { complex( 0.0, 0.0 ); }
編譯器本身又一次違反了一個明顯的語言規則:class現在支持了兩個沒有帶參數的
constructors。固然當class object數組真正被產生出來時,stub實例纔會被產生以及被調用。
運算符new的使用,看起來彷佛時單一運算,像這樣:
int *pi = new int( 5 );
但事實上是由兩個步驟完成的:
1)經過適當的new運算符函數實例,配置所需的內存:
// 調用函數庫中的new運算符 int *pi = _new( sizeof( int ) );
2)將配置得來的對象設立初值:
*pi = 5;
更進一步地說,初始化操做應該在內存配置成功(經由new運算符)後才執行:
// new運算符的兩個分離步驟 // given: int *pi = new int( 5 ); // 重寫聲明 int *pi; if( pi = _new( sizeof( int ) ) ) *pi = 5; // 成功了才初始化
delete運算符的狀況相似。當寫下:
delete pi;
時,若是pi的值是0,C++語言會要求delete運算符不要有操做。所以編譯器必須爲此調用構
造一層保護膜:
if( pi != 0 ) _delete( pi );
請注意pi並不會所以被自動清除爲0,所以像這樣的後繼行爲:
// 沒有良好的定義,可是合法 if( pi && *pi == 5 ) ...
雖然沒有良好的定義,可是可能(也可能不)被評爲真。這是由於對於pi所指向以內存的變
更或再使用,可能(也可能不)會發生。
pi所指對象的生命會因delete而結束。因此後繼任何對pi的參考操做就再也不保證有良好的行
爲,並所以被視爲一種很差的程序風格。然而,把pi繼續當作一個指針來用,仍然是能夠的
(雖然其使用受到限制),例如:
// ok:pi仍然指向合法空間 // 甚至即便存儲於其中的object已經再也不合法 if( pi == sentinel ) ...
在這裏,使用指針pi,和使用pi所指的對象,其差異在於哪個的生命已經結束了。雖然該
地址上的對象再也不合法,地址自己卻仍然表明一個合法的程序空間。所以pi可以繼續被使用,
但只能在受限制的狀況下,很像一個void*指針的狀況:
以constructor來配置一個class object,狀況相似。例如:
Point3d *origin = new Point3d;
被轉換爲:
Point3d *origin; // C++ 僞碼 if( origin = _new( sizeof( Point3d ) ) ) origin = Point3d::Point3d( origin );
若是實現出exception 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 ); }
若是在exception handling的狀況下,destructor應該被放在一個try區段中。exception
handler會調用delete運算符,而後再一次拋出該exception。
通常的library對於new運算符的實現都很直截了當,可是兩個精巧之處值得斟酌(如下版本
並未考慮exception handling):
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的調用都必須傳回一個獨一無二的指針。解決此問題的傳統方法是
傳回一個指針,指向一個默認爲1-byte的內存區塊(這就是爲何程序代碼中size被設爲1的原
因)。這個實現技術的另外一個有趣之處是,它容許使用者提供一個屬於本身的_new_hander()函
數。這正是爲何每一次循環都調用_new_hanlder()之故。
new運算符實際上老是以標準的C malloc()完成,雖然並無規定必定得這麼作不可。相同
狀況,delete運算符也老是以標準的C free()完成:
extern void operator delete( void *ptr ) { if( ptr ) free( ( char* )ptr ); }
當咱們這麼寫:
int *p_array = new int[ 5 ];
時,vec_new()不會真正被調用,由於它的主要功能是把default constructor施行於class
objects所組成的數組的每個元素身上。卻是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 = vew_new( 0, sizeof( Point3d ), 10, &Point3d::Point3d, &Point3d::~Point3d );
在個別的數組元素構造過程當中,若是發生excpetion,destructor就會傳遞給vec_new()。只有
已經構造穩當的元素才須要destructor的施行,由於它們的內存已經被配置出來了,vec_new()
有責任在exception發生的時機把那些內存釋放掉。
在C++2.0版本以前,將數組的真正大小提供給程序的delete運算符,是程序員的責任。所以
若是咱們原先寫下:
int array_size = 10; Point3d *p_array = new Point3d[ array_size ];
那麼咱們就必須對應地寫下:
delete [ array_size ] p_array;
在2.1版中,這個語言有了一些函數,程序員再也不須要在delete時指定數組元素的個數,所以
咱們如今能夠這樣寫:
然而爲了回溯兼容,兩種形式均可以接受。支持。支持此種新形式的第一個編譯器固然就是
cfront。這項技術支持須要知道的首先是指針所指的內存空間,而後時其中的元素個數。
尋找數組維度,對於delete運算符的效率帶來極大的衝擊,因此才致使這樣的妥協:只有在中
括號出現時,編譯器才尋找數組的維度,不然它便假設只有單獨一個objects要被刪除。若是程
序員沒有提供必須的中括號,像這樣:
delete p_array;
那麼就只有第一個元素會被析構。其餘的元素仍然存在——雖然其相關的內存已經被要求歸
還了。
各家編譯器之間存在一個有趣的差別,那就是元素個數若是被顯示指定,是否被拿去利用。
在Jonathan的原始版本中,優先採用使用者(程序員)顯式指定的值。下面是他所寫的原始碼
的虛擬版本(pseudo-version),附帶註釋:
// 首先檢查是狗最後一個被配置的項目(_cache_key) // 是目前要被delete的項目 // // 若是是,就不須要作搜尋操做了 // 若是不是,就尋找元素個數 int elem_count = _cache_key == pointer ? ( ( _cache_key = 0 ), _cache_cout ) : // 取出元素個數 // num_elem: 元素個數,將傳遞給vec_new()。 // 對於配置於heap中的數組,只有面對如下形式,纔會設定一個值: // delete [10] ptr; // 不然cfront會傳-1以表示取出。 if( num_elem == -1 ) // prefer explicit user size if choice! num_elem = ans;
然而幾乎新近全部的C++編譯器都不考慮程序員的顯示指定(若是有的話)。
此一性質被導入的時候,沒有任何程序代碼會不「顯示指定數組大小」。時代演化到
cfront4.0,咱們會把此習慣貼上「落伍」的標記,而且產生一個相似的警告信息。
應該如何記錄元素個數?一個明顯的方法就是爲vec_new()所傳回的每個內存區塊配置
一個額外的word,而後把元素個數包藏在那個word之中。一般這種包藏的數值稱爲所謂的
cookie。然而,Jonathan和Sun編譯器決定維護一個「聯合數組(associative array)」,放置指
針及大小。Sun也把destructor的地址維護於數組之中。
cookie策略有一個廣泛引發憂慮的話題就是,若是一個壞指針應該被交給delete_vec(),
取出來的cookie天然是不合法的。一個不合法的元素個數和一個壞的起始地址,會致使
destrcutor以非預期的次數被施行於一段非預期的區域。然而在「聯合數組」的政策之下,壞指針
的可能結果就只是取出錯誤的元素個數而已。
在原始編譯器中,有兩個主要函數用來存儲和取出所謂的cookie:
// array_key是新數組的地址 // mustn't either be 0 or already entered // elem_count is the count;it may be 0 typedef void *PV; extern int _insert_new_array( PV array_key, int elem_count ); // 從表格中取出(並去除)array_key // 若不是傳回elem_count,就是傳回-1 extern int _remove_old_array( PV array_key );
下面是cfront中的vec_new()原始內容通過脩潤後的一份呈現,並附加註釋:
PV _vec_new( PV ptr_array, int elem_count, int size, PV construct ) { // 若是ptr_array是0,從heap之中配置數組。 // 若是ptr_array不是0,表示程序員寫的是: // T array[ count ] // 或 // new ( ptr_array ) T[ 10 ]; int alloc = 0; // 咱們要在vec_new中配置嗎? int array_sz = elem_count * size; if( alloc = ptr_array == 0 ) // 全局運算符 new ... ptr_array = PV( new char[ array_sz ] ); // 在exception handling之下: // 將拋出exception bad_alloc if( ptr_array == 0 ) return 0; // 把數組元素個數放到cache中 int status = _insert_new_array( ptr_array, elem_count ); if( status == -1 ) { // 在exception handling之下將拋出exception // 將拋出exception bad_alloc if( alloc ) delete ptr_array; return 0; } if( construct ) { register char* elem = ( char* )ptr_array; register char* lim = elem + array_sz; // PF是一個typedef,表明一個函數指針 register PF fp = PF( constructor ); while( elem < lim ) { // 經過fp調用constructor做用於 // ‘this’元素上(由elem指出) ( *fp )( ( void* )elem ); // 前進到下一個元素 elem += size; } } return pV( ptr_array ); }
vec_delete()操做差很少,但其行爲不老是C++程序員所預期或需求的。例如,已知下面兩
個處理class聲明:
class Point { public: Point(); virtual ~Point(); // ... }; class Point3d : public Point { public: Point3d(); virtual ~Point3d(); // ... };
若是咱們配置一個數組,內含10個Point3d objects,咱們會預期Point和Point3d的
constructor被調用各10次,每次做用於數組的一個元素:
// 徹底不是個好主意 Point *ptr = new Point3d[ 10 ];
而當咱們delete「由ptr所指向10個Point3d元素」時,會發生什麼事情?很明顯,咱們須要虛
擬機制的幫助,以得到預期的Point destructor和Point3d destructor各10次的調用(每一次做用
於數組中的一個元素):
// 這並非咱們所須要的 // 只有Point::~Point被調用…… delete [] ptr;
施行於數組上數組上的destructor,如咱們所見,是根據交給vec_delete()函數的「被刪除之
指針類型的destructor」——本例中正是Point destructor。這很明顯並不是咱們所但願。此外,每一
個元素的大小也一併被傳遞過去。這就是vec_delete()如何迭代走過每個數組元素的方式。本
例中被傳遞過去的是Point class object的大小而不是Point3d class object的大小。整個運做過程
很是不幸地失敗了,不僅是由於執行了錯誤的destructor,並且自從第一個元素以後,該
destructor即被施於不正確的內存區塊中(由於元素的大小不對)。
最好是避免以一個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身上。
有一個預先定義好的重載的(overload)new運算符,稱爲placement operator new。它需
要第二個參數,類型爲void *。調用方式以下:
Point2w *ptw = new( arena ) Point2w;
其中arena指向內存中的一個區塊,用以放置新產生出來的Point2w object。這個預先定義好
的placement operator new的實現方法簡直是出乎意料的平凡。它只要將「得到的指針(上例
arena)」所指的地址傳回便可:
void* operator new( size_t, void* p ) { return p; }
若是它的做用只是傳回第二個參數,那麼它有什麼價值呢?也就是說,爲何不簡單地這麼
寫算了(這不就是實際所發生的操做嗎):
Point2w *ptw = ( Point2w* ) arena;
事實上這只是所發生的操做的一半而已。另一半沒法由程序員產生出來。以下問題:
1)什麼是使placement new operator可以有效運行的另外一半擴充(並且是「arena的顯式指定
操做(explicit assignment)」所沒有提供的)?
2)什麼是areana指針的真正類型?該類型暗示了什麼?
Placement new operator所擴充的另外一半操做是將Point2w constructor自動實施於areana所
指的地址上:
// C++僞碼 Point2w *ptw = ( Point2s* ) arena; if( ptw != 0 ) ptw->Point2w::Point2w();
這正是使placement operator new威力如此強大的緣由。這一份代碼決定objects被放置在哪
裏:編譯系統保證object的constructor會施於其上。
然而卻有一個輕微的不良行爲。下面是一個有問題的程序片斷:
// 讓arena 成爲全局性定義 void 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
掉。不過在此例中若是你像下面這樣作,絕對是個錯誤:
// 如下並非實施destructor的正確方法 delete p2w; p2w = new ( arena ) Point2w;
是的,delete運算符會發生做用,這的確是咱們所期待的。可是它會釋放由p2w所指的內
存,它卻不是咱們所但願的,由於下一個指令就要用到p2w了。所以,咱們應該顯式調用
destructor並保留存儲空間以便再使用:
// 施行destructor的正確方法 p2w->~Point2w; p2w = new ( arena ) Point2w;
剩下的惟一問題是一個設計上的問題:在咱們的例子中對placement operator的第一次調
用,會將新object構造於原已存在的objecct之上嗎?仍是會構造於全新地址上?也就是說,如
果咱們這樣寫:
Point2w *p2w = new ( arena ) Point2w;
咱們如何知道arena所指的這塊區域是否須要先析夠?這個問題在語言層面上並無解答。
一個合理的習俗是令執行new的這一端也要負起執行destructor的責任。
另外一個問題關係到arena所表現的真正指針類型。C++ Standard說它必須指向相同類型的
class,要不就是一塊「新鮮」內存,足夠容納該類型的object。注意,derived class很明顯並不在
被支持之列。對於一個derived class,或是其餘沒有關聯的類型,其行爲雖然並不是不合法,卻
也未經定義。
「新鮮」的存儲空間能夠這樣配置而來:
char *arena = new char[ sizeof( Point2w ) ];
相同類型的object則能夠這樣得到:
Point2w *arena = new Point2w;
不論哪種狀況,新的Point2w的存儲空間的確是覆蓋了arena的位置,而此行爲已在良好
控制之下。然而,通常而言,placement new operator並不支持多態(polymorphsim)。被叫
給new的指針,應該適當地指向一塊預先配置好的內存。若是derived class比其base class大,
例如:
Point2w *p2w = new ( arena ) Point3w;
Point3d的constructor將會致使嚴重的破壞。
Placement new operator被引入C++2.0時,最晦澀隱暗的問題是下面這個:
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; // 1 b.f(); // 哪個f()被調用? }
因爲上述兩個classes有相同的大小,把derived object放在爲base class而配置的內存中
是安全的。然而,欲支持這一點,或許必須放棄對於「經由objects靜態調用全部virtual
functions(像是b.f())」一般都會有的優化處理。結果,placement new operator的這種使用方式
在Standard C++未能得到支持,因而上述程序的行爲沒有明肯定義:咱們不能斬釘截鐵地說哪
一個f()函數實例會被調用。儘管大部分使用者可能覺得調用的是Derived::f(),但大部分編譯器調
用的倒是Base::f()。
若是咱們有一個函數,形式以下:
T operator+( const T&, const T& );
以及兩個T objects,a和b,那麼
a + b;
可能會致使一個臨時性對象,以放置傳回的對象。是否會致使一個臨時性對象,視編譯器
的進取性(aggressiveness)以及上述操做發生時的程序語境(program context)而定。例如
下面這個片斷:
T a, b; T c = a + b;
編譯器會產生一個臨時性放置a+b的結果,而後再使用T的copy constructor,把該臨時性對
象當作c的初始值。然而比較更可能的轉換是直接以拷貝構造的方式將a+b的值放到c中,因而就
不須要臨時對象,以及對其constructor和destructor的調用了。
此外,視operator+()的定義而言,named return value(NRV)優化也可能被實施起來。這
將致使直接在上述c對象中求表達式結果,避免執行copy constructor和具名對象(named
object)的destructor。
三種方式所得到的c對象,結果都同樣。其間的差別在於初始化的成本。一個編譯器可能給
咱們任何保證嗎?嚴格地說是沒有。C++ Standard容許編譯器對於臨時性對象的產生有徹底有
徹底的自由度。
理論上,C++ Standard容許編譯器廠商有徹底的自由度。但實際上,因爲市場的競爭,幾
乎保證任何表達式(expression)若是有這種形式:
T c = a + b;
而其中的加法運算符被定義:
T operator+( const T&, const T& );
或
T T::operator+( const T& );
那麼實現時根本不產生一個臨時性對象。
然而請注意,意義至關的assignment敘述句(statement):
c= a + b;
不可以忽略臨時性對象。反而,它會致使下面的結果:
// C++僞碼 // T temp = a + b; T temp; temp.operator+( a, b ); // (1) // c = temp c.operator=( temp ); // (2) temp.T::~T();
表示爲(1)的那一行,未構造的臨時對象被賦值給operator+()。這意思是否是「表達式的
結果比copy cnstructed至臨時對象中」,就是「以臨時對象取代NRV」。在後者中,本來要施行於
NRV的constructor,如今將施行於此臨時對象。
無論是哪種狀況,直接傳遞c(上例賦值操做的目標對象)到運算符函數中都是有問題
的。因爲運算符函數並不爲其外加參數調用一個destructor(它指望一塊「新鮮的」內存),因此
必須在此調用以前先調用destructor。然而,「轉換」語意被用來將下面的assignment操做:
c = a + b; // c.operator=( a + b );
取代爲其copy assignment運算符的隱式調用操做,以及一系列的destructor和copy
construction:
// C++僞碼 c.T::~T(); c.T::T( a + b );
copy constructor、destructor以及copy assignment operator均可以由使用者供應,因此不
可以保證上述兩個操做會致使相同的語意。所以以一連串的destruction和copy construction來取
代assignment通常而言是不安全的,並且會產生臨時對象。因此這樣的初始化操做:
T c = a + b;
老是比下面的操做更有效率地被編譯器轉換:
c = a + b;
第三種運算形式是,沒有出現目標對象:
a + b; // no target
這時候有必要產生一個臨時對象以放置運算後的結果。雖然看起來有點怪異,但這種狀況
實際上在子表達式(subexpression)中十分廣泛,例如,若是咱們這樣寫:
String s( "hello" ), t( "world" ), u( "!" );
那麼不論:
String v; v = s + t + u;
或
printf( "%s\n", s + t );
最後一個表達式來一些密教式的論題,那就是「臨時對象的生命期」。這個論題頗值得深刻
探討。在Standard C++以前,臨時對象的生命(也就是說他destructor什麼時候實施)並無顯式指
定,而是由編譯廠商自行決定。換句話說,上述的printf(0並不保證安全,由於它的正確性和s+t
什麼時候被摧毀有關。
(本例的一個可能性是,String class 定義了一個conversion運算符以下:
String::operator const char*() { return _str; }
其中_str是一個private member addressing storage,在String object構造時配置,在其
destructor中被釋放。)
所以若是臨時對象在調用printf()以前就被摧毀了,經由conbertion運算符交給它的地址就不
合法。真正的結果視底部的delete運算符在釋放內存時的進取性而定。某些編譯器可能會把這塊
內存標示爲free,不以任何方式改變其內容。在這塊內存被其餘地方宣稱「主權」以前,只要它還
沒有被deleted掉,它就能夠被使用。雖然對於軟件工程而言這不足以做爲模範,但像這樣,在
內存被釋放以後又再被使用,並不是罕見。事實上malloc()的許多編譯器會提供一個特殊的調用操
做:
malloc( 0 );
它正是用來保證上述行爲的。
例如,下面是對於該算式的一個可能的pre-Standard轉化。雖然在pre-Standard語言定義中
是合法的,卻可能形成重大災難:
// C++僞碼:pre-Standard的合法轉換 // 臨時性對象被摧毀得太快了 String temp1 = operator+( s, t ); const char *temp2 = temp1.operator const char*(); // 合法可是有欠考慮,太太輕率 temp1.~String(); // 這時候並未定義temp2指向何方 printf( "%s\n", temp2 );
另外一種轉換方式是在調用printf()以後實施String destuctor。在C++ Standard之下,這正是
該表達式的標準轉換方式。標準規格上這麼說:
臨時性對象的被摧毀,應該是對完整表達式(full-expression)求值過程當中的最後一個步
驟。該完整表達式形成臨時對象的產生。
什麼是一個完整表達式(full-expression)?非正式地說,它是被涵括的表達式中最外圍的
那個。下面這個式子:
// tertiary full expression with 5 sub-expressions ( ( objA > 1024 ) && ( objB > 1024 ) ) ? objA + objB : foo( objA, objB );
一共有五個子算式(subexpressions),內含在一個「?:完整表達式」中。任何一個子表達
式所產生的任何一個臨時對象,都應該在完整表達式被求值完成後,才能毀去。
當臨時性對象是根據程序的執行期語意,有條件地被產生出來時,臨時性對象的生命規則就
顯得有些複雜了。舉個例子,像這樣的表達式:
if( s + t || u + v )
其中的u+v子算式只有在s+t被評估爲false時,纔會開始被評估。與第二個子算式有關的臨
時對象必須被摧毀,可是,不能夠被無條件地摧毀。也就是說,咱們但願只有在臨時性對象必
須被摧毀,可是,很明顯,不能夠被無條件地摧毀。也就是說,咱們但願只有在臨時性對象被
產生出來的狀況下,纔去摧毀它。
在討論臨時對象的生命規則以前,標準編譯器將臨時對象的構造和析構附着於第二個子算
式的評估程序。例如,對於如下的class聲明:
class X { public: X(); ~X(); operator int(); X foo(); private: int val; };
以及對於class X的兩個objects的條件測試:
main() { X xx; Y yy; if( xx.foo() || yy.foo() ) ; return 0; }
cfront對於main()產生出如下的轉換結果:
int main( void ) { struct X _1xx; struct X _1yy; int _0_result; // name_mangled default constructor; // X:X( X *this ) _ct_1xFv( &_1xx ); _ct_1xFv( &_1yy ); { // 被產生出來的臨時對象 struct X _0_Q1; struct X _0_Q2; int _0_Q3; /* 每一端變成一個附逗點的表達式, * 有着下列順序: * * tempQ1 = xx.foo(); * tempQ3 = tempQ1.operator int(); * tempQ1.X::~X(); * tempQ3; */ // _opi_1xFv ==> X::operator int() if (((( _0_Q3 = _opi_1xFv((( _0_Q2 = foo_1xFv( &_1xx ) ), ( &_0_Q1 )))), _dt_1xFv( &_0_Q1, 2 )), _0_Q3 ) || ((( _0_Q3 = _opi_1xFv((( _0_Q2 = foo_1xFv( &_1yy ) ), ( &_0_Q2 )))), _dt_1xFv( &_0_Q2, 2 )), _0_Q3 )) { _0_result = 0; _dt_1xFv( &_1yy, 2 ); _dt_1xFv( &_1xx, 2 ); } return _0_result; } }
把臨時性對象的destructor放在每個子算式的求值過程當中,能夠免除「努力追蹤第二個子算
式是否真的須要被評估」。然而在C++ Standard的臨時對象生命規則中,這樣的策略再也不被允
許。臨時性對象在完整表達式還沒有評估徹底以前,不得被摧毀。也就是說某些形式的條件測試
如今必須被安插進來,以決定是否要摧毀和第二算式有關的臨時對象。
臨時性對象的生命規則有兩個例外。第一個例外發生在表達式被用來初始化一個object時,
例如:
bool verbose; ... string progNameVersion = !verbose ? 0 : progName + progVersion;
其中progName和progVersion都是String objects。這時候會生出一個臨時對象,放置加法
運算符的運算結果:
String operator+( const String&, const String& );
臨時對象必須根據對verbose的測試結果,有條件地析構。在臨時對象的生命規則之下,它
應該在完整的」?:表達式「結束評估以後儘快被摧毀。然而,若是progNameVersion的初始化需
要調用一個copy constructor:
// C++僞碼 proNameVersion.String::String( temp );
那麼臨時性對象的析構(在」?:完整表達式「以後)固然就不是咱們所指望的。C++
Standard要求說:
......凡持有表達式執行結果的臨時性對象,應該存留到object的初始化操做完成爲止。
甚至即便每個人都堅守C++ Standard中的臨時對象生命規則,程序員仍是可能對象在他
們的控制中被摧毀。其間的主要差別在於這時候的行爲有明確的定義。例如,在新的臨時對象
生命規則中,下面這個初始化操做保證失敗:
// 不是個好主意 const char *progNameVersion = progName + proVersion;
其中progName和progVersion都是String objects。產生出來的程序代碼看起來像這樣:
// C++ pseudp Code String temp; operator+( temp, progName, progVersion ); progNameVersion = temp.String::operator char*(); temp.String::~String();
此刻progNameVersion指向未定義的heap內存!
臨時性對象的生命規則的第二個例外是」當一個臨時性對象被一個reference綁定「時,例如:
const String &space = " ";
產生出像這樣的程序代碼:
// C++ pseudo Code String temp; temp.String::String( " " ); const String &space = temp;
很明顯,若是臨時性對象如今被摧毀,那麼reference也就差很少沒什麼用了。因此規則上
說:
若是一個臨時性對象被綁定於一個reference,對象將殘留,直到被初始化之reference的生命
結束,或直到臨時對象的生命範疇(scope)結束——視哪種狀況先到達而定。
臨時對象的優化,反聚合(disaggregation)的優化。