C 基礎教程物件導向(學習筆記(61))

2021-08-31 09:55:42 字數 3326 閱讀 9163

讓我們回到我們之前看過的乙個例子:

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...