程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> 關於C++ >> 工作中的C++:基本原理,重點推薦和結束語

工作中的C++:基本原理,重點推薦和結束語

編輯:關於C++

目錄

基本原理

拋棄托管擴展

未來的樂趣

編程的演變

終極目標

結束語,盡管不是再見

這個月,我要改變常規的問答形式來告訴您關於我在網上發現的一個非常不錯的文檔。幾個星期前,有人寫信詢問我,為什麼他不能在 C++/CLI 中聲明 const 函數:

// 引用類
ref class A {
  void f() const; // 不!
};

對此,我回復道:您就是不能,這是規則。公共語言基礎結構 (CLI) 是為支持諸如 Visual Basic®、Java 甚至是 COBOL 語言而設計的 - 這些語言甚至不知道 const 的含義。因為 CLI 不知道 const 成員函數為何物,所以您不能使用。

我打完回復後,模糊地記起了一些埋藏在記憶深處的一些東西,關於 const,關於如何處理編譯器提示以使其他語言可以忽略這些提示。我查找了以前的專欄,發現曾在 2004 年 7 月回答過一個關於 const 的問題。實際上,C++/CLI 的確允許聲明 const 數據成員和參數 - 但不是 const 成員函數。圖 1 顯示了一個小程序,其具有 const 靜態數據成員的引用類。如果編譯此程序,然後使用 ILDASM 來反匯編,您將看到類似於下面的信息:

field public static int32
  modopt([mscorlib]System.Runtime.CompilerServices.IsConst)
    g_private = int32(0x00000001)

Figure1const.cpp

////////////////////////////////////////////////////////////////
// 要編譯類型:
//  cl /clr const.cpp
//
#include <stdio.h>
ref class A {
  int m_val;
  // 允許使用常量數據成員,將生成 modopt
  static const int g_private = 1;
public:
  // 公共常量成員可由 Visual Basic 或其他不能識別常量的程序
  // 來修改(因而改用字面量)。
  literal int g_public = 1;
  A(int i) { m_val = i; }
  void print(); // const;       // 不!不允許使用常量 fn
};
void A::print()
{
  printf("Val is %d\n",m_val);
}
int main()
{
  A a(17);
  a.print();
}

Modopt(可選的修飾符)是一種對 CLI 使用者宣告的 MSIL 聲明符:如果您了解它,很好;如果不了解,可以完全忽略它。相反,modreq(所需的修飾符)表示:如果您不了解它,就不能使用此函數。Volatile 便是 modreq 的一個示例。因為一個 volatile 引用隨時都可以被操作系統/硬件(甚至是另一個線程)更改,所以 CLI 使用者最好知道 volatile 的含義,如果想要使用 volatile 對象的話。但 const 是可選的。請注意,盡管托管擴展會將 C++ const 對象轉化成 CLI 文字量,C++/CLI 不再這麼做了。因此要小心 - 如果聲明一個公共 const 數據成員,用像 Visual Basic 這樣的語言編寫的客戶端能夠改變它的值。如果您希望 CLI 客戶端不能改變該值,就應該聲明對象字面量,如在圖 1 中所示。但是,成員函數呢 - 為什麼 const 成員函數是不允許的?

基本原理

噢,我在查找回復的過程中,偶然發現了一篇很好的文章,稱為《A Design Rationale for C++/CLI》(C++/CLI 設計基本原理),出自 Microsoft 的 Herb Sutter 之手。Herb 是 C++/CLI 的架構師之一。您可以從 Herb 的博客上獲得我在後面稱作“基本原理”的文檔(URL 是非常長的字符串 - 只要在 Web 上搜索“C++/CLI Rationale”就會找到)。正如標題表明的那樣,“基本原理”解釋了 C++/CLI 中所有事物為何以那種方式存在。它回答了從“為什麼首先擴展 C++”到我自己關於 const 函數疑問的所有問題。我認為,每個想要了解 C++/CLI 深層原理的人都有必要閱讀“基本原理”。由於“基本原理”中有很多重復,因此我想給出一些重點推薦。

就讓我們從最重要的問題開始吧,為什麼首先擴展 C++?簡單明了的回答就是:要使 C++ 成為一等 CLI 公民。Microsoft® .NET Framework 是 Windows® 開發的未來。哦,連 COBOL 都支持 CLI。所以,C++/CLI 旨在於確保 C++ 的不斷成功。

