C++語言常見問題解答

== Part 3/4  ============================

comp.lang.c++ Frequently Asked Questions list (with answers, fortunately).
Copyright (C) 1991-96 Marshall P. Cline, Ph.D.
Posting 3 of 4.
Posting #1 explains copying permissions, (no)warranty, table-of-contents, etc

=============================
■□ 第14節:程式風格指導
=============================

Q81:有任何好的 C++ 程式寫作的標準嗎?

感謝您閱讀這份文件,而不是再發明自己的一套。

但是請不要在 comp.lang.c++ 裡問這問題。幾乎所有軟體工程師,或多或少都把這
種東西看成是「大玩具」。而且,一些想成為 C++ 程式撰寫標準的東西,是由那些
不熟悉這語言及方法論的人弄出來的,所以最後它只能成為「過去式」的標準。這種
「擺錯位置」的現象,讓大家對程式寫作標準產生不信任感。

很明顯的,在 comp.lang.c++ 問這問題的人,是想使自己更精進,不會因自己的無
知而絆倒,然而一些回答卻只是讓情況更糟而已。

========================================

Q82:程式撰寫標準是必要的嗎?有它就夠了嗎?

程式撰寫標準不會讓不懂 OO 的人變懂;只有訓練及經驗才有可能。如果它有用處的
話,那就是抑制住那些瑣碎無關緊要的程式片段--當大機構想把零散的程式設計組
織整合起來時,這些片段常常會出現。

但事實上你要的不光是這種標準而已。它們提供的架構讓新手少去擔心一些自由度,
但是系統化的方法論會比這些好看的標準做得更好。組織機構需要的是一致性的設計
與實行“哲學”,譬如:強型別或弱型別?用指標還是參考介面? stream I/O 還是
stdio? C++ 程式該不該呼叫 C 的?反過來呢? ABC 該怎麼用?繼承該用為實作的
技巧還是特異化的技巧?該用哪一種測試策略?一一去檢查嗎?該不該為每個資料成
員都提供一致的 "get" 和 "set" 介面?介面該由外往內還是由內往外設計?錯誤狀
況該用 try/catch/throw 還是傳回值來處理?……等等。

我們需要的是詳細的“設計”部份的「半標準」。我推薦一個三段式標準:訓練、諮
詢顧問以及程式庫。訓練乃提供「密集教學」,諮詢顧問讓 OO 觀念深刻化,而非僅
僅是被教過而已,高品質的程式庫則是提供「長程的教學」。上述三種培訓都有很熱
門的市場景況。(【譯註】無疑的,這是指美、加地區。)接受過上述培訓的組織都
有如此的忠告:「買現成的吧,不要自己硬幹 (Buy, Don't Build.)。」買程式庫,
買訓練課程,買開發工具,買諮詢顧問。想靠自學來達到成功的工具廠商及應用/系
統廠商,都會發現成功很困難。

【譯註】這一段十分具有參考價值。不過有些背景資料得提供給各位參考。別忘了:
        作者是美國人,是以該地為背景,且留意一下他所服務的公司是做什麼的..
        ... :-)   唉!國內有這麼多的專業顧問公司嗎? :-<

少數人會說:程式撰寫標準只是「理想」而已,但在上述的組織機構中,它仍有其必
要性。

底下的 FAQs 提供一些基本的指導慣例及風格。

========================================

Q83:我們的組織該以以往 C 的經驗來決定程式撰寫標準嗎?

No!

不論你的 C 經驗有多豐富,不論你有多高深的 C 能力,好的 C 程式員並不會讓你
直接就成為好的 C++ 程式員。從 C 移到 C++ 並不僅是學習 "++" 的語法語意而已
,一個組織想達到 OOP 的境界,卻未將 "OO" 的精神放進 OOP 裡的話,只是自欺罷
了;會計的資產負債表會把他們的愚蠢顯現出來。

