在托管堆上分配對象實例,似乎是使用托管擴展C++、C#、J#、VB.NET程序員的唯一方法,而使用本地C++的程序員,不但可以在堆上分配內存,甚至更慣於使用基於堆棧的對象實例。
現在回顧一下以前定義的Point引用類,再來看一下以下變量定義:
Point p1, p2(3,4);
從本地C++的角度來說,p1與p2應為基於堆棧的引用類Point實例,哪怕是從一般性的角度來看,它們也是。P1由默認的構造函數初始化,而p2由接受x與y坐標的構造函數初始化。從實現上來看,Point是自包含類型的(也就是說,它不包含任何指針或句柄),然而,作為一個引用類的實例,它仍處於CLI運行時的掌控之下,且在必要時,會被垃圾回收--正因為此,所以不能定義一個引用類的靜態或全局實例。
同時,也不能將sizeof應用於指明是引用類實例的表達式,因為sizeof是在編譯時進行計算的,而Point對象的大小要直到運行時才能確定;但是,可將sizeof應用於句柄,因為它的大小在編譯時就已經確定了。
另外,還不能定義一個基於堆棧的CLI數組實例。
跟蹤引用
本地C++可通過&來定義一個對象的別名,例如,對任意本地類N,可編寫如下代碼:
N n1;
N& n2 = n1;
引用必須在定義時進行初始化,且在整個生命期中,它們都鎖定於引用同一對象,也就是說,它的值不會改變。引用一個引用類的實例與引用一個本地類基本一致,只不過語法不同而已。
在程序執行期間,引用類的實例會在內存中"移動",所以,需要對它們進行跟蹤,而本地指針與引用卻不能夠勝任這項工作(尤其指不能對一個引用類的實例使用取地址符&),因此,C++/CLI對應地提供了句柄及用於跟蹤的引用--在此簡稱為跟蹤引用(Tracking References),例如,你可以定義一個跟蹤引用p3,以追蹤對象p2:
Point% p3 = p2;
跟蹤引用的內存存儲方式必須為自動(atuomatic),另外,盡管本地對象不會在內存中"移動",但在上面的n2中,不能使用%來代替&。在C++/CLI中,%之於^,就如同本地C++中的&之於*。
請看下列代碼:
Point^ hp = gcnew Point(2,5);
Point% p4 = *hp;
Point% p5 = *gcnew Point(2,5);
在此,hp是一個Point的句柄,而p4是此句柄的別名。雖然句柄不是一個指針,但也能使用一元 * 操作符來對句柄解引用。(在C++/CLI標准制定期間,是否就引入一元 ^ 操作符來取代 * 還進行過一場討論,反方觀點是,在編寫模板時,* 對句柄或指針進行解引用有非常高的價值。)當然,即使hp有了一個新值,p4在此仍是同一Point的別名。另外要說明一點,當對象有一個句柄或跟蹤引用時,就不能被垃圾回收器回收了。
再來看p5,對gcnew返回的句柄進行了解引用,雖然差不多每個引用類類型的句柄,都能被解引用,但有兩種類型的句柄卻不能被解引用,這兩種類型是:System::String與array<T>。
取句柄操作符
如果想把p1的值寫到標准輸出,代碼似乎應該像下面這樣:
Console::WriteLine("p1 is {0}", p1);
然而,這卻不能通過編譯,因為WriteLine沒有一個可接受Point的重載版本。前面也提過,任何值類型的表達式(如int、long、double)會由一個"裝箱"的過程,自動轉換為Object^。雖然p1看上去比較像一個值類型的實例,但它實際上卻不是,它是一個引用類的實例,所以代碼需要這樣修改: Console::WriteLine("p1 is {0}", %p1);
通過使用一元 % 操作符,我們創建了對象p1的一個句柄,因為每個引用類最終都是從System::Object繼承的,而WriteLine也有一個其第二個參數可接受Object^的重載版本,所以,%p1的Point^就轉換為Object^,並顯示出p1相應的值。要留意的是,此處沒有裝箱,但這個操作符不能應用到本地類的實例上。
GC-Lvalues
在C++標准中定義及使用了lvalue術語,而C++/CLI標准則添加了gc-lvalue術語,其指"一個引用CLI堆中對象、或包含此對象的數值成員的表達式"。如果有一個指向gc-lvalue的句柄,可對其使用一元 * 操作符來產生一個gc-lvalue;而跟蹤引用也是一個gc-lvalue,當%h中h是一個句柄時,它也可以產生一個gc-lvalue。(因為有從lvalue至gc-lvalue的標准轉換,所以一個跟蹤引用可綁定至任意的gc-lvalue或lvalue。)
拷貝構造函數
在下面的例子中,p6由給定的坐標構造而成,而p7則初始化為p6的一個副本,這就需要Point有一個拷貝構造函數;然而,在默認情況下,編譯器不會為這些引用類產生一個拷貝構造函數。那麼,在這種情況下,就必須自己編寫一個。
Point p6(3,4), p7 = p6;
以下,是Point的拷貝構造函數:
Point(Point% p)
{
X = p.X;
Y = p.Y;
}
而對一個本地類N的拷貝構造函數,一般聲明成如下形式:
N(const N& n);
但是,對引用類來說,因為%取代了&,所以在CLI的世界中,const顯得有點格格不入。
賦值操作符
以下表達式:
p7 = p6;
就需要一個賦值操作符,但再次提醒,這不是自動提供的。以下就是一個自定義的操作符例子:
Point% operator=(Point% p)
{
X = p.X;
Y = p.Y;
return *this;
}
之所以沒有提供默認的拷貝構造函數或賦值操作符,是因為所有的引用類(除了System::Object),都有一個基類:System::Object,而這個類並沒有提供一個拷貝構造函數或賦值操作符。基本上,這兩者默認都會調用它們基類中相應的實現版本,但基類中卻一個對應的定義也沒有。
相等性操作符
通過為Point定義一個拷貝構造函數和一個賦值操作符,就可以處理那些數值類型的實例了,你可以初始化它們、把它們傳給函數、或把它們從函數中返回;但實際上,可能還再需要一個操作符--相等性比較操作符,它能像如下定義:
static bool operator==(Point% p1, Point% p2)
{
if (p1.GetType() == p2.GetType())
{
return (p1.X == p2.X) && (p1.Y == p2.Y);
}
return false;
}
由於一個跟蹤引用不可能為數值nullptr,所以就不必對此值進行檢查了,又由於p1與p2是兩個Point的別名,所以可使用點操作符調用GetType和屬性X與Y的get程序。
能同時滿足兩方面需求嗎?
以前說過,對一個引用類而言,相等性的判別是通過一個Equals函數而不是重載 == 操作符來實現的,並且重載了一個接受句柄的 == 操作符,指出了使用上的問題。那讓我們再來回顧一下這個話題。
當在C++/CLI中設計並實現一個引用類時,就要想到"這個類的使用者,會使用C++/CLI語言進行編程,還是會使用如C#、J#、VB.NET之類的其他語言呢,或者兩者都使用呢?"
C++程序員習慣於把類實例當作數值來對待,所以,他們期待類中有一個拷貝構造函數及一個賦值操作符,且對某些類來說,還會期待實現相等或不相等操作符;另一方面,C#、J#、VB.NET程序員只能通過句柄來操縱類實例,所以他們只想要克隆或Equals函數,至於拷貝構造函數與賦值操作符,他們無須知道,也無須關心。
即便C++程序員更傾向於使用 == 操作符,但一個帶有Equals函數的引用類可被任意語言所調用,所以在設計引用類時應盡量實現此函數,不過話說回來,如果對一個不包含Equals函數的類實例調用此函數,將會產生無法預料的後果。
如果在一個引用類中,提供了可接受兩個跟蹤引用的 == 操作符函數,一般上也可滿足C++/CLI程序員的需要。雖然也能提供一個接受兩個句柄的 == 操作符函數,但似乎不可能被這兩組程序員使用。
簡而言之,既可為C++/CLI程序員,也可為其他.NET語言程序員、或同時為兩者實現一個引用類,那麼,是不是可把它們簡單地分為C++/CLI與"其他語言"兩個陣營呢,但事情似乎總不是這麼簡單的,舉例來說,雖然System::String是一個引用類,它提供了可接受兩個句柄的 == 操作符與 != 操作符函數,但是,比較的是字符串的值,而不是它們的句柄。一般來說,在引用類中使用值這個說法,是有點讓人感覺怪怪的,但對一個string類來說,卻又是合情合理的。
在此非常清楚的一點是,萬能的方法是不存在的。為對引用類的使用者,提供最適當的接口,就必須在基於他們所使用語言的基礎上,多考慮一下他們的期望。但無論如何,C++/CLI程序員想要使用其他語言創建的引用類,就不得不要適應沒有拷貝構造函數與賦值操作符這些情況。
其他話題
以下的話題非常簡短、但卻十分有幫助:
1、前面也提到,const不是很適合CLI,且在引用類中,C++/CLI也不允許用const(或volatile)來限定成員函數;但個關鍵字可用在某些類中實例構造函數或成員函數上。那麼在此情況下,它的類型到底是什麼呢?對本地類型N來說,它是N* const;然而,對一個引用類R來說,它只是一個R^。雖然句柄不是const限定的,但它的值卻不能被修改。
2、Point::ToString的實現使用了如下形式:
return String::Concat("(", X, ",", Y, ")");
另外也可以像下面這樣寫:
return String::Format("({0},{1})", X, Y);
正如它的名字,Format函數允許對文本進行格式化(如前導的空格或零、或者一些分隔符等等),而不是簡單地對字符串進行連接。
3、如果想要知道編譯器是否支持C++/CLI擴展,可測試__cplusplus_cli宏是否已經預定義。如果已定義,它的值將為200406L。
4、CLI庫包含了一個稱為System::Decimal的類型,其至少可表示28位數字的值,這個類型是專為那些需要在沒有四捨五入情況下進行大數額金融計算而准備的。與浮點類型不同,Decimal的小數部分能表示得更加精確,通常,當某數以浮點類型表示時,其經常會有一個無窮的小數,但卻更容易導致捨入上的錯誤;而Decimal有一個稱為"scale(數值范圍)"的屬性,其代表了十進制下所需的位數。舉例來說,2.340的scale為3,其結尾的0非常重要,當兩個十進制數相加或相減時,結果的數值范圍是兩者數值范圍中較大的一者,如:1.0 + 2.000為3.000,而5.0-2.00為 3.00;當兩個十進制數相乘時,結果的數值范圍是兩者數值范圍之和,如:1.0*2.000為2.0000;當兩個十進制相除時,結果的數值范圍是除數的數值范圍比被除數多出的值,如:4.00000/2.000為2.00。然而,數值范圍不可能小於為表示正確的值所需的范圍,例如,3.000/2.000、3.00/2.000、3.0/2.000和3/2都是1.5,下面是Decimal的使用范例:
Decimal x = Decimal::Parse("23.00");
Decimal y = Decimal::Parse("2.000");
Decimal result = x * y + Decimal::Parse("2.5");
Console::WriteLine(result);
輸出為48.50000。請注意,C++/CLI沒有字面意義上的Decimal類型,所以需要使用Parse函數。
5、如果有這樣一種情況,一個類使用了C++/CLI之外的其他語言編寫,且有一個名字為C++/CLI關鍵字的public成員,那就可通過__identifier(x)這種形式的內部函數來訪問它,此處的x可以是一個標識符、一個關鍵字、或一個字面上的字符串,例如,為調用一個類X中名為delete、且沒有參數的靜態函數,就可以像X::__identifier(delete)()這樣使用。
6、一個literal域是一個定義在類中的命名編譯期常量,同樣,它也必須有接受一個常量值的初始化程序。盡管一個literal域使用上像一個靜態數據成員,但它並不能聲明為static,且編譯器會把每個literal域都替換為域值。一個literal域能有任意標量類型,但是,能用作初始化句柄的唯一常量數值,只能為字面上的字符串和nullptr。
literal double PI = 3.1415926;
literal int MinValue = -10, MaxValue = 10;
literal int Range = MaxValue - MinValue + 1;
enum Direction {North, South, East, West};
literal Direction Home = North;
literal System::String^ Title = "Annual Report";