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

解析C++/CLI之頭文件、內聯函數與數組

編輯:關於C++

頭文件與函數聲明

在傳統C++的設計與實現中,你可對需建模的每種類型進行定義,並把定義放在各自的頭文件中;而頭文件中,一般會包含類型名、成員名、及相關小型成員函數的內聯定義。

與各個單獨編譯的源文件是通過頭文件來共享信息不同,在C++/CLI中,這些信息是通過程序集來共享的。就拿常舉例的Point類來說,它單獨編譯,並生成了一個名為"Point.dll"的程序集。任何需要某種類型定義的應用程序,都必須編譯和鏈接帶有此類型的程序集,這同時也要求此DLL形式的程序集中有完整的類型定義;同樣,在類型中所有聲明的函數也必須被定義,否則,鏈接器將會報告錯誤。

舉例來說,你可以在Point類中聲明成員函數GetHashCode,並在類外定義它,但必須在同一源文件中(見例1)。但是,若把此成員函數的定義放在一個單獨的源文件中卻不行,即便源文件是作為同一程序集的輸入、與Point.cpp同時編譯也不行,因為編譯這樣一個文件需要訪問程序集Point.dll,而這正好是此編譯過程要生成的程序集。(此處假定在函數定義時未使用inline,這將在後面討論。)

例1:

public ref class Point
{
...
virtual int GetHashCode() override;
};
int Point::GetHashCode() override
{
return X ^ (Y << 1);
}

在編譯及鏈接任何程序集時,都隱含不使用頭文件,且程序集所依賴的所有其他程序集都必須是已編譯及鏈接過的。

內聯函數

在Point中,每個成員函數的定義都有意寫成了inline(內聯),除了增加定義的靈活性外,還可把代碼保持在同一源文件中,使成員函數不能在類型定義本身之外的另一文件中被定義。

編寫內聯函數的傳統方法是把某個函數都聲明為inline,其對編譯器來說是一個提示,讓編譯器在適當的時候對它進行內聯化處理,是典型的以空間換時間做法。然而,在頭文件定義中使用內聯函數,這種形式的優化對編譯來說,卻非常有限。當Point類編譯時,編譯器會把類型內部對成員函數的調用內聯化,例如,Point定義中所有X與Y屬性的get與set方法都會被內聯化。

那麼,如果要在其他程序集的代碼中使用Point,又會怎麼樣呢?所有對Point成員函數的調用都會因此內聯化嗎?理論上來說,是的,畢竟,為編譯應用程序代碼,編譯器需要訪問Point程序集,故此它非常清楚既定的成員函數是怎樣實現的,由此也會允許對這些函數的引用進行優化。

來看一下GetHashCode,從它簡單的內容來看,似乎很適合進行內聯化。現假定從外部另一程序集中對它的所有調用都是內聯化的,那麼,接下來,編譯器很可能會使用不同的算法重新實現此函數。但如果未重新生成此外部程序集,它將會繼續使用內聯的hashcode算法,而不是新的版本。因為這通常都不是所期望發生的行為,所以要盡量避免跨程序集邊界的內聯,那麼,也就不會對X與Y屬性那些不重要的get與set方法進行內聯了。

可幸的是,優化還可在編譯之外進行,比如說,在最簡單的執行模式中,每次一個程序只要一運行,它的CIL指令就會被執行。然而,一個即時編譯器(JIT)會識別出特定的編碼范式,並進行各種優化,其中就包含了代碼內聯。而那些大型、復雜的程序,會在每次安裝時,都編譯為本地代碼,以這種方法,就不必在每次程序執行時,進行優化了。

GetHashCode的定義是沒有聲明為內聯的,如果聲明了,那對此頭文件的多個包含,會導致同一名稱的多次定義,就別指望鏈接器不會提出"抗議"了。但是要知道,這種方法是用於程序集而不是頭文件的,所以一般不會產生此類錯誤,在程序集中只有這個函數的唯一定義。一般說來,在上下文中使用inline,出於自願而不是強迫,事實上,在本例中,是不可使用的,因為聲明為override的任何函數,都不能再標上inline。