C++ 程式撰寫標準應該由 C++ 專家來調整,不妨先在 comp.lang.c++ 裡頭問問題(
但是不要用 "coding standard" 這種字眼;只要這樣子問:「這種技巧有何優缺點
?」)。找個能幫你避開陷阱的高手,上個訓練課程,買程式庫,看看「好的」程式
庫是否合乎你的程式撰寫標準。絕對不要光靠自己來制定標準,除非你對它已有某種
程度的掌握。沒有標準總比有爛標準好,因為不恰當的「官方說法」會讓不夠聰明的
平民難以追隨。現在 C++ 訓練課程及程式庫,已有十分興盛的市場。

再提一件事:當某個東西炙手可熱時,招搖撞騙者亦隨之而生;務必三思而後行。也
要問一下從某處修過課的人,因為老手不見得也是個好教員。最後,選個懂得指導別
人的從業人員,而不是個對此語言/方法論只有過時知識的全職教師。

【譯註】善哉斯言!

========================================

Q84:我該在函數中間或是開頭來宣告區域變數?

在第一次用到它的地方附近。

物件在宣告的時候就會被初始化(被建構)。如果在初始化物件的地方沒有足夠的資
訊,直到函數中間才有的話,你可以在開頭處初始個「空值」給它,等以後再「設定
」其值;你也可以在函數中間再初始個正確的東西給它。以執行效率來說,一開始就
讓它有正確的值,會比先建立它,搞一搞它,之後再重建它來得好。以像 "String"
這種簡單的例子來看,會有 350% 的速度差距。在你的系統上可能會不同;當然整個
系統可能不會降低到 300+%,但是“一定”會有不必要的性能衰退現象。

常見的反駁是:「我們會替物件的每個資料提供 "set" 運作行為,則建構時的額外
耗費就會分散開來。」這比效能負荷更糟,因為你添加了維護的夢靨。替每個資料提
供 "set" 運作行為就等於對資料不設防:你把內部實作技巧都顯露出來了。你隱藏
到的只有成員物件的實體“名字”而已,但你用到的 List、String 和 float(舉例
來說)型態都曝光了。通常維護會比 CPU 執行時間耗費的資源更多。

區域變數應該在靠近它第一次用到之處宣告。很抱歉,這和 C 老手的習慣不同,但
是「新的」不見得就是「不好的」。

========================================

Q85:哪一種原始檔命名慣例最好? "foo.C"? "foo.cc"? "foo.cpp"?

如果你已有個慣例,就用它吧。如果沒有,看看你的編譯器,看它用的是哪一種。典
型的答案是:".C", ".cc", ".cpp", 或 ".cxx"(很自然的,".C" 副檔名是假設該
檔案系統會區分出 ".C" ".c" 大小寫)。

在 Paradigm Shift 公司,我們在 Makefiles 裡用 ".C",即使是在不區分大小寫的
檔案系統下(在有區分的系統中,我們用一個編譯器選項:「假設 .c 檔案都是 C++
的程式」;譬如:IBM CSet++ 用 "-Tdp",Zortech C++ 用 "-cpp",Borland C++用
"-P",等等)。

========================================

Q86:哪一種標頭檔命名慣例最好? "foo.H"? "foo.hh"? "foo.hpp"?

如果你已有個慣例,就用它吧。如果沒有,而且你的編輯器不必去區分 C 和 C++ 檔
案的話,只要用 ".h" 就行了,否則就用編輯器所要的,像 ".H"、".hh" 或是
".hpp"。

在 Paradigm Shift 公司,我們用 ".h" 做為 C 和 C++ 的原始檔案(然後,我們就
不再建那些純粹的 C 標頭檔案)。

========================================

Q87:C++ 有沒有像 lint 那樣的指導原則?

