C++語言常見問題解答(3) (轉)

gugu99發表於2008-03-07
C++語言常見問題解答(3) (轉)[@more@]== Part 3/4  ============================ ============================= 
■□ 第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++ 並不僅是學習 "++" 的語法語意而已 
,一個組織想達到  的境界,卻未將 "OO" 的精神放進 OOP 裡的話,只是自欺罷 
了;會計的資產負債表會把他們的愚蠢顯現出來。 
 
C++ 程式撰寫標準應該由 C++ 專家來調整,不妨先在 comp.lang.c++ 裡頭問問題( 
但是不要用 "coding standard" 這種字眼;只要這樣子問:「這種技巧有何優缺點 
?」)。找個能幫你避開陷阱的高手,上個訓練課程,買程式庫,看看「好的」程式 
庫是否合乎你的程式撰寫標準。絕對不要光靠自己來制定標準,除非你對它已有某種 
程度的掌握。沒有標準總比有爛標準好,因為不恰當的「官方說法」會讓不夠聰明的 
平民難以追隨。現在 C++ 訓練課程及程式庫,已有十分興盛的市場。 
 
再提一件事:當某個東西炙手可熱時,招搖撞騙者亦隨之而生;務必三思而後行。也 
要問一下從某處修過課的人,因為老手不見得也是個好教員。最後,選個懂得指導別 
人的從業人員,而不是個對此語言/方法論只有過時知識的全職教師。 
 
【譯註】善哉斯言! 
 
======================================== 
 
Q84:我該在中間或是開頭來宣告區域變數? 
 
在第一次用到它的地方附近。 
 
物件在宣告的時候就會被初始化(被建構)。如果在初始化物件的地方沒有足夠的資 
訊,直到函式中間才有的話,你可以在開頭處初始個「空值」給它,等以後再「設定 
」其值;你也可以在函式中間再初始個正確的東西給它。以來說,一開始就 
讓它有正確的值,會比先建立它,搞一搞它,之後再重建它來得好。以像 "String" 
這種簡單的例子來看,會有 350% 的速度差距。在你的系統上可能會不同;當然整個 
系統可能不會降低到 300+%,但是“一定”會有不必要的衰退現象。 
 
常見的反駁是:「我們會替物件的每個資料提供 "set" 運作行為,則建構時的額外 
耗費就會分散開來。」這比效能負荷更糟,因為你新增了維護的夢靨。替每個資料提 
供 "set" 運作行為就等於對資料不設防:你把內部實作技巧都顯露出來了。你隱藏 
到的只有成員物件的實體“名字”而已,但你用到的 List、String 和 float(舉例 
來說)型態都曝光了。通常維護會比  執行時間耗費的資源更多。 
 
區域變數應該在靠近它第一次用到之處宣告。很抱歉,這和 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 ty)是說:編譯器會“靜態地”(於編譯時期)檢驗各運算 
的型態性,而不是產生執行時才會去檢查的程式碼。例如,在靜態型別之下,會 
去偵測比對函式引數的型態簽名,不正確的配對會被編譯器挑出錯誤來,而非在執行 
時才被挑出。 
 
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:當所有東西都衍生自單一的根類別( class),通常叫做 ""。 
提示 #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 語言非常(相對於只是程式的習慣)允許你置放一個 
overr 覆蓋(它會去呼叫個「我看不懂」的運作行為),以達到「隱藏住」繼承 
下來的運作行為的“效果”。更進一步,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  到 [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);它不隨任何物件之生命期而動態地改變,同一個子類別的不同 
物件,也不可能會有不同的成員函式的定義。 
 
現在請回頭重讀前面這一段,但稍作些代換: 
 * 「成員函式」 --&gt 「成員物件」 
 * 「型態簽名」 --&gt 「型別」 
 * 「本體」     --&gt 「真正所屬的類別」 
這樣子,你就看到虛擬資料的定義。 
 
從另一個角度來看,就是把「各個物件」的成員函式與「動態」成員函式區分開來。 
「各個物件」成員函式是指:在任何物件案例中,該成員函式可能會有所不同,可能 
會塞入函式指標來實作出來;這個指標可以是 "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++ 得花時間,以及高素質的訓 
練。若你想有個強大的工具,你必須投資下去。 
 
           <<<>>> 
 
======================================== 
 
Q104:參考語意效率不高,那麼我是否應該用傳值呼叫? 
 
不。 
 
前面的 FAQ 是討論“成員物件”(member object) 的,而不是函式引數。一般說來 
,位於繼承階層裡的物件,應該用參考或指標來傳遞,而非傳值,因為惟有如此你才 
能得到(你想要的)動態繫結(傳值呼叫和繼承不能安全混用,因為如果把大大的子 
類別物件當成基底的物件來傳值的話,它會被“切掉”)。 
 
除非有足以令人信服的反方理由,否則成員物件應該用數值,而引數該用參考傳遞。 
前幾則 FAQs 提到一些「足以信服的理由」,以支援“成員物件該用參考”一事了。 

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10748419/viewspace-1000515/,如需轉載,請註明出處,否則將追究法律責任。

相關文章