C++語言常見問題解答(3) (轉)
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);它不隨任何物件之生命期而動態地改變,同一個子類別的不同
物件,也不可能會有不同的成員函式的定義。
現在請回頭重讀前面這一段,但稍作些代換:
* 「成員函式」 --> 「成員物件」
* 「型態簽名」 --> 「型別」
* 「本體」 --> 「真正所屬的類別」
這樣子,你就看到虛擬資料的定義。
從另一個角度來看,就是把「各個物件」的成員函式與「動態」成員函式區分開來。
「各個物件」成員函式是指:在任何物件案例中,該成員函式可能會有所不同,可能
會塞入函式指標來實作出來;這個指標可以是 "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 提到一些「足以信服的理由」,以支援“成員物件該用參考”一事了。
■□ 第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);它不隨任何物件之生命期而動態地改變,同一個子類別的不同
物件,也不可能會有不同的成員函式的定義。
現在請回頭重讀前面這一段,但稍作些代換:
* 「成員函式」 --> 「成員物件」
* 「型態簽名」 --> 「型別」
* 「本體」 --> 「真正所屬的類別」
這樣子,你就看到虛擬資料的定義。
從另一個角度來看,就是把「各個物件」的成員函式與「動態」成員函式區分開來。
「各個物件」成員函式是指:在任何物件案例中,該成員函式可能會有所不同,可能
會塞入函式指標來實作出來;這個指標可以是 "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/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- APatch常見問題解答
- RapidWeaver 8常見問題解答API
- Ubuntu 常見問題和解答Ubuntu
- C語言常見使用問題2C語言
- C語言指標常見問題C語言指標
- [譯] HTTP/2 常見問題解答HTTP
- NSIS 之 NsDialogs 常見問題解答
- Rhinoceros 6 for Mac的常見問題解答ROSMac
- Screaming Frog SEO Spider常見問題解答IDE
- 小遊戲引擎常見問題解答遊戲引擎
- 關於CleanMyMac常見問題與解答Mac
- 犀牛Rhinoceros 6 for Mac的常見問題解答ROSMac
- Python程式設計常見問題與解答Python程式設計
- SOLIDWORKS認證考試常見問題解答Solid
- 業務規則的常見問題解答
- GNU GPL 許可證常見問題解答(三)
- 全志RV1108常見問題操作解答
- MongoDB常見問題解答:時間與時區MongoDB
- 域名解析常見問題盤點及解答
- SAP document builder一些常見問題的解答UI
- FAQ | PerfDog 常見問題解答第二期
- 前端入門-day2(常見css問題及解答)前端CSS
- 【答疑】物件儲存OSS常見問題解答(工具類1)物件
- 【FAQ】統一掃碼服務常見問題及解答
- 【等保】二級等保常見問題解答彙總
- 【過等保】2022年過等保常見問題解答
- 光學字元識別工具包SmartZone常見問題解答字元
- 【答疑】物件儲存OSS常見問題解答(諮詢類1)物件
- 華為隨行WiFi 2暢享版常見問題解答WiFi
- 【答疑】物件儲存OSS常見問題解答(諮詢類2)物件
- 有關超聲波感測器的常見問題解答
- 程式碼簽名、驅動簽名的常見問題解答
- 域名解析需要多久生效?域名解析常見問題解答
- 【FAQ】申請Health Kit許可權的常見問題及解答
- 常見問題
- Mac有防火牆嗎?關於Mac防火牆常見的問題解答Mac防火牆
- 阿里雲centos7伺服器nginx配置及常見問題解答阿里CentOS伺服器Nginx
- IPv6轉換常見問題盤點
- js常見問題JS