Yes,有一些常見的例子是危險的。
但是它們都不盡然是「壞的」,因為有些情況下,再差的例子也得用上去。
 * "Fred" 類別的設定運算子應該傳回 "*this",當成是 "Fred&"(以允許成串的設
   定指令)。
 * 有任何虛擬函數的類別,都該有個虛擬解構子。
 * 若一個類別有 {解構子,設定運算子,拷貝建構子} 其一的話,通常三者也都全
   部需要。
 * "Fred" 類別的拷貝建構子和設定運算子,都該將它們的參數加上 "const":分別
   是 "Fred::Fred(const Fred&)" 和 "Fred& Fred::operator=(const Fred&)" 。
 * 類別的子物件一定要用初始化串列 (initialization lists) 而不要用設定的方
   式,因為對使用者自訂類別而言,會有很大的效率差距(3x!)。
 * 許多設定運算子都應該先測試:「我們」是不是「他們」;譬如:
        Fred& Fred::operator= (const Fred& fred)
        {
          if (this == &fred) return *this;
          //...normal assignment duties...
          return *this;
        }
   有時候沒必要測試,但一般說來,這些情況都是:沒有必要由使用者提供外顯的
   設定運算子的時候(相對於編譯器提供的設定運算子)。
 * 在那些同時定義了 "+="、"+" 及 "=" 的類別中,"a+=b" 和 "a=a+b" 通常應該
   做同樣的事;其他類似的內建運算子亦同(譬如:a+=1 和 ++a; p[i] 和 *(p+i);
   等等)。這可使用二元運算子 "op=" 之型式來強制做到;譬如:
        Fred operator+ (const Fred& a, const Fred& b)
        {
          Fred ans = a;
          ans += b;
          return ans;
        }
   這樣一來,有「建構性」的二元運算甚至可以不是夥伴。但常用的運算子有時可
   能會更有效率地實作出來(譬如,如果 "Fred" 類別本來就是個 "String",且
   "+=" 必須重新配置/拷貝字串記憶體的話,一開始就知道它的最後長度,可能會
   比較好)。


==============================================
■□ 第15節:Smalltalk 程式者學習 C++ 之鑰
==============================================

Q88:為什麼 C++ 的 FAQ 有一節討論 Smalltalk?這是用來攻擊 Smalltalk 的嗎?

世界上「主要的」兩個 OOPLs 是 C++ 與 Smalltalk。由於這個流行的 OOPL 已有第
二大的使用者總數量,許多新的 C++ 程式者是由 Smalltalk 背景跳過來的。這一節
會回答以下問題:
 * 這兩個語言的差別?
 * 從 Smalltalk 跳到 C++ 的程式者,要知道些什麼,才能精通 C++?

這一節 *!*不會*!* 回答這些問題:
 * 哪一種語言「最好」?
 * 為什麼 Smalltalk「很爛」?
 * 為什麼 C++「很爛」?

這可不是對 Smalltalk 恐怖份子挑釁,讓他們趁我熟睡時戳我的輪胎(在我很難得
有空休息的這段時間內 :-) 。

========================================

Q89:C++ 和 Smalltalk 的差別在哪?

最重要的不同是:

 * 靜態型別或動態型別?
 * 繼承只能用於產生子型別上?
 * 數值語意還是參考語意 (value vs reference semantics)?

頭兩個差異會在這一節中解釋,第三點則是下一節的討論主題。

如果你是 Smalltalk 程式者,現在想學 C++,底下三則 FAQs 最好仔細研讀。

========================================

Q90:什麼是「靜態型別」?它和 Smalltalk 有多相似/不像?

靜態型別(static typing)是說:編譯器會“靜態地”(於編譯時期)檢驗各運算
的型態安全性,而不是產生執行時才會去檢查的程式碼。例如,在靜態型別之下,會
去偵測比對函數引數的型態簽名,不正確的配對會被編譯器挑出錯誤來,而非在執行
時才被挑出。

OO 的程式裡,最常見的「型態不符」錯誤是:欲對某物件啟動個成員函數,但該物
件並未準備好要處理該運算動作。譬如,如果 "Fred" 類別有成員函數 "f()" 但沒
有 "g()",且 "fred" 是 "Fred" 類別的案例,那麼 "fred.f()" 就是合法的,
"fred.g()" 則是非法的。C++(靜態地)在編譯期捕捉型別錯誤,Smalltalk 則(動
態地)在執行期捕捉。(技術上,C++ 很像 Pascal--“半”靜態型別--因為指
標轉型與 union 都能用來破壞型別系統;這提醒了我們:你用指標轉型與 union 的
頻率,只能像你用 "goto" 那樣。)