遵從CLS

如果屬性中的set與get方法使用了與眾不同的可訪問性,那麼,就會阻礙語言間協同工作的能力。CLI的其中一個目標就是在無須主動請求的情況下,提升語言間的互操作性,為此,它定義了一個通用語言規范(CLS [1])和一套CLS規則,例如,第25條規則寫明:"屬性之訪問性存取程序應為一致。"

當為CLI環境下實現一種類型時,需要考慮,是否導出了類型的多個方面,如成員函數簽名等等,舉例來講,並不是所有基於CLI的語言都支持無符號整數及指針類型,又或只有一小部分語言能理解const與volatile。

CLS要求不具備某種特性的語言也能以函數調用的語法,來訪問它們,正因為這個原因,屬性X的存取程序在元數據中被各自稱為get_X與set_X,類似地,對操作符函數,也有相應的元數據名,所以它們也能被那些沒有操作符重載概念的語言所調用。

Equals函數 PK ==操作符

對一個引用類而言,相等性比較是通過一個名為Equals的函數,而不是重載 == 操作符來實現的。但是,還是可以重載此操作符的,見例2。

例2:

public ref class Point
{
  ...
  static bool operator==(Point^ p1, Point^ p2)
  {
   Object^ o1 = p1;
   Object^ o2 = p2;
   /*1*/ if (o1 == nullptr || o2 == nullptr)
   {
    return false;
   }
   if (o1 == o2) //在測試自身嗎?
   {
    return true;
   }
   if (p1->GetType() == p2->GetType())
   {
    return (p1->X == p2->X) && (p1->Y == p2->Y);
   }
   return false;
  }
};

在標號1中,指明了不接受空值的句柄,但是,此處如果用p1與p2來代替o1與o2,就會造成自身的遞歸調用,因此,必須隱式轉換為Object^。為遵從CLS,在此把函數標為static。(非靜態操作符函數不符合CLS。)

除了句柄符號之外,這個函數與用傳統C++編寫起來非常類似,然而,最大的不同之處恰恰在於此操作符的使用,如果有這樣一種情況,(p == q),p與q都是Point^類型,問題在於,代碼的閱讀者可能會認為此處是在比較兩個句柄,但實際上,是在比較這些句柄引用的Point。為明確地比較句柄,需要這樣編寫代碼:

if (static_cast<Object^>(p1) == static_cast<Object^>(p2))

雖然也可為Point類提供 == 操作符函數,但仍必須提供Equals,否則,其他人對一個Point調用Equals時,就會轉到System::Object中的相應函數,而其比較的是引用相等性,而不是值相等性。也就是說,如果指定Object的實例與當前實例為同一實例,它返回true,否則,返回false。

CLI數組

因為標准C++中也存在數組,它們與C語言中的數組也非常類似,也具有同樣的利弊關系,即,它們都在編譯時分配空間,有固定大小,且沒有強制檢查數組邊界。多維數組並不真正存在,實際上,它們都是數組的數組,或數組的數組的數組等等,在此,我們把這類數組稱為"本地數組"。

而在CLI中,數組則為對象,並分配在垃圾回收堆上,它們的大小在編譯時可以為未知狀態,且在運行時會自動進行數組邊界檢查,還支持真正的多維數組。同樣地,就需要新的語法來表示這類CLI數組。

例3:

int main()
{
  /*1*/ array<int>^ numbers = gcnew array<int>(5) {10, 20, 30, 40};
  Display1DArray("numbers", numbers);
  /*2*/ array<Point^>^ points = gcnew array<Point^> {gcnew Point(3,4), gcnew Point(5,7)};
  Display1DArray("points", points);
  /*3*/ numbers = gcnew array<int>{55, 66, 77};
  Display1DArray("numbers", numbers);
  /*4*/ numbers = gcnew array<int>{};
  Display1DArray("numbers", numbers);
  /*5a*/ points[0]->Move(2,5);
  /*5b*/ points[1] = gcnew Point(8,1);
  /*5c*/ Console::WriteLine("points[0] is {0}", points[0]);
}

