C++語言常見問題解:#81 ~ #93

這是我從臺灣的http://www.cis.nctu.edu.tw/chinese/doc/research/c++/C++FAQ-Chinese/發現的《C++ Frequently Asked Questions》的繁體翻譯,作者是:葉秉哲,也是《C++ Programming Language》3/e繁體版的譯者,該文章是非常的好,出於學習用途而將它轉貼,本人未取得作者的授權,原文章的版權仍然歸屬原作者.

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.)。」買鏈接庫,
買訓練課程,買開發工具,買諮詢顧問。想靠自學來達到成功的工具廠商及應用/系
統廠商,都會發現成功很困難。

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

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

底下的 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 恐怖份子挑釁,讓他們趁我熟睡時戳我的輪胎(在我很難得
有空休息的這段時間內 Smile

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

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++ 編譯器利用公共繼承
額外附的語意信息,以提供靜態型別。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章