========================================

Q91:「靜態型別」與「動態型別」哪一種比較適合 C++?

若你想最有效率使用 C++,請把她當成靜態型別語言來用。

C++ 極富彈性,你可以(藉由指標轉型、union 或 #define)讓她「長得」像
Smalltalk。但是不要這樣做。這提醒了我們:少用 #define。

有些場合,指標轉型和 union 是必要的,甚至是很好的做法,但須謹慎為之。指標
轉型等於是叫編譯器完全信賴你。錯誤的指標轉型可能會毀壞堆積、在別的物件記憶
體中亂搞、呼叫不存在的運作行為、造成一般性錯誤(general failure)。這是很
糟糕的事。如果你避免用與這些相關的東西,你的 C++ 程式會更安全、更快,因為
能在編譯期就檢測的東西,就不必留到執行期再做。

就算你喜歡動態型別,也請避免在 C++ 裡使用,或者請考慮換另一個將型態檢查延
遲到執行期才做的語言。C++ 將型態檢驗 100% 都放在編譯時期;她沒有任何執行期
型態檢驗的內建機制。如果你把 C++ 當成一個動態型別的 OOPL 來用,你的命運將
操之汝手。

========================================

Q92:怎樣分辨某個 C++ 物件程式庫是否屬於動態型別的?

提示 #1:當所有東西都衍生自單一的根類別(root class),通常叫做 "Object"。
提示 #2:當容器類別 container classes,像 List、Stack、Set 等,都不是
         template 版的。
提示 #3:當容器類別(List、Stack、Set 等)把插入/取出的元素,都視為指向
         "Object" 的指標時。(你可以把 Apple 放進容器中,但當你取出時,編
         譯器只知道它是衍生自 Object,所以你得用指標轉型將它轉回 Apple* ;
         你最好祈禱它真的是個 Apple,否則你會腦充血的。)

你可用 "dynamic_cast"(於 1994 年才加入的)來使指標轉型「安全些」,但這種
動態測試依舊是“動態”的。這種程式風格是 C++ 動態型別的基本要素,你可以呼
叫函數:「把這個 Object 轉換成 Apple,或是給我個 NULL,如果它不是 Apple的
話」,你就得到動態型別了:直到執行時期才知道會發生什麼事。

若你用 template 去實作出容器類別,C++ 編譯器會靜態偵測出 99% 的型態資訊(
"99%" 並不是真的;有些人宣稱能做到 100%,而那些需要持續性 (persistence) 的
人,只能得到低於 100% 的靜態型別檢驗)。重點是:C++ 透過 template 來做到泛
型(genericity),而非透過繼承。

========================================

Q93:在 C++ 裡怎樣用繼承?它和 Smalltalk 有何不同?

有些人認為繼承是用來重用程式碼的。在 C++ 中,這是不對的。說明白點,「繼承
不是『為』重用程式碼而設計的。」

【譯註】這一個分野相當重要。否則,C++ 使用者就會感染「繼承發燒症」
        (inheritance fever)。

C++ 繼承的目的是用來表現介面一致性(產生子類別),而不是重用程式碼。C++ 中
,重用程式碼通常是靠「成份」(composition) 而非繼承。換句話說,繼承主要是用
來當作「特異化」(specialization) 的技術,而非實作上的技巧。

這是與 Smalltalk 主要的不同之處,在 Smalltalk 裡只有一種繼承的型式(C++ 有
"private" 繼承--「共享程式碼,但不承襲其介面」,有 "public" 繼承--表現
"kind-of" 關係)。Smalltalk 語言非常(相對於只是程式的習慣)允許你置放一個
override 覆蓋(它會去呼叫個「我看不懂」的運作行為),以達到「隱藏住」繼承
下來的運作行為的“效果”。更進一步,Smalltalk 可讓觀念界的 "is-a" 關係“獨
立於”子類別階層之外(子型別不必也是子類別;譬如,你可以讓某個東西是一個
Stack,卻不必繼承自 Stack 類別)。

相反的,C++ 對繼承的限制更嚴:沒辦法不用到繼承就做出“觀念上的 is-a”關係
(有個 C++ 的解決方法:透過 ABC 來分離介面與實作)。C++ 編譯器利用公共繼承
額外附的語意資訊,以提供靜態型別。

========================================

Q94:Smalltalk/C++ 不同的繼承,在現實裡導致的結果是什麼?

Smalltalk 讓你做出不是子類別的子型別,做出不是子型別的子類別,它可讓
Smalltalk 程式者不必操心該把哪種資料(位元、表現型式、資料結構)放進類別裡
面(譬如,你可能會把連結串列放到堆疊類別裡)。畢竟,如果有人想要個以陣列做
出的堆疊,他不必真的從堆疊繼承過來;喜歡的話,他可以從陣列類別 Array 中繼
承過來,即使 ArrayBasedStack 並“不是”一種陣列!)

