程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> 關於C++ >> 實例解析C++/CLI之靜態構造函數

實例解析C++/CLI之靜態構造函數

編輯:關於C++

就某些類而言,當在程序中第一次使用時,最好能有一個初始化過程;當程序不再需要時,也最好能做一些收尾工作,這些都是非常好的類設計習慣。

引出問題

如果有這樣一種情況,某種類型的每個實例都必須有其唯一的ID,比如說某種交易類型,這些ID可用於在處理過程中追蹤每筆交易,或之後用於審計員查看數據文件;為討論方便,此處的ID為從0起始的有符號整型數。

如果把一個nextID值保存在內存中,並在每個新實例構造時,把它遞增1,這無疑是一個不錯的想法,但是,為使在程序連續的執行過程中保持ID值的唯一,就需要在每次程序結束時保存此值,並在下次程序開始運行時恢復這個值,但在標准C++中,是沒辦法來達到這個目的的,實際上,使用標准CLI庫也同樣沒辦法完成。然而,在CLI的.NET實現中有幾個擴展庫,它們卻可以完成這個任務。

問題重現

這回又用到了Point類,因為帶有唯一ID的點很適合此主題。例1中的程序輸出在代碼之後:

例1:

using namespace System;
Point F(Point p) {
  return p;
}
int main()
{
  /*1*/ Point::TraceID = true;
  /*2*/ Point^ hp1 = gcnew Point;
  Console::WriteLine("hp1: {0}", hp1);
  /*3*/ hp1->Move(6,7);
  Console::WriteLine("hp1: {0}", hp1);
  /*4*/ Point^ hp2 = gcnew Point(3,4);
  Console::WriteLine("hp2: {0}", hp2);
  /*5*/ Point p1, p2(-1,-2);
  Console::WriteLine("p1: {0}, p2: {1}", %p1, %p2);
  /*6*/ p1 = F(p2);
  Console::WriteLine("p1: {0}", %p1);
}

輸出:

hp1: [0](0,0)
hp1: [0](6,7)
hp2: [1](3,4)
p1: [2](0,0), p2: [3](-1,-2)
p1: [2](-1,-2)

在程序開始運行時,從一個文本文件中讀取下一個可用的ID值,並用它來初始化一個Point類中的私有靜態(private static)字段。最開始,這個文件包含的值為零。

基於公共靜態布爾屬性TraceID的值,Point中ToString函數生成的字符串可有選擇地包含Point的ID,並以 [id] 的形式作為一個前綴。如果此屬性值為true,就包含ID前綴;否則,就不包含。默認情況下,這個屬性值被設為false,因此,在標號1中我們把它設為true。

在標號2中,使用默認構造函數為Point分配了內存空間,並顯示它的ID為0及值為(0,0)。在標號3中,通過Move函數修改了Point的x與y坐標值,但這不會修改Point的ID,畢竟,它仍是同一個實例--只不過用了不同的值。接著,在標號4中,使用了接受兩個參數的構造函數為另一個Point分配了內存空間,並顯示它的ID為1及值為(3,4)。

在標號5中創建了兩個基於堆棧的實例,並顯示出它們的ID及值。在第三個及第四個Point創建時,它們的ID分別為2和3。

在標號6中,p1被賦於了一個新值,然而,p1仍是它之前的同一個Point,所以它的ID沒有改變。

第二次運行程序時,輸出如下:

hp1: [6](0,0)
hp1: [6](6,7)
hp2: [7](3,4)
p1: [8](0,0), p2: [9](-1,-2)
p1: [8](-1,-2)

如上所示,4個新實例都被賦於了連續的ID值,且與第一次執行時截然不同,但是,還缺少ID 4和5。請留意標號6及函數F的定義,Point參數是傳值到此函數的,而一個Point也是通過值返回的。同樣地,這兩者都會調用到復制構造函數,而其則"忠實"地創建了一個新實例,且每個新實例都有一個唯一的ID。因此,當p2通過值傳遞時,會創建一個ID為4的臨時Point,緊接著,當副本通過值返回時,又會創建一個ID為5的副本,而兩個副本都是可丟棄的。當程序結束時,寫入到文件中下一個可用的ID為6,而在程序下次運行時,這就是第一個Point在分配空間時將用到的ID。

