# [31] 引用與值的語義
## FAQs in section [31]:
* [31.1] 什么是值和/或引用傳遞,在C++用哪個最好?
* [31.2] 什么是“虛成員”,如何/為什么在C++中使用?
* [31.3] 怎么區別虛擬數據和動態數據?
* [31.4] 應該通常使用數據成員對象指針或者使用“組合”?
* [31.5] 什么是使用成員對象指針的3個相對性能開銷?
* [31.6] “內聯虛函數”的會被“內聯”嗎?
* [31.7] 聽起來像我不應該使用引用?
* [31.8] 引用的性能問題是否意味著我要使用值傳遞?
## 31.1 什么是值和/或引用傳遞,在C++用哪個最好?
對于引用,被賦值的是一個指針拷貝。而值傳遞,被賦值的是值的拷貝而不是指針。C++中你可以選擇使用賦值操作符或者拷貝值(值傳遞),或者使用指針拷貝來復制一個指針(引用傳遞)。C++也允許你重寫復制操作符來實現你想要的操作,但是默認選擇是拷貝值。
引用傳遞的好處:靈活和動態綁定(只有使用指針或者引用的時候,才能獲得動態綁定)。
值傳遞的好處:速度。因為值傳遞需要拷貝一個對象(而不是一個指針),你可能很奇怪為什么會這樣。事實是大家通常使用一個對象,而不是拷貝多個對象,因此偶爾的拷貝開銷比間接指針訪問對象帶來的開銷要小。
三種情況你會獲得一個對象而不是對象指針:本地對象,全局或者靜態對象,以及類的非指針成員對象。最重要的是后者(對象組合)。
下個FAQ會給出更多的值/引用傳遞的信息。請閱讀所有內容以有個全面認識。前幾個傾向于使用值傳遞,如果你只閱讀前幾個,可能你會得到一個片面的認識。
賦值還包括其他問題(比如淺拷貝和深拷貝),這里不討論這些。
## 31.2 什么是“虛成員”,如何/為什么在C++中使用?
"虛成員(Virtual Data)"允許子類改變父類的成員對象。C++并不嚴格支持“虛成員”,但是可以模擬實現。雖然實現的不是很漂亮。
模擬實現要求基類要有一個成員對象指針,子類必須提供一個新對象,基類的成員對象指針指向這個新對象。基類可以有一個或者多個正常的構造函數提供成員指針對象的對象(通過`new`),基類的析構函數將會“`delete`”這個對象。
例如, `Stack`類可能有個`Array`成員對象(使用指針)而子類`StrechableStack`可以重寫基類的`Array`成員為`StrechableArray`。要使這個實現,`StretchableArray`必須從`Array`繼承,這樣`Stack`類可以使用`Array*`。`Stack`類的正常構造函數可以初始化`Array*`為`new Array`, 但是`Stack`類也要有一個構造函數(很可能`protected`屬性的構造函數)可以接受一個來自子類的`Array*`。 `StretchableStack`類的構造函數為基類的這個特殊構造函數提供`new StretchableArray`對象。
好處:
* 易于實現`StretchableStack` (多數代碼可以被繼承)
* 用戶可以傳遞`StrechableStack`為`Stack`類型的參數或者變量
缺點:
* 為訪問`Array`增加了額外層
* 為堆內存分配需要增加額的`new`和`delete`操作
* 增加了額外的動態綁定開銷 (理由見下節FAQ)
換句話講,我們簡化了`StrechableStack`的實現代碼,但是所有的用戶都要付出代價。不幸的是,不僅`StrechableStack`用戶而且`Stack`用戶都要付出這個代價。
_請閱讀本節其他內容。(這樣你會有一個全面認識)_
## 31.3 怎么區別虛擬數據和動態數據?
最簡單的辦法是虛函數分析法。虛函數: 虛函數意味著“聲明(簽名)”在子類中必須一樣,但是“定義(實現)”可以被重寫。繼承的成員函數的重寫是子類的靜態屬性,不會隨著任何特定對象的改變而動態改變,也不可能應為子類的不同實例而有不同的實現。
現在重新閱讀上面段落,但是要做下面替換:
* "成員函數" → "成員對象"
* "簽名" → "類型"
* "實現" → "確切類"
這樣你就可以定義“虛數據”。
另外一種方法是辨別"per-object"成員函數和"dynamic" 成員函數。 "per-object" 成員函數是指在不同的實例中實現有可能不同的成員函數,可以使用函數指針實現,這個指針可以是`const`,因為該指針在對象的生命周期中不會被改變。而"dynamic" 成員函數是指將會隨時間而動態改變的成員函數,也可以由函數指針實現,但是函數指針不能為`const`。
概括一下上面的分析,數據成員有三種概念:
* 虛數據: 類的成員對象定義可以在子類中被重寫,假設成員對象的生命(類型)相同。這種重寫是子類的靜態屬性。
* per-object-data: 任何類的既定對象可以在初始化(wrapper對象)的時候實例化一個不同conformal(相同類型)的成員對象,成員對象的確切類是Wrapper類的靜態屬性。
* dynamic-data: 成員對象的確切類可以被動態改變。
他們相似的原因是他們都不被C++支持,只有很少情況下可以這樣使用。在這種情況下,模擬機制都是相同的:通過指向基類(很可能是抽象類)的指針。在支持“first class” abstraction mechanisms的語言中,可能這種區別很明顯一些,因為他們將會有各自不同的語法表示。
## 31.4 應該通常使用數據成員對象指針或者使用“組合”?
組合。
一般來說,你的成員對象應該被包含在組合對象中(并不總是這樣,包裝器(Wrapper)對象是一個你可以使用指針或者引用的好例子; 而N-to-1-uses-a關系也需要指針或者引用)。
完全包含成員對象性能優于指針的原因有三點:
* 訪問對象時候是否需要額外的間接訪問
* 額外的堆內存分配(在構造函數中使用`new`,在析構函數中使用`delete`)
* 額外的動態綁定(理由見下面FAQ)
## 31.5 什么是使用成員對象指針的3個相對性能開銷?
前一節FAQ列舉了3個相對性能開銷::
* 就自身來說,一個額外的間接訪問開銷不值一提。
* 堆內存分配可能成為一個性能問題(`malloc`的傳統實現的性能會下降,隨著內存分配的增加; 面向對象軟件很容易使得內存分配增加,除非你很細心)。
* 額外的動態綁定來自于對象指針,而不是對象。只要C++編譯器能夠知道確切的`class`, 虛函數調用就會被靜態綁定,靜態綁定允許內聯。而內聯將會帶來成千上萬的優化機會,比如 procedural integration, register lifetime issues等等。下面三種情況下C++編譯器能夠知道對象確切的`class`: 本地變量,全局/靜態變量,完全包含的成員對象。
因此完全包含成員對象允許重要的優化,而這在使用對象指針的情況下是不可能的。這是具有引用語義的編程語言為什么面臨繼承性能挑戰的主要原因。
_請閱讀下面__3__個__FAQ__一遍獲得全面理解!_
## 31.6 “內聯虛函數”的會被“內聯”嗎?
有時...
當對象是個指針或者引用的時候, 虛函數調用不能被內聯,因為函數必須被動態調用。原因:編譯器無法知道實際的代碼來調用直到運行時(即動態),因為該代碼可能是來自一個派生類,調用函數編譯以后才創建的。
因此,只有當編譯器知道虛函數調用的目標的“確切類”的時候,”內聯虛函數”才有可能被內聯。發生這種情況只有在編譯器知道一個實際的對象,也就是說,本地對象,全局/靜態對象,或在組合的完全包含對象,而不是一個指針或引用的時候。
注意,內聯和非內聯之間的差別遠遠超過普通函數調用和虛函數調用的差別。例如,普通函數調用和虛函數調用的差別常常只有兩個額外的內存引用,但內聯函數和非內聯函數的差別可以多達一個數量級(數以億計的調用無關緊要的成員函數, 內聯虛函數的損失可能會導致25倍的差距!Doug Lea, "Customization in C++," proc Usenix C++ 1990。
這種頓悟的實際后果:不要陷在無休止的辯論中(或銷售策略!),來比較編譯器/語言的虛函數調用的成本。和具有擴展“內聯”成員函數調用的語言/編譯器做比較是沒有任何意義的。也就是說,許多語言實現廠商帶鼓吹他們的調度策略是如何好,但如果沒有內聯成員函數的話,系統的整體性能會很差,正是因為靠內聯調度,他們才具有最好的性能。
_注意:請閱讀下面的__2__個__FAQs__一遍了解另一方面!_
## 31.7 聽起來像我不應該使用引用?
不對。
引用是個好東西。我們不能生活在沒有引用。我們只是不希望我們的軟件使用太多的指針。在C++中,你可以挑選你想要引用語義(指針/引用)以及值語義(如對象包含其他對象等)。在一個大的系統中,應該有一個平衡。然而,如果你無論什么都使用指針的話,你將得到許多速度方面的問題。
求解問題的對象往往要比更高層次的對象占用更多的存儲空間。這些”問題空間“抽象類的ID通常比他們的“值”更重要,因此以用語義應該被用于求解問題的對象。
請注意,這些求解問題的對象通常在較高的抽象層次,相比那些處于解決方案空間的對象來說。因此求解問題的對象通常有一個相對較低的使用頻率。因此,C ++中為我們提供了一個理想的情況:對于那些需要獨特的身份的對象,或過大而不能復制我們選擇使用引用語義,對于其他對象我們可以選擇值語義。因此,最高使用頻率的對象將最終使用值語義,因為在靈活性方面我們沒有損失,但是在性能方面,實現了我們最需要的!
這些只是真正的面向對象設計的諸多問題中的一部分。精通面向對象設計/C++需要需要時間和高質量的訓練。如果你想有一個強有力的工具,你要投入時間和精力。
_不要停下來!__無比閱讀下一個問題!!_
## 31.8 引用的性能問題是否意味著我要使用值傳遞?
不是。
前面FAQ談論的是成員對象,而不是參數。一般而言,對象是繼承層次結構的一部分,應該通過引用或指針來傳遞,而不是值傳遞,因為只有這樣你才能得到(期望的)動態綁定(按值傳遞和繼承不相符,因為派生類對象將會被切片 ,當按值傳遞到一個基類對象的時候)。
除非另有其他的理由,成員對象應當按值傳遞,參數應按引用傳遞。以前的FAQ里面討論了應該按引用傳遞的成員對象的“其他的理由”。
- C++ FAQ Lite
- [1] 復制許可
- [2] 在線站點分發本文檔
- [3] C++-FAQ-Book 與 C++-FAQ-Lite
- [6] 綜述
- [7] 類和對象
- [8] 引用
- [9] 內聯函數
- [10] 構造函數
- [11] 析構函數
- [12] 賦值算符
- [13] 運算符重載
- [14] 友元
- [15] 通過 <iostream> 和 <cstdio>輸入/輸出
- [16] 自由存儲(Freestore)管理
- [17] 異常和錯誤處理
- [18] const正確性
- [19] 繼承 — 基礎
- [20] 繼承 — 虛函數
- [21] 繼承 — 適當的繼承和可置換性
- [22] 繼承 — 抽象基類(ABCs)
- [23] 繼承 — 你所不知道的
- [24] 繼承 — 私有繼承和保護繼承
- [27] 編碼規范
- [28] 學習OO/C++
- [31] 引用與值的語義
- [32] 如何混合C和C++編程
- [33] 成員函數指針
- [35] 模板 ?
- [36] 序列化與反序列化
- [37] 類庫