C++語言常見問題解:#94 ~ #104

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

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
可能有無限大的 (但你可能只會相信有半打的 Smile 最佳化機會,像是 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
[email protected] / 315-353-6100 / FAX: 315-353-6110

發佈了26 篇原創文章 · 獲贊 0 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章