但為什麼弄亂 C++?為什麼要用像 ^ 和 % 這樣討厭的新概念和像 ref、value 和 property 這樣的新關鍵字來擴展語言呢?哦,是因為沒有其他辦法了。關於此內容,“基本原理”引用的不是別的,正是 Bjarne Stroustrup 寫的:“類幾乎可以表示我們所需的所有概念。只有庫的道路確實不可行時,才應該走語言擴展的道路。”

對於普通的 C++ 代碼,把 CLI 作為目標就像是編寫一個針對不同處理器的編譯器:沒有問題。但是,CLI 引入了需要特殊代碼生成且不能在 C++ 中表達的新概念。例如,屬性需要特殊的元數據。無法用庫或模板實現屬性。“基本原理”討論了一些為屬性(和其他 CLI 功能)考慮的替代的語法,以及它們被拒絕的原因。

拋棄托管擴展

“基本原理”還解釋了為什麼 Microsoft 決定拋棄托管擴展。托管擴展為托管對象和本機對象均使用 * 是一個聰明勇敢的嘗試,將 C++ 和 CLI 統一,而不改變語言,但這樣使引用和本機對象之間的重要區別不明顯了。這兩種指針看起來一樣,但行為不同。例如,析構語義、復制構造函數和構造函數/析構函數中的虛擬調用都根據指針實際指向的對象種類而具有不同的行為。太糟了!正如“基本原理”所述,“不但要隱藏不必要的區別,而且要顯露本質的區別,這很重要”,再次引用 Bjarne 說過的:“我試著使重要操作高度可見”。本機和托管類是根本不同的領域,因此最好將區別突出,而不是掩蓋。C++/CLI 團隊考慮並否定了各種機制,最終決定是 gcnew ^(句柄)和 %(跟蹤引用)。像這樣區分托管和本機類具有意想不到的附加收益。例如,使用單獨的 gcnew 運算符來分配托管對象開啟了某一天本機類可以從托管堆分配的可能性 - 反之亦然!

您有沒有想過,為什麼托管擴展具有那些討厭的下劃線關鍵字(像 __gc 和 __value)?因為托管擴展嚴格遵循 C++ 標准,標准提到“如果你確實必須引入新的關鍵字,就應該將其命名為從雙下劃線開始!”猜猜會發生什麼?Microsoft 引入了 __gc、__value 和其他的關鍵字後,Redmondtonians 遭到了來自程序員們“出乎意料強烈”的抱怨。是的!全世界的程序員們都聯合起來了!只有放棄下劃線了。下劃線使代碼看起來很討厭,就像某種匯編語言程序或別的什麼。所以 C++/CLI 有 ref 和 value,沒有下劃線。這意味著向 C++ 添加新的關鍵字,但那又怎麼樣呢?就像 Bjarne 說的,“我的經驗是人們熱衷於引入概念的關鍵字,本身不具有關鍵字的概念很難講授。這個作用比人們口頭上表達的對新的關鍵字不喜歡來說更重要,更根深蒂固。”(確實如此。我喜歡 Bjarne 描述編程心理學。)所以 C++/CLI 拋棄了下劃線。通過使其成為位置關鍵字,而不是保留的關鍵字,它們就不會與可能已經用這些字作為變量或函數名稱的程序相沖突了。

好奇的讀者可能想知道:在拋棄其下劃線的過程中,gc 怎麼就變成 ref 了呢?正如“基本原理”解釋的那樣,關於托管類,重要之處不是它們存在於托管(垃圾收集)堆上。重要之處是它們的引用語義。句柄 (^) 起到類似於引用的作用,而不是指針。明白了嗎?讀過“基本原理”之後,一切自然明了。

如果顯露本質的區別很重要,那麼隱藏表面的區別也同樣重要。例如,無論對象是本機、引用還是值,所有運算符重載都按照任何一個 C++ 程序員都會期望的方式“正常運行”。如果需要 C++/CLI 掩蓋區別的其他例子,請看下面的摘錄:

// 引用類 R 作為本地變量
void f()
{
  R r; // 引用類位於棧上?
  r.DoSomething();
  ...
}

在這裡,r 看起來像是一個棧對象,但就連 Mopsie 大嬸都知道托管類不能在棧上進行物理分配;它們必須從托管堆中分配。那麼?編譯器可以使這個摘錄片斷按照期望的方式運行,方法是生成和下面一樣的內容:

