对象切割 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)
,那么会调用正确的函数.
实际上最常用的方法仍然是使用引用或者指针保留多态行为,重载的做法限制了代码的泛化性和可扩展性。
虚函数和多态#
如果不使用虚函数,那么方法调用是静态的,也就是说在编译时决定调用的函数,此时的调用行为取决于声明的类型,而不是实际类型。 也就是说:
没有虚函数,即使避免了对象切割(例如通过传引用或指针),派生类的方法也不会通过基类的引用或指针被调用。在通过基类的指针或引用调用函数时,总是会调用基类的方法,而不是派生类中可能存在的重写版本。