在 C++ 中,你不可能不為此操心。只有機制(運作行為的程式碼),而非表現法(
資料位元)可在子類別中被覆蓋掉,所以,通常你“不要”把資料結構放進類別裡比
較好。這會促成 Abstract Base Classes (ABCs) 的強烈使用需求。

我喜歡用 ATV 和 Maseratti 之間的差別來比喻。ATV(all terrain vehicle,越野
車)很好玩,因為你可以「到處逛」,任意開到田野、小溪、人行道等地。另一方面
,Maseratti 讓你能高速行駛,但你只能在公路上面開。就算你喜歡「自由表現力」
,偏偏喜歡駛向叢林,但也請不要在 C++ 裡這麼做;它不適合。

========================================

Q95:學過「純種」的 OOPL 之後才能學 C++ 嗎?

不是(事實上,這樣可能反而會害了你)。

(注意:Smalltalk 是個「純種」的 OOPL,而 C++ 是個「混血」的 OOPL。)讀這
之前,請先讀讀前面關於 C++ 與 Smalltalk 差別的 FAQs。

OOPL 的「純粹性」,並不會讓轉移到 C++ 更容易些。事實上,典型的動態繫結與非
子型別的繼承,會讓 Smalltalk 程式者更難學會 C++。Paradigm Shift 公司曾教過
數千人 OO 技術,我們注意到:有 Smalltalk 背景的人來學 C++,通常和那些根本
沒碰過繼承的人學起來差不多累。事實上,對動態型別的 OOPL(通常是,但不全都
是 Smalltalk)有高度使用經驗的人,可能會“更難”學好,因為想把過去的習慣“
遺忘”,會比一開始就學習靜態型別來得困難。

【譯註】作者是以「語言學習」的角度來看的。事實上,若先有 Smalltalk 之類的
        物件導向觀念的背景知識,再來學 C++ 就不必再轉換 "paradigm"--物件
        導向的中心思維是不會變的,變的只是實行細節而已。

========================================

Q96:什麼是 NIHCL?到哪裡拿到它?

NIHCL 代表 "national-institute-of-health's-class-library",美國國家衛生局
物件程式庫。取得法:anonymous ftp 到 [128.231.128.7],
檔案:pub/nihcl-3.0.tar.Z 。

NIHCL(有人唸作 "N-I-H-C-L",有人唸作 "nickel")是個由 Smalltalk 轉移過來
的 C++ 物件程式庫。有些 NIHCL 用到的動態型別很棒(譬如:persistent objects
,持續性物件),也有些地方動態型別會和 C++ 語言的靜態型別相衝突,造成緊張
關係。

詳見前面關於 Smalltalk 的 FAQs。


===============================
■□ 第16節:參考與數值語意
===============================

Q97:什麼是數值以及參考語意?哪一種在 C++ 裡最好?

