C++/CLI解析之基于堆棧的對象與跟蹤引用

2010-08-28 10:50:01來源:西部e網(wǎng)作者:

    在托管堆上分配對象實例,似乎是使用托管擴展C++、C#、J#、VB.NET程序員的唯一方法,而使用本地C++的程序員,不但可以在堆上分配內存,甚至更慣于使用基于堆棧的對象實例。

  現(xiàn)在回顧一下以前定義的Point引用類,再來看一下以下變量定義:

Point p1, p2(3,4);

  從本地C++的角度來說,p1與p2應為基于堆棧的引用類Point實例,哪怕是從一般性的角度來看,它們也是。P1由默認的構造函數(shù)初始化,而p2由接受x與y坐標的構造函數(shù)初始化。從實現(xiàn)上來看,Point是自包含類型的(也就是說,它不包含任何指針或句柄),然而,作為一個引用類的實例,它仍處于CLI運行時的掌控之下,且在必要時,會被垃圾回收--正因為此,所以不能定義一個引用類的靜態(tài)或全局實例。

  同時,也不能將sizeof應用于指明是引用類實例的表達式,因為sizeof是在編譯時進行計算的,而Point對象的大小要直到運行時才能確定;但是,可將sizeof應用于句柄,因為它的大小在編譯時就已經確定了。

  另外,還不能定義一個基于堆棧的CLI數(shù)組實例。

  跟蹤引用

  本地C++可通過&來定義一個對象的別名,例如,對任意本地類N,可編寫如下代碼:

N n1;
N& n2 = n1;

  引用必須在定義時進行初始化,且在整個生命期中,它們都鎖定于引用同一對象,也就是說,它的值不會改變。引用一個引用類的實例與引用一個本地類基本一致,只不過語法不同而已。

  在程序執(zhí)行期間,引用類的實例會在內存中"移動",所以,需要對它們進行跟蹤,而本地指針與引用卻不能夠勝任這項工作(尤其指不能對一個引用類的實例使用取地址符&),因此,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);

  通過使用一元 % 操作符,我們創(chuàng)建了對象p1的一個句柄,因為每個引用類最終都是從System::Object繼承的,而WriteLine也有一個其第二個參數(shù)可接受Object^的重載版本,所以,%p1的Point^就轉換為Object^,并顯示出p1相應的值。要留意的是,此處沒有裝箱,但這個操作符不能應用到本地類的實例上。



  GC-Lvalues

  在C++標準中定義及使用了lvalue術語,而C++/CLI標準則添加了gc-lvalue術語,其指"一個引用CLI堆中對象、或包含此對象的數(shù)值成員的表達式"。如果有一個指向gc-lvalue的句柄,可對其使用一元 * 操作符來產生一個gc-lvalue;而跟蹤引用也是一個gc-lvalue,當%h中h是一個句柄時,它也可以產生一個gc-lvalue。(因為有從lvalue至gc-lvalue的標準轉換,所以一個跟蹤引用可綁定至任意的gc-lvalue或lvalue。)

  拷貝構造函數(shù)

  在下面的例子中,p6由給定的坐標構造而成,而p7則初始化為p6的一個副本,這就需要Point有一個拷貝構造函數(shù);然而,在默認情況下,編譯器不會為這些引用類產生一個拷貝構造函數(shù)。那么,在這種情況下,就必須自己編寫一個。

Point p6(3,4), p7 = p6;


  以下,是Point的拷貝構造函數(shù):

Point(Point% p)
{
 X = p.X;
 Y = p.Y;
}


  而對一個本地類N的拷貝構造函數(shù),一般聲明成如下形式:

N(const N& n);


  但是,對引用類來說,因為%取代了&,所以在CLI的世界中,const顯得有點格格不入。

  賦值操作符

  以下表達式:

p7 = p6;


  就需要一個賦值操作符,但再次提醒,這不是自動提供的。以下就是一個自定義的操作符例子:

Point% operator=(Point% p)
{
 X = p.X;
 Y = p.Y;
 return *this;
}


  之所以沒有提供默認的拷貝構造函數(shù)或賦值操作符,是因為所有的引用類(除了System::Object),都有一個基類:System::Object,而這個類并沒有提供一個拷貝構造函數(shù)或賦值操作符;旧希@兩者默認都會調用它們基類中相應的實現(xiàn)版本,但基類中卻一個對應的定義也沒有。


    相等性操作符

  通過為Point定義一個拷貝構造函數(shù)和一個賦值操作符,就可以處理那些數(shù)值類型的實例了,你可以初始化它們、把它們傳給函數(shù)、或把它們從函數(shù)中返回;但實際上,可能還再需要一個操作符--相等性比較操作符,它能像如下定義:

static bool operator==(Point% p1, Point% p2)
{
 if (p1.GetType() == p2.GetType())
 {
  return (p1.X == p2.X) && (p1.Y == p2.Y);
 }
 return false;
}

  由于一個跟蹤引用不可能為數(shù)值nullptr,所以就不必對此值進行檢查了,又由于p1與p2是兩個Point的別名,所以可使用點操作符調用GetType和屬性X與Y的get程序。

  能同時滿足兩方面需求嗎?

  以前說過,對一個引用類而言,相等性的判別是通過一個Equals函數(shù)而不是重載 == 操作符來實現(xiàn)的,并且重載了一個接受句柄的 == 操作符,指出了使用上的問題。那讓我們再來回顧一下這個話題。

  當在C++/CLI中設計并實現(xiàn)一個引用類時,就要想到"這個類的使用者,會使用C++/CLI語言進行編程,還是會使用如C#、J#、VB.NET之類的其他語言呢,或者兩者都使用呢?"

  C++程序員習慣于把類實例當作數(shù)值來對待,所以,他們期待類中有一個拷貝構造函數(shù)及一個賦值操作符,且對某些類來說,還會期待實現(xiàn)相等或不相等操作符;另一方面,C#、J#、VB.NET程序員只能通過句柄來操縱類實例,所以他們只想要克隆或Equals函數(shù),至于拷貝構造函數(shù)與賦值操作符,他們無須知道,也無須關心。

  即便C++程序員更傾向于使用 == 操作符,但一個帶有Equals函數(shù)的引用類可被任意語言所調用,所以在設計引用類時應盡量實現(xiàn)此函數(shù),不過話說回來,如果對一個不包含Equals函數(shù)的類實例調用此函數(shù),將會產生無法預料的后果。

  如果在一個引用類中,提供了可接受兩個跟蹤引用的 == 操作符函數(shù),一般上也可滿足C++/CLI程序員的需要。雖然也能提供一個接受兩個句柄的 == 操作符函數(shù),但似乎不可能被這兩組程序員使用。

  簡而言之,既可為C++/CLI程序員,也可為其他.NET語言程序員、或同時為兩者實現(xiàn)一個引用類,那么,是不是可把它們簡單地分為C++/CLI與"其他語言"兩個陣營呢,但事情似乎總不是這么簡單的,舉例來說,雖然System::String是一個引用類,它提供了可接受兩個句柄的 == 操作符與 != 操作符函數(shù),但是,比較的是字符串的值,而不是它們的句柄。一般來說,在引用類中使用值這個說法,是有點讓人感覺怪怪的,但對一個string類來說,卻又是合情合理的。

  在此非常清楚的一點是,萬能的方法是不存在的。為對引用類的使用者,提供最適當?shù)慕涌冢捅仨氃诨谒麄兯褂谜Z言的基礎上,多考慮一下他們的期望。但無論如何,C++/CLI程序員想要使用其他語言創(chuàng)建的引用類,就不得不要適應沒有拷貝構造函數(shù)與賦值操作符這些情況。

其他話題

  以下的話題非常簡短、但卻十分有幫助:

  1、前面也提到,const不是很適合CLI,且在引用類中,C++/CLI也不允許用const(或volatile)來限定成員函數(shù);但個關鍵字可用在某些類中實例構造函數(shù)或成員函數(shù)上。那么在此情況下,它的類型到底是什么呢?對本地類型N來說,它是N* const;然而,對一個引用類R來說,它只是一個R^。雖然句柄不是const限定的,但它的值卻不能被修改。

  2、Point::ToString的實現(xiàn)使用了如下形式:

return String::Concat("(", X, ",", Y, ")");

  另外也可以像下面這樣寫:

return String::Format("({0},{1})", X, Y);

  正如它的名字,F(xiàn)ormat函數(shù)允許對文本進行格式化(如前導的空格或零、或者一些分隔符等等),而不是簡單地對字符串進行連接。

  3、如果想要知道編譯器是否支持C++/CLI擴展,可測試__cplusplus_cli宏是否已經預定義。如果已定義,它的值將為200406L。

  4、CLI庫包含了一個稱為System::Decimal的類型,其至少可表示28位數(shù)字的值,這個類型是專為那些需要在沒有四舍五入情況下進行大數(shù)額金融計算而準備的。與浮點類型不同,Decimal的小數(shù)部分能表示得更加精確,通常,當某數(shù)以浮點類型表示時,其經常會有一個無窮的小數(shù),但卻更容易導致舍入上的錯誤;而Decimal有一個稱為"scale(數(shù)值范圍)"的屬性,其代表了十進制下所需的位數(shù)。舉例來說,2.340的scale為3,其結尾的0非常重要,當兩個十進制數(shù)相加或相減時,結果的數(shù)值范圍是兩者數(shù)值范圍中較大的一者,如:1.0 + 2.000為3.000,而5.0-2.00為 3.00;當兩個十進制數(shù)相乘時,結果的數(shù)值范圍是兩者數(shù)值范圍之和,如:1.0*2.000為2.0000;當兩個十進制相除時,結果的數(shù)值范圍是除數(shù)的數(shù)值范圍比被除數(shù)多出的值,如:4.00000/2.000為2.00。然而,數(shù)值范圍不可能小于為表示正確的值所需的范圍,例如,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函數(shù)。

  5、如果有這樣一種情況,一個類使用了C++/CLI之外的其他語言編寫,且有一個名字為C++/CLI關鍵字的public成員,那就可通過__identifier(x)這種形式的內部函數(shù)來訪問它,此處的x可以是一個標識符、一個關鍵字、或一個字面上的字符串,例如,為調用一個類X中名為delete、且沒有參數(shù)的靜態(tài)函數(shù),就可以像X::__identifier(delete)()這樣使用。

  6、一個literal域是一個定義在類中的命名編譯期常量,同樣,它也必須有接受一個常量值的初始化程序。盡管一個literal域使用上像一個靜態(tài)數(shù)據(jù)成員,但它并不能聲明為static,且編譯器會把每個literal域都替換為域值。一個literal域能有任意標量類型,但是,能用作初始化句柄的唯一常量數(shù)值,只能為字面上的字符串和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";

關鍵詞:C++/CLI