// 編譯器如何才能看到它。
void f()
{
  R^ r = gcnew R; // 在 gc 堆上分配
  try
  {
   r->DoSomething();
   ...
  }
  finally
  {
   delete r;
  }
}

對象不存在於物理棧上,但誰又介意呢?重要的是局部變量語法按照任何一個 C++ 程序員都會期望的方式運行。特別是,r 的析構函數在離開 f 前被調用。而說到析構函數,同理解釋了 C++/CLI 恢復決定性析構的原因:這樣,析構就遵循了每個 C++ 程序員都熟悉並喜愛的相同語義。好啊!非決定性析構是托管擴展最痛苦的事情之一。C++/CLI 將析構函數映射到 Dispose 並為終結器引入特殊的 ! 語法。直到垃圾收集器抽出時間時,引用對象使用的內存才會被收回,但這沒什麼大不了。C++ 程序員不怎麼在乎一個對象被銷毀時收回內存;在乎的是析構函數可以在期望運行時運行。C++ 程序員經常使用構造/析構模式來初始化/釋放非內存資源(像文件句柄、數據庫鎖等)。使用 C++/CLI,對於引用和本機類,熟悉的構造/析構模式就會像您預期的那樣運行。Redmondtonians 值得信任,因為他坦白承認托管擴展存在的問題 - 還因為他處理了這些問題。

未來的樂趣

在“基本原理”更吸引人的章節中,有一個稱為“Future Unifications”(未來統一化)的章節。它給出了一些關於 C++/CLI 未來走勢的引人入勝的提示。例如,本機類當前無法從托管類派生,反之亦然。但可以獲得同樣的效果,解決辦法是:將“基”類作為數據成員,然後編寫 passthrough 包裝程序,該包裝程序只起到調用包含的實例的作用。哦,這個聽起來挺老套,那麼為什麼編譯器做不到呢?編譯器可以將混合對象作為分成兩部分的對象表示,其中一個是包含所有 CLI 部分的 CLI 對象,另一個是包含所有 C++ 部分的 C++ 對象。

在這裡,Sutter 報道了一個有趣的轶事:當他第一次向 Bjarne Stroustrup 表明這個混合類的主意時,Bjarne 走向書架“然後打開一本書,其中寫道他一貫堅持(不管批評)C++ 不必要求對象在一個單獨的內存塊中連續排列。”那時,沒有人看到非連續對象有任何好處 - 但那時也沒有誰預料到 .NET 和 CLI。Bjarne 堅持敞開非連續的大門使得混合對象成為可能。如果在未來的 C++/CLI 版本中看到它們,不要驚訝。而寓意就是:當您正在設計一種期望會永久存在並會以不可預計的方式發展的新的語言或復雜的程序時,請不要做不必要的設想,因為這樣會讓生活更輕松!

“基本原理”提供了另一則有趣的歷史珍聞:Redmondtonians 原來為 C++/CLI 取的內部名稱是 MC^2。正如在 M(托管)C(++) 中,有一個致與 Albert Einstein 的帽尖 (^)。但正如 Sutter 所說的,那樣“太矯揉造作”。我同意。您真的認為如果它被稱作 MC^2,大家就會熱烈接受 C++/CLI 嗎?在決定將其稱為 C++/CLI 方面,架構師再一次沿著 Bjarne 的足跡。Bjarne 說:“我選擇了 C++,因為它簡短、有很好的釋義,而且它不是‘修飾的 C’的形式”。C++/CLI 表明 C++ 在前,而且還謹慎地避免了“修飾的 C++”的形式。

“基本原理”證明了其他的 C++/CLI 擴展(像 property、gcnew、generic 和 const)是有效的,並以很有用的常見問題解答部分結束。有關詳細信息,請下載“基本原理”自行閱讀。說到 const,回到我最初的問題 - 為什麼 C++/CLI 允許 const 數據,但不允許 const 函數呢?簡短的回答就是:CLI 不允許您將 modopt/modreq 直接放在函數上(盡管 CLI 實際上確實有辦法在元數據中編碼此信息,只是未測試而已)。至少現在還不行,“基本原理”闡述得很小心,暗示可能某一天會加上這個功能。

編程的演變

