讓我們回到我們之前看過的乙個例子:
class base
virtual const char* getname() const
int getvalue() const };
class derived: public base
virtual const char* getname() const };
int main()
在上面的示例中,ref引用和ptr指向派生,derived具有base部分和derived部分。因為ref和ptr是base型別,ref和ptr只能看到派生的base部分 - 派生的derived部分仍然存在,但是根本無法通過ref或ptr看到。但是,通過使用虛函式,我們可以訪問函式的派生版本。因此,上面的程式列印:
derived is a derived and has value 5
ref is a derived and has value 5
ptr is a derived and has value 5
但是,如果不是設定base引用或指向derived物件的指標,我們只需將 derived物件分配給 base物件會發生什麼?
int main()
請記住,derived包含base部分和derived部分。當我們將derived物件分配給base物件時,只複製derived物件的base部分。派生部分不是。在上面的示例中,base接收派生的base部分的副本,但不接收derived部分的副本。衍生部分實際上已被「切掉」。因此,將derived類物件分配給base類物件稱為物件切片(或簡稱切片)。
因為變數base沒有derived部分,所以base.getname()解析為base :: getname()。
以上示例列印:
base is a base and has value 5
認真使用,切片可以是良性的。但是,如果使用不當,切片會以很多不同的方式導致意外結果。我們來看看其中一些案例。
切片和函式
現在,你可能會認為上面的例子有點傻。畢竟,為什麼你會像這樣分配派生到base?你可能不會。但是,切片更容易意外發生函式。
考慮以下功能:
void printname(const base base) //注意:base按值傳遞,而不是引用
這是乙個非常簡單的函式,具有通過value傳遞的const base物件引數。如果我們這樣呼叫這個函式:
int main()
編寫此程式時,您可能沒有注意到base是值引數,而不是引用。因此,當呼叫printname(d)時,我們可能期望base.getname()呼叫虛擬化函式getname()並列印「i am a derived」,這不是發生的事情。相反,derived物件d被切片,只有base部分被複製到base引數中。當base.getname()執行時,即使getname()函式被虛擬化,也沒有該類的派生部分供其解析。因此,該程式列印:
i am a base
在這種情況下,發生的事情非常明顯,但如果您的函式實際上沒有列印任何類似的識別資訊,那麼追蹤錯誤可能具有挑戰性。
當然,通過使函式引數成為引用而不是按值傳遞,可以很容易地避免切片(另外乙個原因是為什麼通過引用而不是值傳遞類是個好主意)。
void printname(const base &base) //注意:base現在通過引用傳遞
int main()
這列印:
i am a derived
切片向量
新程式設計師遇到切片問題的另乙個領域是嘗試用std :: vector實現多型性。考慮以下程式:
#include int main()
這個程式編譯得很好。但是在執行時,會列印:
i am a base with value 5
i am a base with value 6
與前面的示例類似,因為std :: vector被宣告為base型別的向量,所以當derived(6)被新增到向量時,它被切片。
解決這個問題要困難一些。許多新程式設計師嘗試建立乙個物件的std :: vector引用,如下所示:
std::vectorv;
不幸的是,這不會編譯。std :: vector的元素必須是可賦值的,而引用不能被重新賦值(僅初始化)。
解決這個問題的一種方法是製作乙個指標向量:
#include int main()
這列印:
i am a base with value 5
i am a derived with value 6
有效!但是,由於您現在必須處理動態記憶體分配,所以還有一點額外的麻煩。
int main()
這可以按照您的期望工作:
i am a base with value 5
i am a derived with value 6
並避免必須處理動態記憶體。
如果這一點看起來有點遲鈍或模糊(尤其是巢狀型別),那麼在我們介紹模板類之後再回過頭來看看它會更容易理解。
the frankenobject
在上面的例子中,我們已經看到切片導致錯誤結果的情況,因為派生類已被切掉。現在讓我們看一下派生物件仍然存在的另乙個危險情況!
請考慮以下**:
int main()
函式中的前三行非常簡單。建立兩個派生物件,並將base引用設定為第二個。
第四行是事情誤入歧途的地方。由於b指向d2,並且我們將d1分配給b,您可能會認為結果將是d1將被複製到d2中 - 如果b是derived,它將會是。但是b是base,並且預設情況下c ++提供的類的運算子不是虛擬的。因此,只有d1的base部分被複製到d2中。
因此,您將發現d2現在具有d1的base部分和d2的derived部分。在這個特定的例子中,這不是問題(因為derived類沒有自己的資料),但在大多數情況下,您將剛剛建立了乙個frankenobject - 由多個物件的一部分組成。更糟糕的是,沒有簡單的方法可以防止這種情況發生(除了盡可能避免這樣的分配)。
conclusion
雖然c ++支援通過物件切片將派生物件分配給基礎物件,但一般來說,這可能只會導致頭痛,並且通常應該盡量避免切片。確保您的函式引數是引用(或指標),並嘗試在派生類時避免任何型別的pass-by-value。
C 基礎教程物件導向(學習筆記5(2))
在編寫具有多個建構函式的類 大多數建構函式 時,必須為每個建構函式中的所有成員指定預設值會導致冗餘 如果更新成員的預設值,則需要觸控每個建構函式。從c 11開始,可以直接為普通類成員變數 不使用static關鍵字的變數 提供預設初始化值 class rectangle void print int ...
C 基礎教程物件導向(學習筆記(23))
過載一元運算子 與您目前看到的運算子不同,正 負 和邏輯非 運算子都是一元運算子,這意味著它們只能在乙個運算元上執行。因為它們僅對它們所應用的物件進行操作,所以通常將一元運算子過載實現為成員函式。所有三個運算元都以相同的方式實現。讓我們看一下我們如何在前面的例子中使用的cents類上實現operat...
C 基礎教程物件導向(學習筆記(24))
過載比較運算子相對簡單,因為它們遵循我們在過載其他運算子時看到的相同模式。因為比較運算子都是不修改左運算元的二元運算子,所以我們將使過載的比較運算子宣告為友元函式。這是乙個帶有過載運算子 和operator!的car類的示例。include include class car friend bool...