在參考語意 (reference semantics) 中,「設定」是個「指標拷貝」的動作(也就
是“參考”這個詞的本意),數值語意 (value semantics,或 "copy" semantics)
的設定則是真正地「拷貝其值」,而不是做指標拷貝的動作。C++ 讓你選擇:用設定
運算子來拷貝其值(copy/value 語意),或是用指標拷貝方式來拷貝指標
(reference 語意)。C++ 讓你能覆蓋掉 (override) 設定運算子,讓它去做你想要
的事,不過系統預設的(而且是最常見的)方式是拷貝其「數值」。

參考語意的優點:彈性、動態繫結(在 C++ 裡,你只能以傳指標或傳參考來達到動
態繫結,而不是用傳值的方式)。

數值語意的優點:速度。對需要物件(而非指標)的場合來說,「速度」似乎是很奇
怪的特點,但事實上,我們比較常存取物件本身,較不常去拷貝它。所以偶爾的拷貝
所付出的代價,(通常)會被擁有「真正的物件本身」、而非僅是指向物件的指標所
帶來的效益彌補過去。

有三個情況,你會得到真正的物件,而不是指向它的指標:區域變數、整體/靜態變
數、完全被某類別包含在內 (fully contained) 的成員物件。這裡頭最重要的就是
最後一個(也就是「成份」)。

後面的 FAQs 會有更多關於 copy-vs-reference 語意的資訊,請全部讀完,以得到
較平衡的觀點。前幾則會刻意偏向數值語意,所以若你只讀前面的,你的觀點就會有
所偏頗。

設定 (assignment) 還有別的事項(譬如:shallow vs deep copy)沒在這兒提到。

========================================

Q98:「虛擬資料」是什麼?怎麼樣/為什麼該在 C++ 裡使用它?

虛擬資料讓衍生類別能改變基底類別的物件成員所屬的類別。嚴格說來,C++ 並不「
支援」虛擬資料,但可以模擬出來。不漂亮,但還能用。

欲模擬之,基底類別必須有個指標指向成員物件,衍生類別必須提供一個 "new" 到
的物件,以讓原基底類別的指標所指到。該基底類別也要有一個以上正常的建構子,
以提供它們自己的參考(也是透過 "new"),且基底類別的解構子也要 "delete" 掉
被參考者。

舉例來說,"Stack" 類別可能有個 Array 成員物件(採用指標),衍生類別
"StretchableStack" 可能會把基底類別的成員資料 "Array" 覆蓋成
"StretchableArray"。想做到的話,StretchableArray 必須繼承自 Array,這樣子
Stack 就會有個 "Array*"。Stack 的正常建構子會用 "new Array" 來初始化它的
"Array*",但 Stack 也會有一個(可能是在 "protected:" 裡)特別的建構子,以
自衍生類別中接收一個 "Array*"; StretchableArray 的建構子會用
"new StretchableArray" 把它傳給那個特別的建構子。

優點:
 * 容易做出 StretchableStack(大部份的程式都繼承下來了)。
 * 使用者可把 StretchableStack 當成“是一種”Stack 來傳遞。

缺點:
 * 多增加額外的間接存取層,才能碰到 Array。
 * 多增加額外的自由記憶體配置負擔(new 與 delete)。
 * 多增加額外的動態繫結負擔(原因請見下一則 FAQ)。

換句話說,在“我們”這一邊,很輕鬆就成功做出 StretchableStack,但所有用戶
卻都為此付出代價。不幸的,額外負荷不僅在 StretchableStack 會有,連 Stack
也會。

請看下下一則 FAQ,看看使用者會「付出」多少代價。也請讀讀下一則 FAQ 以後的
幾則(不看其他的,你將得不到平衡的報導)。

========================================

Q99:虛擬資料和動態資料有何差別?

最容易分辨出來的方法,就是看看頗為類似的「虛擬函數」。虛擬成員函數是指:在
所有子類別中,它的宣告(型態簽名)部份必須保持不變,但是定義(本體)的部份
可以被覆蓋(override)。繼承下來的成員函數可被覆蓋,是子類別的靜態性質
(static property);它不隨任何物件之生命期而動態地改變,同一個子類別的不同
物件,也不可能會有不同的成員函數的定義。

