Petty C++ - 对象切割

对象切割 Object Slicing 算是 C++ 的一个常见的多态问题。当一个派生类对象赋值给一个基类时，会丢弃掉派生部分，只保留基类对象部分，造成一些意料之外的结果。

对象切割示例

考虑下面的代码：

Copy
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 函数，使其接收引用而非对象本身来解决：

Copy
void Speak(Animal& a) { cout << a.GetVoice(); }

这样修改后，多态性将得到正确应用，Speak (d) 将输出 "Woof"，Speak (c) 将输出 "Miaow"。

也可以创建了重载，比如void Speak(Dog d)和void Speak(Cat c)，那么会调用正确的函数.

实际上最常用的方法仍然是使用引用或者指针保留多态行为，重载的做法限制了代码的泛化性和可扩展性。

虚函数和多态

如果不使用虚函数，那么方法调用是静态的，也就是说在编译时决定调用的函数，此时的调用行为取决于声明的类型，而不是实际类型。 也就是说：

没有虚函数，即使避免了对象切割（例如通过传引用或指针），派生类的方法也不会通过基类的引用或指针被调用。在通过基类的指针或引用调用函数时，总是会调用基类的方法，而不是派生类中可能存在的重写版本。