請看例3,與引用類的實例相同,CLI數組的對象本質上沒有名稱,且它們是通過指向它們的句柄來訪問的,正如標號1、2、3、4中所見,一個CLI數組類型是用類似於模板的符號進行編寫的,如array<int>和arrar<Point^>。(在C++/CLI早期的版本中,必須使用using namespace cli::language,編譯器才能理解這個符號,現在已不再需要了。)請仔細留意標號1與2,定義了指向數組的句柄,而不是數組,在標號2中,數組類型為"指向Point的句柄數組",而不是"Point數組"。

默認以自動方式存儲的句柄,值為nullptr,然而,在上面的數組定義中,用gcnew分配了一塊內存來初始化句柄,在gcnew後緊跟著數組類型,其後可選的圓括號中指明了元素的個數,再往後可選的花括號中的列明了初始化列表。如果省略初始化列表,元素則取它們的默認值;如果省略元素個數,那麼分配的個數則為初始化列表中表達式的個數;如果指定的個數大於列表中的個數,那余下的元素則取它們的默認值。(例如,numbers[4]的值為零。)元素的個數與初始化列表不能同時省略,且它們也不一定要為常量;元素個數可為零,初始化列表也能為空,兩者都指明了一個零元素的數組,但這與完全沒有數組卻有很大的區別。

在標號2中,省略了元素個數,如果它指定了為3,那第三個元素將會被初始化為nullptr,而不是由默認構造函數生成的指向Point的句柄,這與想像的有點不同。

在標號3中,把int數組句柄numbers設為了一個新值--包含了3個int的數組,這將導致新數組比舊數組中的元素減少一個,如果減少的是已分配空間中唯一的元素,那它最終將會被垃圾回收器回收。因此,盡管數組可以有一個固定大小,但一個一維或多維數組的句柄,能被重新設為類型與維數不變情況下的任意數組,而不用在乎元素的個數(其由系統維護)。

正如標號5a、5b與5c中所示,可用下標來訪問一維CLI數組。

函數Display1DArray將顯示它第一個參數的文本,第二個參數所指定的數組中元素的個數,及這些元素的具體值(如果有的話),以下是輸出:

numbers 5: 10 20 30 40 0
points 2: (3,4) (5,7)
numbers 3: 55 66 77
numbers 0:
points[0] is (2,5)

例4是Display1DArray的源代碼。(眼下,暫時把關鍵字generic當作template。)

例4:

generic<typename T>
void Display1DArray(String^ text, array<T>^ ary)
{
  /*6*/ if (ary == nullptr)
  {
   Console::WriteLine("nullptr passed");
   return;
  }
  /*7*/ Console::Write("{0} {1}:", text, ary->Length);
  /*8*/ for each (T element in ary)
  {
   Console::Write(" {0}", element);
  }
  /*9*/ Console::WriteLine();
}

很明顯,參數ary是一個類型T的CLI數組句柄,但是,ary不但可為一個數組的句柄,也能為一個nullptr值的句柄,所以在標號6中,對它進行了相應的檢查。

在標號7中,顯示了傳入進來的文本及數組中元素的個數,而後者是用只讀屬性Length獲取的,這是所有數組都有的屬性。(所有的CLI數組都隱式從類System::Array繼承,而類System::Array則具有Length屬性。)

接下來,在標號8中循環遍歷了此數組,並在同一行中顯示出每個元素,並以標號9中的新行結束。此處,沒有使用一個從零到ary->Length - 1的整數索引,而是使用了新的循環語句:for each(它們兩者合成為一個關鍵字),用它來枚舉集合中的元素。在此,不必深究for each的工作原理,只需了解,一個CLI數組也是一個CLI的集合,當然也可使用這種語法來進行遍歷了。雖然使用此方法不能訪問到每個元素的下標索引,但要知道,並不是所有集合都是線性的(如二叉樹),在這種情況下,索引毫無意義。

當使用下標來訪問CLI數組時,並不是使用數組的 [] 操作符,而是使用了定義在System::Array中,稱為Item的索引屬性。雖然一個標量屬性只能為一個單一的值,但一個索引屬性卻能有不同的值,且每個值都能用下標來訪問。

