對象切割 Object Slicing 算是 C++ 的一個常見的多態問題。當一個派生類對象賦值給一個基類時,會丟棄掉派生部分,只保留基類對象部分,造成一些意料之外的結果。
對象切割示例#
考慮下面的代碼:
class Animal {
public:
virtual string GetType() const { return "Animal"; }
virtual string GetVoice() const { return "Voice"; }
};
class Dog : public Animal {
public:
string GetType() const override { return "Dog"; }
string GetVoice() const override { return "Woof"; }
};
class Cat : public Animal {
public:
string GetType() const override { return "Cat"; }
string GetVoice() const override { return "Miaow"; }
};
void Type(Animal& a) { cout<<a.GetType(); }
void Speak(Animal& a) { cout<<a.GetVoice(); }
int main()
{
Dog d; Type(d); cout<<" speak "; Speak(d); cout<<" - ";
Cat c; Type(c); cout<<" speak "; Speak(c); cout<<endl;
return 0;
}
這裡的正確輸出:
"Dog speak Voice - Cat speak Voice"
Speak 函數的聲明確實使用的是按值傳遞,這會導致對象切割。在這種情況下,傳遞給 Speak 函數的 Animal 對象只會保留 Animal 基類部分的信息,派生類的信息(如 Dog 或 Cat 的特定實現)將會丟失。
避免切割#
這個問題可以通過改變 Speak 函數,使其接收引用而非對象本身來解決:
void Speak(Animal& a) { cout << a.GetVoice(); }
這樣修改後,多態性將得到正確應用,Speak (d) 將輸出 "Woof",Speak (c) 將輸出 "Miaow"。
也可以創建了重載,比如void Speak(Dog d)
和void Speak(Cat c)
,那麼會調用正確的函數。
實際上最常用的方法仍然是使用引用或者指針保留多態行為,重載的做法限制了代碼的泛化性和可擴展性。
虛函數和多態#
如果不使用虛函數,那麼方法調用是靜態的,也就是說在編譯時決定調用的函數,此時的調用行為取決於聲明的類型,而不是實際類型。 也就是說:
沒有虛函數,即使避免了對象切割(例如通過傳引用或指針),派生類的方法也不會通過基類的引用或指針被調用。在通過基類的指針或引用調用函數時,總是會調用基類的方法,而不是派生類中可能存在的重寫版本。