現在請回頭重讀前面這一段,但稍作些代換:
 * 「成員函數」 --> 「成員物件」
 * 「型態簽名」 --> 「型別」
 * 「本體」     --> 「真正所屬的類別」
這樣子,你就看到虛擬資料的定義。

從另一個角度來看,就是把「各個物件」的成員函數與「動態」成員函數區分開來。
「各個物件」成員函數是指:在任何物件案例中,該成員函數可能會有所不同,可能
會塞入函數指標來實作出來;這個指標可以是 "const",因為它在物件生命期中不會
變更。「動態」成員函數是指:該成員函數會隨時間動態地改變;也可能以函數指標
來做,但該指標不會是 const 的。

推而廣之,我們得到三種不同的資料成員概念:
 * 虛擬資料:成員物件的定義(真正所屬的類別)可被子類別覆蓋,只要它的宣告
   (型別)維持不變,且此覆蓋是子類別的靜態性質。
 * 各物件的資料:任何類別的物件在初始化時,可以案例化不同型式(型別)的成
   員物件(通常是一個 "wrapper" 包起來的物件),且該成員物件真正所屬的類別
   ,是把它包起來的那個物件之靜態性質。
 * 動態資料:成員物件真正所屬的類別,可隨時間動態地改變。

它們看起來都差不多,是因為 C++ 都不「直接支援」它們,只是「能做得出來」而
已;在這種情形下,模擬它們的機制也都一樣:指向(可能是抽象的)基底類別的指
標。在直接提供這些 "first class" 抽象化機制的語言中,這三者間的差別十分明
顯,它們各有不同的語法。

========================================

Q100:我該正常地用指標來配置資料成員,還是該用「成份」(composition)?

成份。

正常情況下,你的成員資料應該被「包含」在合成的物件裡(但也不總是如此;
"wrapper" 物件就是你會想用指標/參考的好例子;N-to-1-uses-a 的關係也需要某
種指標/參考之類的東西)。

有三個理由說明,完全被包含的成員物件(「成份」)的效率,比自由配置物件的指
標還要好:

 * 額外的間接層,每當你想存取成員物件時。
 * 額外的動態配置("new" 於建構子中,"delete" 於解構子中)。
 * 額外的動態繫結(底下會解釋)。

========================================

Q101:動態配置成員物件有三個效率因素,它們的相對代價是多少?

這三個效率因素,上一則 FAQ 有列舉出來:
 * 以它本身而言,額外的間接層影響不大。
 * 動態配置可能是個效率問題(當有許多配置動作時,典型的 malloc 會拖慢速度
   ;OO 軟體會被動態配置拖垮,除非你事先就留意到它了)。
 * 用指標而非物件的話,會帶來額外的動態繫結。每當 C++ 編譯器能知道某物件「
   真正的」類別,該虛擬函數呼叫就能“靜態”地繫結住,能夠被 inline。Inline
   可能有無限大的 (但你可能只會相信有半打的 :-) 最佳化機會,像是 procedural
   integration、暫存器生命期等等事項。三種情形之下,C++ 編譯器能知道物件真
   正的類別:區域變數、整體/靜態變數、完全被包含的成員物件。

完全被包含的成員物件,可達到很大的最佳化效果,這是「成員物件的指標」所不可
能辦到的。這也就是為什麼採用參考語意的語言,會「與生俱來」就效率不彰的原因
了。

注意:請讀讀下面三則 FAQs 以得到平衡的觀點!

========================================

Q102:"inline virtual" 的成員函數真的會被 "inline" 嗎?

Yes,可是...

一個透過指標或參考的 virtual 呼叫總是動態決定的,可能永遠都不會被 inline。
原因:編譯器直到執行時(亦即:動態地),才會知道該呼叫哪個程式,因為那一段
程式,可能會來自呼叫者編譯過後才出現的衍生類別。