為使Display1DArray對數組類型不敏感,一般會想到template,然而,template是一個編譯時的操作,對程序中使用的每種數組類型,都會產生一個相應的副本。在版本V2中,CLI添加了對generic的支持,generic是一個依賴於上下文的標識符,它的功能類似於模板,但它在運行時只會有單一的一份副本。(與大家想的一樣,也有generic類型。)

C++/CLI支持真正的多維數組

CLI數組的第二個參數為可選,其代表了一個數組的序(也即是數組的維數),默認為1。比如在前一個例子中,array<int>也能寫成array<int,1>。(與模板的非類型參數相似,其必須為一個編譯時的常量。)

例5:

int main()
{
  /*1*/ array<String^, 2>^ names = gcnew array<String^, 2> (2,3) {
  {"John", "Robert", "Peter"},
  {"Mary", "Alice"}
};
/*2*/ Console::WriteLine("names has {0} elements", names->Length);
/*3*/ Console::WriteLine("names has {0} dimensions", names->Rank);
/*4*/ Console::WriteLine("names[0,0] is {0}", names[0,0]);
/*5*/ names = gcnew array<String^, 2> (5,7);
/*6*/ Console::WriteLine("names has {0} elements", names->Length);
}

請看例5,在標號1中,定義了一個指向String句柄的兩維數組的句柄,但並未指明行數或列數,接下來,分配了一個2×3數組的內存空間,並以5個字符串及一個nullptr初始化了這6個數組元素,以下是輸出:

names has 6 elements
names has 2 dimensions
names[0,0] is John
names has 35 elements

Length屬性給出了元素總數,而Rank屬性給出了維數。

請注意標號4,可在一對中括號使用逗號分隔的索引數,用以訪問多維數組的一個元素,在此,逗號為一個標點符號,而不是一個操作符。

也可把例中的names引用至任意的String^兩維數組,比如在標號5中,把它重新引用至一個元素值全部為nullptr的5×7數組。

參數數組

請看下列代碼的重載:

static String^ Concat(... array<Object^>^ list);

左邊圓括號後面聲明的參數(必須有一個CLI數組類型),表明了其可接受一個數目可變的既定元素類型的參數。

例6演示了一個可接受多個Point句柄當作參數的函數,其返回最左邊(即最小)的X坐標。(當然,這個函數也能是Point類的一個靜態成員函數。)

例6:

int LeftMostX(... array<Point^>^ points);
int main()
{
  /*1*/ array<Point^>^ p = gcnew array<Point^> {
   gcnew Point(10,3), gcnew Point(5,20), gcnew Point(-3, 4),
   gcnew Point(1,30), gcnew Point(-5,2)
  };
  /*2*/ Console::WriteLine("LeftMostX is {0}", LeftMostX(p[1], p[3], p[0]));
  /*3*/ Console::WriteLine("LeftMostX is {0}", LeftMostX(p));
}
int LeftMostX(... array<Point^>^ points)
{
  /*4*/ int leftMostX = Int32::MaxValue;
  for each (Point^ p in points)
  {
   if (p->X < leftMostX)
   {
    leftMostX = p->X;
   }
  }
  return leftMostX;
}

在標號2中,調用了LeftMostX,並傳遞給它3個Point句柄,然而,在幕後,這個函數實際上只接受一個參數--一個指向Point句柄數組的句柄,同樣地,編譯器安排了這3個Point句柄傳遞進一個數組,並把數組的句柄傳給函數。但也能像標號3那樣,直接傳遞一個Point數組的句柄。

LeftMostX定義中,唯一新的東西是標號4,因為每種C++基本類型都會映射為一個相應定義實現在CLI庫中的類型,例如,在Microsoft的實現中,short映射為System:Int16,int映射為System::Int32,而long long則映射為System:Int64;這些類型均為值類型,它們的實例被分配在堆棧上,而不是垃圾回收堆上。

MaxValue是類型Int32中一個公共靜態字段,其值為2,147,483,647,它是有符號32位補碼整數的最大值。(嚴格來說,MaxValue只是一個字面上的名稱。)

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