解決方法

例2中為Point類的修訂版本,非常明顯,每個實例現在必須包含一個額外的字段(在此為ID),用以保存ID,在此選擇的類型為int,雖然標准C++允許其最小為16位,但在CLI環境中,其至少為32位。如果以零開始,那麼在ID重復之前,能表示20億個不同的實例;當然,也能以負20億開始,那麼能表示的范圍又將擴展一倍;倘若想要把ID字段再進行擴展,可使用類型long long int,那麼至少能有64位,可以創建數不勝數的實例。那麼ID為unsigned行嗎?如果它的值不會輸出到它的父類之外,是可以的,請記住一點,無符號整型與CLS不兼容。(還可選擇System::Decimal,其可表示128位。)

例2:

using namespace System;
using namespace System::IO;
public ref class Point
{
  int x;
  int y;
  /*1*/ int ID;
  /*2*/ static int nextAvailableID;
  /*3*/ static int GetNextAvailableID() { return nextAvailableID++; }
  /*4*/ static bool traceID = false;
  /*5*/ static String^ masterFileLocation;
  /*6*/ static Point()
  {
   /*6a*/ AppDomain^ appDom = AppDomain::CurrentDomain;
   /*6b*/ masterFileLocation = String::Concat(appDom->BaseDirectory, "\\PointID.txt");
   /*6c*/ try {
    /*6d*/ StreamReader^ inStream = File::OpenText(masterFileLocation);
    /*6e*/ String^ s = inStream->ReadLine();
    /*6f*/ nextAvailableID = Int32::Parse(s);
    /*6g*/ inStream->Close();
    /*6h*/ appDom->ProcessExit += gcnew
    EventHandler(&Point::ProcessExitHandler);
   }
   /*6i*/ catch (FileNotFoundException^ ioFNFEx)
   {
    //采取某些必要的措施
   }
   /*6j*/ finally
   {
    appDom = nullptr;
   }
  }
  /*7*/ static void ProcessExitHandler(Object^ sender, EventArgs^ e)
  {
   /*7a*/ StreamWriter^ outStream = File::CreateText(masterFileLocation);
   /*7b*/ outStream->WriteLine("{0}", nextAvailableID);
   /*7c*/ outStream->Close();
  }
  public:
  // ...
  /*8*/ static property bool TraceID
  {
   bool get() { return traceID; }
   void set(bool val) { traceID = val; }
  }
  // define instance constructors
  Point()
  {
   /*9*/ ID = GetNextAvailableID();
   X = 0;
   Y = 0;
  }
  Point(int xor, int yor)
  {
   /*10*/ ID = GetNextAvailableID();
   X = xor;
   Y = yor;
  }
  Point(Point% p) // copy constructor
  {
   /*11*/ ID = GetNextAvailableID();
   X = p.X;
   Y = p.Y;
  }
  // ...
  /*12*/ virtual int GetHashCode() override
  {
   // ...
  }
  virtual String^ ToString() override
  {
   /*13*/ if (traceID)
   {
    return String::Format("[{0}]({1},{2})", ID, X, Y);
   }
   else
   {
    return String::Format("({0},{1})", X, Y);
   }
  }
};

一旦作為static,標號2至5中定義的成員屬於類,而不屬於任何實例;而作為private,它們只是一個實現的細節。

使用這個類

C++/CLI在非本地類中,引入了靜態構造函數的概念,它的類名聲明為static,如上例標號6所示。盡管一個靜態構造函數是在類第一次使用之前被調用,但"使用"意味著什麼呢?一個引用類靜態構造函數的執行,是由類中對某個靜態數據成員的第一次引用觸發的。

根據C++/CLI標准:一個靜態構造函數不應有一個ctor初始化過程(ctor-initializer),靜態構造函數也不可以被繼承,且不能被直接調用。如果一個類的初始化過程帶有靜態字段,那麼這些字段會在靜態構造函數執行之前,以聲明的順序被初始化。