C++/CLI 使 C++ 成為了一等 CLI 公民。此外,如果您閱讀了“基本原理”,就會意識到做到這一點只對 C++ 作了最少的改動。而且 C++ 仍然是系統編程最好的語言,因為其相對於任何其他語言,提供了更多對 CLI 的直接訪問,還因為您如果想要調用舊的 Win32® API(將會陪伴我們更長的時間,我肯定),仍可以降回到 C。

要理解 C++/CLI 有多麼重要以及它代表了什麼,就必須考慮我們處於編程演變過程中的什麼位置。就讓我給您一個簡潔、特別而快速的回顧吧。在過去,程序員使用切換開關來編寫程序。紙帶是一個改進,但每台計算機都有其自身的“機器”語言。隨著計算機的發展壯大,程序員不得不為每台新的機器都重新編寫其程序。唉,那是非常煩的,因此程序員發明了像 FORTRAN、BASIC 和 C 這樣的高級語言,使用了稱為“編譯器”的東西來將高級語言翻譯為針對每台機器的機器指令。圖 2 說明了這點。現在,可以編寫一次程序,然後針對不同的機器進行編譯。太酷了!C 語言成為系統編程的選擇,因為它是“最低級的高級語言”,這意味著它在自身和機器之間引入了最少的累贅。現在使用的大多數操作系統都是用 C 語言編寫的,在少數對性能要求特別嚴格或與硬件交互的部分可能采用匯編語言進行編碼。

許多年以後,C++ 改進了 C 語言,使其面向對象,哦,而且更有趣。再次引用 Bjarne 的話:“C++ 的設計旨在於使作者和他的朋友不必用匯編語言、C 語言或各種現代高級語言來編程。它的主要目的就是使單個程序員更輕松愉快地編寫好的程序。”C++ 很棒,但是高級語言互相之間的通訊不太好。如果您用 C++ 編寫了某些代碼,那麼不能在 BASIC 中使用它 - 反過來也一樣,至少非常困難。每種語言都在其自己的世界中運行。對於獨立的應用程序來說,這很好,但是隨著應用程序變得更加復雜和環境更加分散,共享代碼的需求就變得更加迫切。從第一個子程序開始,程序員就在尋求完全封裝的可重用組件的終極目標:程序員可通過組裝軟件小構造塊來創建應用程序。那麼為什麼所有部分都得用相同的語言編寫呢?

多年來,形成了各種各樣解決互操作組件問題的辦法。起初,語言與庫一起提供(想想 C 運行庫和 printf)。在 Windows 領域內,DLL 提供了延時加載(DLL 中的動態)來節約內存。DLL 也提供了互操作性,因為像 Visual Basic 和 COBOL 這樣的語言可以調用 DLL,方法是通過引入指引編譯器將正確的 C 鏈接調用發到 DLL 中的導入語句。但是應用程序和 DLL 之間的鏈接太緊密、太脆弱而且太容易中斷。每個應用程序都要知道 DLL 中每個入口的名稱和簽名。另外,反向調用(從 DLL 到應用程序)也很討厭,因為您必須傳遞函數指針作為回調。因此,程序員發明了 VBX,後來成為了 OCX,再後來成為了 COM。很高興,COM 與語言無關:它具有“類型庫”,所以語言不必在鏈接時知道函數;它們可以在運行時查詢類型庫。COM 非常酷,但眾所周知,很難編寫。(諷刺地是,用 C++ 編寫 COM 是最困難的,而 COM 正是使用這種語言構思和實現的!)COM 也有其他問題:它太低級了,而且不處理安全性或內存管理這樣的事情。

終極目標

現在是 2007 年,我們擁有 .NET Framework 及其標准子集 CLI。通過在編程語言和機器之間插入一個新的抽象層,CLI 以一種截然不同的方式解決了復用性問題。編譯器現在生成的是 MSIL 代碼,而不是機器指令,然後 CLI 虛擬機/實時 (JIT) 編譯器將代碼動態編譯為機器代碼。虛擬機(VES 或虛擬執行系統,對於那些喜歡縮寫詞的人)提供了一個位於機器之上的抽象層。虛擬機不是新的。事實上,它們已經存在了很長時間。像 Pascal 和 ZIL(Zork Implementation Language,在 Infocom 內部用於編寫游戲,我曾在 Infocom 工作過)這樣的語言,通過將高級程序編譯為 P 代碼(或 Z 代碼)來運行,隨後由虛擬機進行解釋。但是,CLI 提供了一個所有語言都可以使用的公共虛擬機(CLI 中的 C)。CLI 支持像類、屬性、繼承、反射等的基本概念。VES/VM 提供了像內存管理和安全性這樣的功能,因此程序員不必擔心緩沖區溢出,或者其它可能為惡意病毒打開大門的程序錯誤。隨著 .NET Framework 和 CLI 越來越受歡迎,像 Visual Basic、Fortran、COBOL 和 C# 這樣的高級語言都已變得越來越相似,也與 C++ 越來越相似,因為它們都必須支持基本 CLI 概念 - 類、繼承、成員函數及其他基本概念。每種語言仍保留其特性,因此程序員不必完全重新學習以使用 .NET Framework;它們只需要學習一些新的概念。