因此,inline virtual 的呼叫可被 inline 的唯一時機是:編譯器有辦法知道某物
件「真正所屬的類別」之時,這是虛擬函數呼叫裡所要知道的事情。這只會發生在:
編譯器擁有真正的物件,而非該物件的指標或參考。也就是說:不是區域變數、整體
/靜態物件,就是合成物件裡的完全包含物件。

注意:inline 和非 inline 的差距,通常會比正常的和虛擬的函數呼叫之差別更為
顯著。譬如,正常的與虛擬的函數呼叫,通常只差兩個記憶體參考的動作而已,可是
inline 與非 inline 函數就會有一個數量級的差距(與數萬次影響不大的成員函數
呼叫相比,函數沒有用 inline virtual 的話,會造成 25X 的效率損失!
[Doug Lea, "Customization in C++," proc Usenix C++ 1990]).

針對此現象的對策:不要陷入編譯器/語言廠商之間,對彼此產品的虛擬函數呼叫,
做永無止盡的性能比較爭論(或是廣告噱頭!)之中。和語言/編譯器能否將成員函
數呼叫做「行內展開」相比,這種比較完全沒有意義。也就是說,許多語言編譯器廠
商,拼命強調他們的函數分派方式有多好,但如果他們沒做“行內”成員函數呼叫的
話,整體性能還是會很差,因為 inline--而非分派--才是最重要的性能影響因
素。

注意:請讀讀下兩則 FAQs 以看看另一種說法!

========================================

Q103:看起來我不應該用參考語意了,是嗎?

錯。

參考語意是個好東西。我們不能拋棄指標,我們只要不讓軟體的指標變成一個大老鼠
窩就行了。在 C++ 裡,你可以選擇該用參考語意(指標/參考)還是數值語意(物
件真正包含其他物件)的時機。在大型系統中,兩者應該取得平衡。然而如果你全都
用指標來做的話,速度會大大的降低。

接近問題層次的物件,會比較高階的物件還要大。這些針對「問題空間」抽象化的個
體本身,通常比它們內部的「數值」更為重要。參考語意應該用於問題空間的物件上
。

注意:問題空間的物件,通常會比解題空間的更為高階抽象化,所以相對地問題空間
的物件通常會有較少的交談性。因此 C++ 給我們一個“理想的”解決法:我們用參
考語意,來對付那些需要獨立的個體識別 (identity) 者,或是大到不適合直接拷貝
的物件;其他情形則可選擇數值語意。因此,使用頻率較高的就用數值語意,因為(
只有)在不造成傷害的場合下,我們才去增加彈性;必要時,我們還是選擇效率!

還有其他關於實際 OO 設計方面的問題。想精通 OO/C++ 得花時間,以及高素質的訓
練。若你想有個強大的工具,你必須投資下去。

           <<<< 還不要停下來!  請一併讀讀下一則 FAQ!! >>>>

========================================

Q104:參考語意效率不高,那麼我是否應該用傳值呼叫?

不。

前面的 FAQ 是討論“成員物件”(member object) 的,而不是函數參數。一般說來
,位於繼承階層裡的物件,應該用參考或指標來傳遞,而非傳值,因為惟有如此你才
能得到(你想要的)動態繫結(傳值呼叫和繼承不能安全混用,因為如果把大大的子
類別物件當成基底的物件來傳值的話,它會被“切掉”)。

除非有足以令人信服的反方理由,否則成員物件應該用數值,而參數該用參考傳遞。
前幾則 FAQs 提到一些「足以信服的理由」,以支持“成員物件該用參考”一事了。

--
Marshall Cline
--
Marshall P. Cline, Ph.D. / Paradigm Shift Inc / PO Box 5108 / Potsdam NY 13676
cline@sun.soe.clarkson.edu / 315-353-6100 / FAX: 315-353-6110



更新日期:



以上英文原作者為 Marshall Cline,中譯者為葉秉哲。
翻譯文件為 USENET comp. lang, c++ FAQ, Jan31, 1996。