為靜態構造函數生成的元數據總會標記為private,而不管它們是否帶有聲明或暗指的訪問指定符。(但編譯器會發出警告:"Accessibility on class constructor was ignored")。在本文寫作時,至於一個帶有給定訪問指定符的靜態構造函數,是否應為private之外的問題,仍在討論之中,因此,訪問指定符總是會被忽略。

而一個沒有顯式指明靜態構造函數的引用類,它的行為,就會像是有一個空的靜態構造函數體一樣。

在上例標號6a中,利用AppDomain類,為當前線程獲取了應用程序域(Application domains)。而根據CLI標准庫:應用程序域表現為System::AppDomain對象,提供了隔離性、卸載及托管代碼執行時的安全邊界檢查。多個應用程序域可運行於單個進程中,但是,也不存在應用程序域與線程的一對一關系,可以同時有幾個線程屬於某一個應用程序域,且同時某一個既定的線程也不會限制在某個單獨的應用程序域中,但無論何時,一個線程只能在一個應用程序域中執行。

用於追蹤在程序執行時下一個可用的ID的文本文件名為"PointID.txt",與可執行程序位於同一目錄中,如標號6b所示。(Concat可同時用於一個Unicode寬字符串及普通窄字符串,其會在編譯時自動轉換為寬字符串。)在標號6d中打開此文件,並在標號6e中讀取,輸入的字符串在標號6f中轉換為一個整數,接著,在標號6g中關閉此文件。而try/catch塊用於可能拋出的I/O異常。

只讀屬性BaseDirectory與CurrentDomain是Microsoft對標准CLI庫的擴展。

在I/O中使用的類型,如StreamReader與File,存在於System::IO命名空間中。

標號6h注冊了一個處理函數,用於在程序快要結束時調用。注意,對一個類來說,沒有靜態析構函數。

Finally子句

C++/CLI支持對try/catch的一個擴展,也就是finally子句,位於它塊內的代碼總會被執行,而不管對應的try塊中是否產生了一個異常。這就是說,finally子句會在try塊正常結束後執行,或者說,會在與try相聯的catch塊之後執行。

在上例的標號6j中,finally子句只是簡單地把appDom句柄設置為null值,因此,就不會再對AppDomain對象進行訪問了。但這種做法有點多余,因為父類塊退出時,總會執行到這一行,所以,在此只是作為一個對此功能的簡要介紹。

事件處理

CLI支持事件的概念,簡單來說,一個事件就是一個非本地類成員,它可使一個對象或類提供通知機制。標准CLI類System::AppDomain包含了幾個這樣的事件,但Microsoft的擴展版本甚至包含了更多的事件,比如說ProcessExit,其在例2的標號6h中被引用。

當一個特定的事件發生時,與事件相聯的函數會以它們之前相聯的順序被調用,從最簡單的形式來說,一個事件只與一個函數發生聯系,而這也只是通過簡單的賦值完成的,也就是說,包裝了函數的代理被賦值給事件成員。而從更一般的形式來說,一個事件在不同時間,通過 += 復合賦值操作符,可與任意多個函數相聯。之所以在標號6h中使用這個操作符,是因為不知道事件是否已與事件處理程序相聯,如果已經相聯,又使用了簡單的 = 賦值符,那麼這些函數將不再與此事件相聯系。

每個事件都有一個類型,以ProcessExit來說,類型為System::EventHandler,其是一個用於包裝分別接受兩個參數System::Object^ 與System::EventArgs^ 函數的代理類型,且有一個void返回類型。而定義在標號7中的ProcessExitHandler函數,也正好具有同樣的特征(參數類型)。同時,在標號6h中,把此函數注冊為一個事件處理程序,以便在進程退出的事件發生時調用。當這個函數被調用時,它會覆寫此前的文本文件,寫入一個下次執行時可用的ID值,而傳遞進來的參數會被忽略。

代理

根據C++/CLI標准:代理定義為一個從System::Delegate繼承而來的類,它可用於調用所有帶有一組參數的代理實例的函數。(注意,與指向成員函數的指針不同,一個代理實例能被綁定至任意類的成員,只要函數類型與代理類型匹配就可,所以,代理非常適合於"匿名"調用。)

