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

實例解析C++/CLI之開卷有益

編輯:關於C++

C++/CLI可以說是標准C++語言一種新的"方言",它是Microsoft為充分利用CLI(Common Language Infrastructure)平台而開發出來的。那麼,它在語言方面有何新穎獨到之處呢,下面,就讓我們一起開始奇妙的C++/CLI語言之旅(文中所有示例代碼,均以Visual Studio.NET 2005 Professional編譯通過,所有的講解內容,也均以Visual Studio.NET 2005環境為基礎)。

程序集與元數據

傳統的C++編譯模式包括把單獨的源文件編譯為目標文件(obj),再把目標文件與庫函數鏈接在一起,以生成可執行程序。而CLI模式卻大不相同,它涉及到程序集的創建與使用。

簡單來說,在不計輸入源文件數目的基礎上,程序集即為單次編譯的輸出。如果輸出帶有一個進入點函數(例如main函數),它即為一個.exe文件;如果沒有,它則為一個.dll文件。任何引用外部程序集而生成的編譯,必須要訪問所依賴的程序集,此時也沒有類似傳統鏈接時用到的頭文件機制,而是通過編譯器在所依賴的程序集內部查找,來訪問所需的外部信息。

程序集包含了元數據,其描述了包含在那裡的類型與函數,還有CIL(Common Intermediate Language)指令--Microsoft稱其為"MSIL"。元數據與指令能通過獨立的VES(Virtual Execution System)來執行。

CLI類型

例1是一個模擬二維點的類。此處不得不提到命名空間,所有的CLI標准庫類型都屬於System命名空間,或嵌套在其內部的某個命名空間之下,例如System::Object和System::String,還有System::IO、 System::Text、System::Runtime::CompilerOptions等等。標記1可避免在程序中一直使用namespace限定詞。

例1:

/*1*/
using namespace System;
/*2*/
public ref class Point
{
  int x;
  int y;
  public:
   //定義用於讀寫X與Y實例屬性
   /*3a*/ property int X
   {
    /*3b*/ int get() { return x; }
    /*3c*/ void set(int val) { x = val; }
   }
   /*4a*/ property int Y
   {
    /*4b*/ int get() { return y; }
    /*4c*/ void set(int val) { y = val; }
   }
   //定義實例構造函數
   /*5a*/ Point()
   {
    /*5b*/ X = 0;
    /*5c*/ Y = 0;
   }
   /*6a*/ Point(int xor, int yor)
   {
    /*6b*/ X = xor;
    /*6c*/ Y = yor;
   }
   //定義實例方法
   /*7a*/ void Move(int xor, int yor)
   {
    /*7b*/ X = xor;
    /*7c*/ Y = yor;
   }
   /*8a*/ virtual bool Equals(Object^ obj) override
   {
    /*8b*/ if (obj == nullptr)
    {
     return false;
    }
    /*8c*/ if (this == obj) //我們在測試自己嗎?
    {
     return true;
    }
    /*8d*/ if (GetType() == obj->GetType())
    {
     /*8e*/ Point^ p = static_cast<Point^>(obj);
     /*8f*/ return (X == p->X) && (Y == p->Y);
    }
    return false;
   }
   /*9*/ virtual int GetHashCode() override
   {
    return X ^ (Y << 1);
   }
   /*10a*/ virtual String^ ToString() override
   {
    /*10b*/ return String::Concat("(", X, ",", Y, ")");
   }
};

在標記2中,我們定義了一個稱為Point的引用類(ref class),一個引用類是一個CLI引用類型,當兩者一起使用時,ref與class(中間有空格)表示了一個新的關鍵詞。

public前綴表明了類型在它的父類程序集之外可見--即可訪問(只有兩種類型的可見性,public和private,類型默認為private),另外,只有類型才能有可見性屬性,非成員函數、全局變量及文件范圍內的typedef都不能在它們的父類程序集之外訪問。

與C++程序員預想的一樣,除了默認的成員可訪問性,一個引用結構(ref struct)與引用類基本上一模一樣,在這,我們把兩者都稱為引用類。

每個引用類都有一個基類,如果沒有顯式指定,那麼默認的基類即為System::Object,一個引用類有且只能有一個基類。

我們先不管Point在內部是怎麼表示的,考慮到它有X與Y屬性,我們在此使用了笛卡爾坐標,實現起來非常簡單;如果它使用極坐標,那麼就復雜多了。

作為成員的標量屬性,也對實例提供了類似字段的訪問性,在標記3(a)中,用int類型定義了一個X屬性,property符號是一個上下文關鍵字,而不是一個全局保留的關鍵字,它的用法只限於在這個上下文中。

對於get與set存取程序,在一個屬性中即可有任意一個,也可兩者兼有。在標記3(b)中,get返回既定屬性的值;而在標記3(c)中,set使用編程者提供的值來設置即定的屬性值。這兩個存取程序分別以名字get與set定義為單獨的函數,必須接受或返回相應的聲明類型值,在本例中,為int(注意,這兩個名字不是關鍵字)。存取程序也能具有不同的可訪問性,但可能會妨礙到語言間的互操作性(interop),因為其他CLI語言可能不支持。

在標記5(b)與5(c)代表的默認構造函數中,是使用set的簡單例子--X與Y均被設置為零,注意,不能使用X=Y=0來代替,因為set為一個void返回類型,所以子表達式Y=0不能出現在另一個表達式中。

對一個引用類來說,相等性是通過函數Equals來實現的,而不是重載==操作符,如標記8(a)所示。因為Point重載了System::Object::Equals,所以Point::Equals必須被聲明為virtual,再次提醒的是,override符號也是一個上下文關鍵字,而不是一個保留關鍵字。而這個函數重載了Object中的一個函數,所以需要接受一個Object作為參數,而不是一個Point。

實際上,參數帶有類型Object^,其表示"Object的句柄",並指向托管堆(垃圾回收)中的一個對象。句柄在此是一個C++/CLI術語,CLI實際上把它稱為"引用",但C++已經有引用了,這是兩回事。

有經驗的C++類設計人員可能會留意到,在這個類的定義中,缺乏了兩個重要的東西:函數未const限定;且參數不是作為一個const句柄傳遞的。為什麼會這樣呢?因為引用類的成員函數不會用const來限定,CLI也沒有概念上的const函數;把參數聲明為一個const句柄將會使它成為另一種類型,這樣它就不再能被System::Object::Equals重載了(const類型的句柄是允許的,但它們只能被用在一個C++/CLI上下文之內,而不能與任何CLI標准庫函數一起使用的,因為目前CLI中還未有const這個概念,未來版本的C++/CLI有可能會全面支持const,但其他語言仍不會支持const)。

在標記8(b)中,我們把obj與nullptr作一比較。nullptr關鍵字表示常量空值,當使用在一個句柄上下文中時,它表示空句柄--沒有指向任何對象的句柄;當使用在一個指針上下文中時,它表示空指針--沒有包含任何地址的指針。

為防止自身比較,在標記8(c)中,把obj與this作一對比。在一個非引用類(指本地類)中,this是一個實例函數調用時指向對象的指針,可帶有const限定符;在一個引用類中,則是實例函數調用時指向對象的句柄--此處要再次提醒大家,不允許帶有const限定符。也可以通過類似以指針訪問成員時的指向操作符 ->,來訪問類中成員,只不過此處使用的是句柄。

Equals是為了確保其比較的兩個對象有著相同的類型,所以在標記8(d)中調用了System::Object::GetType,其返回一個代表當前實例運行時類型的System::Type句柄,如果兩個System::Type對象引用指向同一對象,則它們代表了同一類型。此處,我們比較的是兩個句柄,而不是兩個類型對象。

一旦你獲知兩個對象為同一類型,就可以安全地把Object句柄向上轉換為一個Point句柄,進而執行數據比較,而不用擔心發生錯誤的類型匹配這樣的異常,在此,使用了static_cast。

為使哈希表(散列表)數據結構工作正常,在對象中必須有一個名為GetHashCode的函數。基本上,如果一個類型定義了Equals,它也應該同時定義GetHashCode,其是重載System::Object的版本,如標記9。

與相等性比較類似,值的格式化是通過一個重載System::Object的函數實現的,如標記10(a),而不是重載<<操作符。這個函數稱為ToString,它的功能是創建並返回一個當前實例的字符串,它調用了System::String::Concat連接三個字符串及兩個int,實現了所需功能。

毫無疑問,不可能對任一參數及類型的搭配,Concat都能有一個適當的重載版本,那麼,Concat是怎樣處理這些參數的呢?本例中使用的重載版本如下: static String^ Concat(... array<Object^>^ list);

圓括號中的參數聲明(其必須有一托管的數組類型),表明可接受任意數量給定元素類型的參數,即,它是一個類型安全的varargs--參數數組,參數列表為一指向對象句柄托管數組的句柄。

那麼這兩個int--X與Y,是怎樣轉換為Object^的呢?其實,在基本數據類型對Object^的表達式中,都存在著一個隱式轉換,這個過程稱為"裝箱",也就是包含基本數據類型值的對象,在托管堆上的分配。逆過程稱為"解箱",這需要顯式轉換。

最後提一下命名約定。CLI指定了類、函數、屬性必須以PascalCase模式來編寫,也就是說,每個單詞的首字母必須大寫,而CLI標准庫也遵循這條原則。

一個簡單的示例程序

例2是一個使用了Point類的簡單程序,下面以此為例簡單講解各方面的含義:

例2:

using namespace System;
int main()
{
  /*1*/ Point^ p1 = gcnew Point;
  /*2*/ Console::WriteLine("p1 = {0}, p1's HashCode = {1}", p1, p1->GetHashCode());
  /*3*/ p1->Move(5, 7);
  /*4*/ Console::WriteLine("p1 = {0}, p1's HashCode = {1}", p1, p1->GetHashCode());
  /*5*/ Console::WriteLine("p1 Equals Point(9, 1) = {0}",
  p1->Equals(gcnew Point(9, 1)));
}

分配托管內存:在標記1中,定義了一個指向Point類型的句柄,並用gcnew操作符返回的位置初始化它,gcnew操作符是一個關鍵字,它為一個新的Point對象在托管堆中,分配了相應的空間,與大家想的一樣,此處還會調用默認的構造函數。在目前的C++/CLI版本中,引用類的對象只能駐留於堆棧或托管堆中,與其他CLI語言不同,C++/CLI可以讓你編寫能被傳遞,並通過復制構造函數或 = 操作符賦值的引用類,還可以重載Clone函數,實現虛擬(深度)賦值。 格式化輸出:CLI提供了一系列的I/O類型--使用功能性注解的函數。最簡單的例子就是System::Console Write和WriteLine(見標記2)的重載版本,其向標准輸出設備輸出文本,WriteLine會跟上一個新行,而Write則不會。

這類函數有許多重載的版本,然而,最常見的形式是接受一個包含文本的格式化字符串,並帶有可選的格式指定符--由花括號進行分隔,其後緊接需要格式化其值的參數。格式指定符 {0} 對應於緊接著格式化字符串傳遞進來的第一個參數;而 {1} 則對應於第二個參數,以此類推。與Concat類似,也有一些接受幾個固定參數的重載版本,或可接受幾個固定參數並同時接受一個可變數目的參數,在本例中,使用了如下的版本:

static void WriteLine(String^ format, Object^ arg0, Object^ arg1);

字符串在此被隱式轉換為String^。因為p1是一個Point^,且Point是從Object繼承而來,所以p1是is關系。GetHashCode返回一個int,因此在被傳遞之前,會被裝箱為Object^。一旦執行到WriteLine,它會調用第二個和第三個參數的ToString函數,並輸出結果字符串。以下是程序的輸出:

p1 = (0,0), p1's HashCode = 0
p1 = (5,7), p1's HashCode = 11
p1 Equals Point(9, 1) = False

垃圾回收:由句柄p1引用的內存駐留於托管堆中,而托管堆則處於垃圾回收器"監視"之下,當一個句柄超出作用域時,其引用的內存就少了一個與此相聯的句柄,繼而當句柄計數為零時,內存就被自動回收了。如果一個句柄在某段時間內並沒有超出作用域,但你已不需要其引用的內存了,就可以設置句柄為nullptr來減少其的引用計數,在此,沒有辦法來顯式釋放一塊托管內存。另外,也可以對句柄調用delete,它會馬上運行析構函數(Dispose函數),但這塊內存仍不會被回收,直到垃圾回收器決定回收它。

編譯程序

如果要把Point與main程序放在兩個不同的程序集中,必須創建兩個項目--為Point類創建Point項目,為應用程序創建Main項目。

要創建Point項目,可在Visual Studio.NET 2005中選擇"文件|新建|項目|空項目"(不要選擇"類庫")。在"解決方案資源管理器"中找到"源文件",鼠標右鍵單擊選擇"添加|新建項",在對話框左邊的類別欄中選擇"代碼",接著在右邊選擇"C++文件",輸入Point名稱,並在打開的文件中粘貼例1中代碼,保存文件。

在"解決方案資源管理器"中,右鍵單擊項目名Point,首先,選擇"屬性|配置屬性|常規",把"配置類型"改為"動態庫(.dll)",選擇"公共語言運行庫支持"為"公共語言運行庫支持(/clr)";其次,在"C/C++|代碼生成"中,把"運行時庫"改為多線程 DLL (/MD);最後,在"鏈接器|常規"欄中,把"輸出文件"後綴名從.exe改為.dll。

雖然在選擇"類庫"時,這些都是由Visual Studio.NET 2005自動完成的,但它會生成一大堆你不需要的支持文件。此時,選擇"生成",就會在Point\debug目錄中找到Point.dll了。

創建Main項目與創建Point項目非常類似,除了這個項目叫做"Main",且源文件為Main.cpp外。(在此有一個小技巧,你可以運行Visual Stuio.NET的兩個實例,這樣,你就可以同時編輯兩個項目了。)默認情況下,選擇"空項目"會生成一個.exe文件,這正是我們想要的。因為Main.cpp引用了Point類型,所以需要告訴編譯器在哪可以找這個類型的父類程序集:首先,在"解決方案資源管理器"中,右鍵單擊項目名Main,依次選擇選擇"屬性|配置屬性|常規",選擇"公共語言運行庫支持"為"公共語言運行庫支持(/clr)",點擊對話框的"應用"按鈕;其次,在"通用屬性|引用|添加新引用"對話框中,選擇"浏覽"選項頁,定位至Point目錄的Point.dll文件,點擊"確定"退出;最後,在"C/C++|代碼生成"中,把"運行時庫"改為多線程 DLL (/MD)。此時,選擇"生成",就會在Main\debug目錄中生成Main.exe了,執行此文件,就可以看到相應的輸出。

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