所以,現在程序員可以用他們所選的任何語言來編寫類,其他程序員可以用他們所選的任何語言來使用這些類。任何人可以用任何語言來編寫任何組件,只需很少的編程就可以使所有組件無縫地一起工作。它們均受益於安全性、垃圾收集和其他的基礎特征(CLI 中的 I)。而且,將來 Redmondtonians 添加新的 CLI 特征後,所有語言也都將從中受益。有了 Windows Vista™ 和 .NET Framework 3.0(具有與 Windows Presentation Foundation、Windows Communication Foundation、Windows Workflow Foundation 和 Windows CardSpace™ 這樣的新技術相聯系的 10,000 個新類),Windows 本身正被作為 CLI 類重寫。可重用的、語言無關並且可互操作的組件的目標似乎已經最終達到了。這代表了巨大的模式轉變,而且令人驚訝地以一種相對漸進、進化的方式進行。您應該高興!編程的確是變容易了。

如果您回首凝視,就會發現 C++ 怎麼能不參與到這個“勇敢的新世界”呢?C++/CLI 就加入進來了!沒有 C++/CLI,C++ 就會成為不能用於編寫 Windows 程序的僅有的現代編程語言,處於孤立的位置。C++ 可能會緩慢消亡,或至少嚴重地被邊緣化。C++/CLI 確保不會發生那樣的情況。它保證了熱愛 C++ 的程序員(像我)可以在新的時代繼續使用它。圖 2 說明了從物理機器到虛擬機的模式轉換,以及“C++”在何處適合和不適合“/CLI”。

圖 2編程是怎樣演變的

結束語,盡管不是再見

那麼,現在有一條特別的消息。這是最後一次在“工作中的 C++”中寫入(鍵入?)“您忠實的”。我的專欄要退休了。我喜歡把這看成是為了祝願我長命百歲 - 您知道,就像團隊讓許多優秀的運動員引退的方式?我的離開並不會影響 Microsoft 或 MSDN® 雜志對 C++ 所做的承諾。更正確的是,它反映了這樣一個簡單的事實,就是繼 164 個專欄(這是我的第 165 個)之後,橫跨了幾乎 14 年,DiLascia 先生有點累了。而且,這有一個有趣的轶事:執行編輯 Josh Trupin 最近稱我為“MSDN 雜志的 Cal Ripken”。哦,我雖不是棒球球迷(我更喜歡足球),但我也知道誰是 Cal Ripken。盡管我很好奇:Cal 到底打了多少場比賽呢?我在 Web 上找到了答案:2,632. 我還找到一個傳記,這樣寫道“大多數專家都認為 Cal 如果能休息一下的話,將會是一個更棒的運動員”。那麼,我把它作為了一個啟示。但從不害怕,這並不意味著我不會再為 MSDN 雜志寫稿了。

在離開之前,我想感謝所有忠實的讀者,是他們發來問題、指出錯誤和疏漏,使我真實可信,並給出善意的贊揚之詞,鼓舞了我的自尊心,這些我將永遠珍惜。還有一些讀者,甚至在發表之前測試解決方案來幫助我寫專欄。我還想要感謝在 MSDN 雜志和我一起工作過的所有了不起的同事們,包括以前和現在的,沒有特定的順序:Steve、Josh、Joanne、Eric、Etya、Terry、Laura、Joan(她們倆)、Nancy、Val 和 Milo。我漏下誰了嗎?多年來,這些人一直在大力支持我,並給了我廣闊的自由空間去寫我最想寫的內容。我尤其想要感謝 Gretchen Bilson。幾年前她離開了,但她是在 1993 年反聘我的人,那時 MSDN 雜志還是《Microsoft Systems Journal》。謝謝,Gretchen!

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