而在本例中,用到了一個定義在CLI庫中的代理類型,名為System::EventHandler。然而,使用關鍵字delegate,也能定義自己的代理類型。在標號6h中,就使用了gcnew創建了一個代理的實例。由於被包裝的函數為static,而構造函數的調用也只給了一個參數,所以,指向成員函數ProcessExitHandler的指針,其類型也必須與代理相匹配。(要包裝一個實例函數,必須提供實例自身的句柄作為第一個參數。)

對Point的其他修改

對TraceID屬性的讀取與寫入定義在標號8中,而使用在標號12中。

三個構造函數(標號9、10、11)全部會創建新的Point實例,所以它們需要為ID分配一個唯一的值,且其他的成員函數只會對現有的實例進行操作,而不會修改任何ID值。初始化只會在當一個對象創建時才會發生,因此也需要一個新的ID,而賦值操作發生在對象創建之後,所以在此不需要新的ID。

在標號12中,GetHashCode返回一個int,其正是ID所需的類型。同樣,這個函數也能返回一個值,從而保證有一個唯一的哈希值。(當然了,如果ID的類型為unsigned或long long,就需要把它縮減為一個int類型。)

至於是否包含ID前綴,全在ToString中完成,見標號13。

Initonly字段

在非本地類中,如果一個字段聲明中帶有initonly標識符,其通常為一個在ctor初始化過程、構造函數體、或一個靜態構造函數中的左值,而在其他情況中,其為一個右值。(特別要說明,一個靜態的initonly字段只能被靜態的構造函數所修改,而一個實例initonly字段只能被實例構造函數所修改。)除了當類第一次使用,或一個實例被創建時之外,都可以把這個字段當作只讀類型,例如,某些工程數據類型有一張靜態系統表,在每次程序運行時,其值都必須從一個文件中讀出,但之後,就當作只讀類型,例3就是這樣一種情況。

例3:

using namespace System;
public ref class EngineeringData
{
  /*1*/ static initonly array<double>^ coefficients;
  /*2*/ static EngineeringData()
  {
   int elementCount;
   //找出需要多大的數組
   // elementCount = ...
   coefficients = gcnew array<double>(elementCount);
   for (int i = 0; i < elementCount; ++i)
   {
    // coefficients[i] = ...
   }
  }
  public:
   /*3*/ static property double Coeff[int] {
    double get(int index) { return coefficients[index]; }
   }
};
int main()
{
  double d;
  try {
   /*4*/ d = EngineeringData::Coeff[2];
  }
  catch (IndexOutOfRangeException^ ex)
  {
   //處理異常
  }
}

保存了系數的靜態數組在標號1中聲明為initonly,在靜態構造函數中,打開了一個包含系數的文件,在確定數目後分配了相應大小的數組,並從文件中讀取數值,保存到數組中。

與其讓數組成為public或讓程序員用下標來直接訪問數組,倒不如讓數組隱藏在一個只讀的命名索引屬性之後。(方括號表示了索引屬性。)在本例中,是以逗號隔開的索引列表,這意味著,可以使用一個下標來索引到這個類,如標號4所示。(與多維數組下標相似,索引訪問一個索引屬性是使用了[]中的逗號分隔索引列表。)

C++/CLI默認情況下還允許一個索引屬性名作為一個關鍵字,也就是說,一個實例名可被直接索引,而無須使用任何成員名。然而,這只對實例索引的屬性可行,所以在此不能使用。同樣地,屬性名為Coeff。

一個initonly字段不是一個編譯時命名常量,因此,它無須包含一個帶有常量的初始化過程,且initonly也不會限制是否帶有一個標量。

如果一個類包含了帶有初始化過程的任意initonly字段,它們會以聲明的順序,在靜態構造函數執行之前被初始化。

那能把Point類中的nextAvailableID標為initonly嗎?畢竟,它只會在構造函數中被修改,答案是不可以,因為它是一個靜態成員,且它只能被靜態構造函數所更新。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved