轉:C/C++ 應用程序路線圖

轉:C/C++ 應用程序路線圖
 
來源:[url]http://msdn2.microsoft.com/zh-cn/library/ms379588[/url](vs.80).aspx
Kate Gregory
Gregory Consulting
適用於:
Microsoft Visual C++
Microsoft.NET 公共語言運行庫 (CLR)
C++/CLI 擴展
Microsoft .NET Framework
WinFX
摘要: 本文針對(至少到目前爲止)在新的和現有的應用程序中還沒有以Microsoft .NET Framework 爲目標開發平臺的 C++ 程序員。 文中爲如何轉向 .NET Framework 而且避免拋棄現有代碼中的投資提供了一些指導,並闡述了爲什麼應該考慮轉向 .NET Framework ,這不僅適用於新的開發工作,對現有應用程序亦然。
本頁內容

爲什麼要使用公共語言運行庫?

Microsoft 總是支持程序員從他們自己的代碼中訪問操作系統功能。 在 Windows 的早期,我們從 C 語言程序中使用 Windows API ,進行函數調用(比如 GetMessage(), TranslateMessage(), 等等)以達到目的。 隨着時間變遷, Windows 功能開始使用 COM 組件比如 Shell 來公開。 爲了充分利用 Windows 完整的功能,程序員學會了 COM 的概念,創建了許多 COM 應用程序。 這種演變仍在繼續,如今,爲了充分發揮 WinFX 的功能,你的應用程序應該使用公共語言運行庫,也稱 CLR 了。 WinFX 是一種託管 API ,設計成從爲 CLR 編寫的託管應用程序中調用,通過 WinFX ,你的應用程序能夠利用 Avalon 和 Indigo 這樣的新技術的功能。 WinFX 是基於 .NET 對象模型的。
訪問操作系統功能當然不是 CLR 的唯一優勢: .NET Framework 提供了比 COM 和 DCOM 更佳而且更簡單的組件模型。 .NET Framework 中包含的託管代碼庫所能提供的功能,遠遠超出了操作系統本身,這使我們能夠將精力放在應用程序中專門解決特定問題的部分,而用不着再去處理許多人已經解決過的問題。 同樣,可以構建一個基於組件的解決方案,而不需涉及以前與 COM 和 DCOM 部署相關的那些困難。

相關語言回顧

這是否意味着你應該重新編寫所有應用程序呢? 當然不是。 如果原來的應用程序是用 C++ 編寫的,現在不對代碼做任何改變就可以針對 CLR 進行編譯。 重新編寫會帶來巨大風險,因爲在將程序移植到另一種語言比如 C# 時,可能在能夠工作的代碼中引入錯誤。 重新編寫和轉換代碼所能期望的最佳值,也不過是實現與以前同樣的功能。 如果想以 CLR 爲目標平臺根本不需要這樣做。 相反,你的時間和精力可以花在使用新的系統功能和擴展應用程序的功能上。
自 .NET Framework, CLR 和 C# 發佈以來的幾年中,許多開發人員都對 Microsoft 的 C++ 計劃感到驚奇。 有些人推測 C# 將取代 C++ ,事實上當然並非如此。 C# 是一種比 C++ 更容易學習的語言,它提供了訪問 CLR 功能的途徑。 對於已經瞭解 C++ 的人來說,要訪問 CLR 的功能,無需學習其他語言, C++ 具備 C# 中沒有的功能,因此轉向 C# 實際上將會喪失一些能力。
Microsoft Visual C++ 的每個版本與標準的兼容性都比前一版更好,當前版本 Visual C++ .NET 2003 ,大約與 ISO C++ 標準達到了 98% 兼容。 這一版本中訪問 CLR 功能的關鍵字都以雙下劃線開始,因此不會干擾與標準的兼容性。 雖然這種方式可行,但是很笨拙,而且不夠直觀。 Visual C++ 2005 中將包含一個新的 C++ 到 .NET 的綁定,該綁定正在以 C++/CLI 的名稱進行標準化。 這一修訂包括當前 C++ 標準中沒有的關鍵字,但是同樣不會干擾符合標準的 C++ 程序,因爲它們遵守 ISO C++ 的標準擴展機制。 C++/CLI 擴展的國際標準正在由 ECMA 制定,最終將提交給 ISO 。 與今天的 C# 一樣, C++/CLI 將被標準化,因此 Microsoft 將不會是 C++/CLI 編譯器的唯一來源。 Visual C++ 2005 現在已經處於 beta 版,因此我們可以立即探討這些新的擴展。 在這篇白皮書中,我們將在代碼示例中使用新的 C++/CLI 語法。 ( CLI 代表的是公共語言基礎結構,是 .NET Framework 的標準化部分,包括 .NET 公共語言運行庫。)

理解 .NET 上的 C++

當你編寫在 CLR 上運行的代碼時,你所編寫的就是託管代碼。 標準 C++ 代碼,也就是能夠在任何符合標準的 C++ 編譯器上編譯的代碼,可以編譯成非託管(本機)代碼或者編譯成 MSIL : 只需使用一個編譯器開關即可。 指定 /clr 選項,編譯器就會生成 MSIL ,一個在 CLR 上運行的程序集。 使你的代碼成爲託管的就是使用 編譯器選項。 不需要使用任何特殊的關鍵字或者太多改變代碼(如果需要改變的話),就能夠通過 /clr 選項乾淨利索地進行編譯。
編寫了託管代碼之後,就可以(如果你願意)使用 CLR 功能了,比如基類庫: 這是能夠實現 XML 操作、加密解密、數據訪問等等功能的強大的類庫。 非託管代碼,即沒有使用 /clr 選項編譯的代碼,就無法聲明託管類的實例,並按託管代碼的方式直接調用它們的方法。 可以通過 .NET Interop 從非託管代碼中訪問託管代碼,該技術能夠使 .NET 對象看上去像是一個 COM 組件。 這種方式與將代碼編譯成託管的,並直接調用託管代碼相比,肯定要慢。
無論是否使用基類庫(和其他託管庫),你仍然可以用 C++ 編寫程序,仍然擁有 C++ 賦予你的所有功能和靈活性。 可以使用模板,編寫操作符重載,創建內聯函數,等等。 編譯爲 MSIL 並不會阻止你使用任何 C++ 功能。 比如說,多重繼承被排除在外不是因爲代碼編譯爲 MSIL 了,而只是因爲你編寫的是託管代碼而已。

託管和非託管類型

一個普通的 C++ 類,就是編程語言入門課程中教授的那種,將定義一個非託管類型:
class A
{
private:
    int x;
public:
    A(int xx): x(xx) {}
};
無論是否帶 /clr 選項編譯代碼(託管還是非託管代碼),這都是一個非託管類型,也俗稱爲非託管類或者非託管數據。 這個類的實例可以分配在堆棧中,這也是編程語言入門課程中教授的內容:
A something(3);
它們還可以在本機或者非託管堆中創建:
A* otherthing = new A(4);
程序員然後還必須記住清除非託管堆中的對象,使用 delete 操作符:
delete otherthing;
無論是哪一種方式,都不會涉及垃圾回收器,即使代碼編譯爲 MSIL ,而且應用程序運行在運行庫上。
但是你也可能需要編寫託管類型(也稱託管類或者託管數據)。 這些類型可以從其他程序集中調用,其他運行在運行庫上的託管代碼,無論其他程序集是用什麼語言編寫的。 用 C# 、用 Visual Basic .NET 或者用你沒有聽說過但是碰巧能編譯成 MSIL 的語言編寫的代碼,都可以使用你的託管類型。 這種交互是由運行庫託管的,而且在大多數情況下,你的代碼和其他程序集所創建的那些類型實例的生存期也是由運行庫託管的。
這些託管類型是第一級的 .NET 對象。 用 C++/CLI 創建託管類型,可以採取一種自然的語法,與傳統 C++ 區別不大:
ref class R
{
private:
    int x;
public:
    R(int xx): x(xx) {}
};
這個類定義使用了一個空格關鍵字——僅包含一個空格的關鍵字。 從技術上說, C++/CLI 中並沒有 ref 關鍵字,而只有 ref class 關鍵字。 這意味着你可以使用名爲 ref 的變量而不會引起衝突。 而且這個類 R 可以被用 C# 或者 Visual Basic .NET 或者其他支持 .NET 的語言編寫的代碼所使用。 可以用 C++/CLI 編寫一個類庫,使用你作爲一位 C++ 程序員多年形成的技術和技巧,然後將這個庫用在根本沒有使用 C++ 編寫的應用程序中。 它們只需要是運行在 CLR 上的應用程序即可。

依然是 C++

轉向 CLR 並不意味着要轉向 C# 。 許多 C++ 開發人員在 C# 發佈時轉向了 C# 。 其原因多種多樣: C# 的嚮導和設計器支持更好,而且管理層經常會支持新語言,僅僅是因爲它新,而一些開發人員並沒有認識到託管應用程序也能夠用 C++ 創建。 但是許多開發人員都拒絕這種轉向,甚至某種程度上完全拒絕轉向 CLR 。 拒絕的理由中常見的主題是: “我喜歡 C++ 。” C++ 具有其他語言不具備的功能,比如真正的確定性析構和模板。 選擇用 C++ 編寫託管代碼時,你可以獲得所有 C++ 的功能和所有 CLR 功能: 可謂魚與熊掌兼得。
確定性析構
在其他支持 .NET 的語言比如 Visual Basic .NET, C# , 或者 Visual C++ .NET 2003 C++ 託管擴展中,實例的位置取決於要創建的類型。 如果創建的是一個託管類型的實例,它將創建在託管堆中:
Dim o as new R(3)   ' VB.NET
R o = new R(3); // C#
R* o = new R(3); // managed extensions for C++
實例(在所有這些例子中都是 o )使用的內存是由運行庫託管的,可以被垃圾回收器清除或者重新組織。
相反,如果你創建的是一個值類型的實例,在所有三種語言中,實例都是在堆棧中創建的:
Dim i As Int32 = 7   ' VB.NET
int i = 7; // C#
int i = 7; // managed extensions for C++
只有在 C++/CLI 中你能夠獲得自由,自行決定在哪裏創建對象,是否想讓實例的內存由運行庫託管。 你可以在託管堆中創建一個引用類的實例(即前面用 ref class 關鍵字定義的那個實例),如下:
R^ o = gcnew R(3);   // C++/CLI
如果願意,可以在堆棧中創建實例:
R os(3);
o 和 os 之間的區別在它們的生存期上,或者說得更加具體一些,是對它們生存期的控制力。 如果編寫的是託管代碼,你可能不會介意放棄對內存的控制權,反而願意信任運行庫和垃圾回收器爲你管理內存。 但是開發人員仍然需要操心與內存無關的清除工作: 比如關閉文件或者連接。 垃圾回收本身不足以處理你在應用程序中使用的所有資源。 在 C++ 中,這種與內存無關的清除通常是在析構函數中進行的。
託管堆中的對象是通過句柄 o 訪問的,當控制達到帶有 gcnew 的那一行時對象就開始存在。 未來某個時候, o 將超出控制範圍。 可能控制已經超過用 return 或者 exit 語句聲明它的代碼塊,可能代碼塊是 if, for, 或者 while 語句的謂詞而且控制已經以通常的方式離開,或者出現了異常。 無論原因如何, o 都將超出範圍。 這時候,事情變得有些複雜。 如果任何代碼都有句柄的副本,副本將到處都是,然後只要範圍中有句柄,對象就將繼續在託管堆中存在。 如果句柄對象應該回收了,但是回收的準確時間並不知道,因此何時運行析構函數是未知的。 這取決於應用程序施加的內存壓力數量等等因素。
對於堆棧中的對象 os ,情況就大大不同了。 在超出範圍後(按照使 o 超出範圍的同樣情況),對象的一切就結束了。 它的析構函數,如果有的話,將在 os 離開範圍後立即運行。 你可以準確地知道與內存無關的清除何時發生,而且能夠儘快發生。 這就是所謂確定性析構。
順便提及, os 實例(我們認爲它在堆棧中)實際上使用的是託管堆上的內存(依然是由垃圾回收器託管的)。 析構函數並不回收該實例使用的內存;它關心的是與內存無關的清除。 引用類型只能模擬爲在堆棧中。 如果你已經習慣不管內存管理,並信任垃圾回收器處理一切,這種模擬是非常理想的。
C# 中的 using 構造提供了類似的能力,但是 C++ 中的自動範圍更簡單: 編寫的代碼更少,而且不會忘記。 在 C++ 中的析構函數和可以用其他語言編寫的託管類型中的 Dispose() 方法之間有很好的對應關係: 實際上它們是相同的。 當 C# 代碼使用你的託管類型並調用 Dispose() 時,實際上運行的就是析構函數。 如果 C++/CLI 代碼使用一個不是用 C++ 編寫的託管類型,並在堆棧中創建它,當實例超出範圍時,就不會運行一個 C++ 析構函數,而是運行一個 Dispose() 方法。 對於 C++ 開發人員而言,這就是確定性析構。 這意味着我可以這樣編寫 C# 代碼:
{
    using( System::Data::SqlClient::SqlConnection conn
      = new System::Data::SqlClient::SqlConnection(connString) ) {
       // work with the connection in some way
       // including code that might throw an exception
       using( System::Data::SqlClient::SqlCommand cmd
         = new System::Data::SqlClient::SqlCommand(
               queryString, conn) ) {
          // work with the command
       // must write "using"s to call Dispose or Close
       }
    }
}
在 C++ 中,我可以編寫這樣的代碼:
{
    System::Data::SqlClient::SqlConnection conn(connString);
    // work with the connection in some way
    // including code that might throw an exception
    System::Data::SqlClient::SqlCommand cmd(queryString, %conn);
    // work with the command
    // don't call Dispose or Close explicitly
}
SqlConnection 和 SqlCommand 對象實現了 IDisposable ,但是 C++/CLI 程序員不需要記着調用 Dispose() 。 編寫代碼更少,不會遺忘,都是 C++ 中析構機制的自然優點。 使用 CLR 上的這一機制非常自然和直觀,而無需要求庫用 C++ 編寫或者實現析構函數。
模板
C++/CLI 中使我們能夠在堆棧中創建託管類型實例的因素,也使我們能夠在傳統 C++ 模板中使用託管類型:
set<String^>^ SetofStrings;
. . .
String^ s = "Hello World";
SetofStrings->insert( s );
這意味着使用託管類型(無論是你自己的還是來自基類庫)時, STL 中的一切都可以訪問。 C++/CLI 帶有一些輔助模板,包括 auto_close<> (這是一個智能指針的 Variant ,能夠調用它所包裝的實例的 Close 方法),和 marshal_as<> ,能夠轉換相關的類型比如 System::String 和 std::string 。

混合託管和本機開發

如果你還在猶豫是否用 /clr 選項重新編譯標準非託管 C++ 代碼,請不要煩惱! 在一個應用程序中混合和匹配託管和本機(非託管)代碼是非常直接的。
從託管代碼,你可以調用任何現有的非託管代碼,就像非託管到非託管似的。 包含頭文件,與庫連接之後,然後就一切就緒了。 你是調用一個老的 C 語言庫,一個 C++ 庫還是一個 COM API 都無關緊要。 你所調用的代碼使用了什麼庫也無關緊要。 只需 #include 並如常繼續即可。 你的代碼可以在託管呼叫代碼和非託管目標代碼之間過渡,然後爲託管代碼帶來最高性能的回報。 其他支持 .NET 的語言都沒有這樣的選擇。 這種功能,以前被稱爲 It Just Works interop ,現在稱爲 C++ Interop 。
從非託管代碼可以訪問所有託管類型,不過需要包裝爲 COM 類型。 regasm 實用工具爲託管類型生成和註冊類型庫,可以使用編譯器支持(比如 #import )從本機代碼中訪問組件。 這一選擇會影響性能: 可以改而選擇用 /clr 編譯呼叫代碼,並直接通過運行庫訪問託管類型。 爲了保持 C++ 的理念,選擇由你決定。

.NET 路線圖

C++/CLI 爲你提供了極大的餘地,可以選擇如何訪問由 CLR (以及由 WinFX )提供的功能。 如果你要編寫一個新的應用程序,用 C++/CLI 編寫以 CLR 爲目標平臺所具有的優點是不可抗拒的。 可以擁有可靠性,可以訪問重要的託管 API ,從託管庫中獲得極高的開發人員生產率,而又無需放棄訪問本機庫或者任何 C++ 範型。 進行這種決策簡直是再清楚不過了: 用 C++/CLI 爲 Windows 編寫一個託管應用程序。
那麼現有的應用程序怎麼辦呢? 有些應用程序已經到了它們生存週期的末期: 你已經不想增加功能或者進行明顯改動了。 用戶不再指望它與其他應用程序集成。 像這樣的應用程序無需轉向 .NET 。 它們將在未來運行在 Microsoft Windows 代號 "Longhorn" 之上,因此可以不去管它們。
剩下的就是還沒有到生存期末期的現有應用程序了。 你想維護和增強這些應用程序,可能還計劃在增強中使用託管庫。 或者你可能願意以 CLR 爲目標平臺以減少大型分佈式應用程序部署時的麻煩。 基本上,有三種選擇: 重寫,集成或者遷移。
重寫是風險最大的一種方式。 必須完整地閱讀代碼,找出 .NET 世界中有等效物的庫和構造。 例如,原來可能使用 MFC 構建 Windows 應用程序的用戶界面: 而今天 .NET 中的等效物是 Microsoft Windows 窗體,在 "Longhorn" 時代是 Avalon 。 可以使用 ADO 或者 MFC 數據訪問類處理數據: 而 .NET 的等效物是 Microsoft ADO.NET ,使用 DataSet 或者 DataReader 類。 尋找這些庫和構造的工作量是非常大的。 然後還要重新編寫應用程序的大部分,在此過程中還可能引入錯誤,所以還必須進行全面測試。 這都需要時間和成本,但是當工作全部完成,你將擁有一個現代的應用程序,全部用 C++/CLI 編寫,儘可能使用託管庫。
如果當前代碼基礎比較老,而且已經更新多次,或者你的開發團隊並不能很好地理解,但是它又必須以某種方式改進或者更新,那麼重新編寫可能是最適合你的方式。 如果必須創建可以驗證的程序集以用於部分信任情況,可能也需要採取一些重新編寫的方式。 雖然對於大多數開發人員來說這一方式是第一計劃,但是實際上它應該是最後的選擇。 如果現在你的代碼很簡潔,文檔齊全,性能很好,應該儘可能拒絕進行重新編寫。
集成方式是將所有 .NET 庫(基類庫, WinFX, Indigo, Avalon, 等等)都視爲要使用的新庫。 現有的代碼基礎保持不變,用使用這些新庫的新模塊擴展它。 可以將應用程序的主幹代碼重新編譯爲 MSIL ,直接調用運行庫中的新庫,並使用 C++ Interop 訪問老庫。 或者,可以將主幹代碼保留爲本機(非託管)代碼,使用 COM 可調用包裝來包裝託管類型,將它們公開給舊代碼。 進行一些性能測試有助於進行決策。 可以根據選擇用 /clr 編譯部分代碼基礎。
遷移方式處於上面兩個極端之間。 在你的應用程序已經分成多個組件或者層時最適合。 (巨大的單塊應用程序是非常難於維護的,因此無論如何都應該將應用程序組件化,但是這種重構也是一種形式的重新編寫,會帶來風險。) 然後可以將每個組件用公開託管類型的 C++/CLI 代碼包裝起來。 新的主幹應用程序可以使用這些託管類型,新的託管類型也可以與 .NET 庫交互。 隨着時間推移,在包裝中的本機“核心”可以重新編寫爲完全的託管代碼,只使用託管庫,如果有重要的性能原因或者代碼安全原因需要如此的話。

小結

無論進行何種開發: 從頭編寫一個新的應用程序,維護我們無需做太多工作的現有應用程序,或者使一個歷盡滄桑的老應用程序重新煥發青春, .NET Framework 都可以使你的工作更加簡單。 使用 C++/CLI 將應用程序與 CLR 集成,或者將應用程序遷移到 CLR ,都比將應用程序移植到 C# 更加可行。 C++/CLI 爲我們提供了CLR 上 C++ 的功能和靈活性,提供了真正的確定性析構(即使是託管類型),提供了性能最高的交互操作。 對於任何要以 CLR 爲目標平臺的開發應用程序的 C++ 程序員而言,這都是一種非常自然的選擇。
作者簡介
Kate Gregory 是一位 Microsoft 地區經理, Visual C++ MVP ,以及《 Microsoft Visual C++ .NET 2003 Kick Start 》一書的作者。 Gregory 諮詢公司爲全北美提供諮詢和開發服務,專門利用最新技術從事軟件開發、集成項目、技術寫作、指導和培訓。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章