协慌网

登录 贡献 社区

什么是对象切片?

有人在 IRC 中提到它是切片问题。

答案

在 “切片” 中,您将派生类的对象分配给基类的实例,从而丢失了部分信息 - 其中一些信息被 “切片” 了。

例如,

class A {
   int foo;
};

class B : public A {
   int bar;
};

因此,类型B的对象具有两个数据成员foobar

然后,如果您要编写此代码:

B b;

A a = b;

然后, b有关成员bar的信息丢失在a

这里的大多数答案都无法解释切片的实际问题是什么。他们只说明切片的良性案例,而不说明危险的切片。像其他答案一样,假设您正在处理两个类AB ,其中B (公开地)从A派生。

在这种情况下,C ++ 允许您将B的实例传递给A的赋值运算符(也传递给副本构造函数)。之所以可行,是因为B的实例可以转换为const A& ,这是赋值运算符和复制构造函数所期望的参数。

良性案例

B b;
A a = b;

那里什么都没发生 - 您要求A的实例,它是B的副本,而这正是您所得到的。当然, a不会包含b的某些成员,但是应该如何?毕竟是A ,而不是B ,所以它甚至都没有听说过这些成员,更不用说能够存储它们了。

奸诈案

B b1;
B b2;
A& a_ref = b2;
a_ref = b1;
//b2 now contains a mixture of b1 and b2!

您可能会认为b2将是b1的副本。但是,a, 不是 !如果检查它,您会发现b2是弗兰肯斯坦主义的生物,它由b1一些块( BA继承的块)和b2一些块(仅B包含的块)组成。哎哟!

发生了什么?好吧,默认情况下,C ++ 不会将赋值运算符视为virtual 。因此,行a_ref = b1将调用A的赋值运算符,而不是B的赋值运算符。这是因为,对于非虚函数, 声明的类型(即A& )确定要调用哪个函数,而不是实际的类型(应为B ,因为a_ref引用B的实例)。现在, A的赋值运算符显然只知道A声明的成员,因此它将仅复制那些成员,而保持B添加的成员不变。

一个办法

仅分配给对象的一部分通常没有什么意义,但是不幸的是,C ++ 没有提供内置的方法来禁止这种情况。但是,您可以自己滚动。第一步是使赋值运算符为virtual 。这样可以确保始终调用的是实际类型的赋值运算符,而不是声明的类型。第二步是使用dynamic_cast验证分配的对象是否具有兼容类型。第三步是在(受保护的!)成员assign()进行实际分配,因为Bassign()可能要使用Aassign()复制A的成员。

class A {
public:
  virtual A& operator= (const A& a) {
    assign(a);
    return *this;
  }

protected:
  void assign(const A& a) {
    // copy members of A from a to this
  }
};

class B : public A {
public:
  virtual B& operator= (const A& a) {
    if (const B* b = dynamic_cast<const B*>(&a))
      assign(*b);
    else
      throw bad_assignment();
    return *this;
  }

protected:
  void assign(const B& b) {
    A::assign(b); // Let A's assign() copy members of A from b to this
    // copy members of B from b to this
  }
};

请注意,为纯粹方便起见, Boperator=协变量会覆盖返回类型,因为它知道它正在返回B的实例。

如果您具有基类A和派生类B ,则可以执行以下操作。

void wantAnA(A myA)
{
   // work with myA
}

B derived;
// work with the object "derived"
wantAnA(derived);

现在,方法wantAnA需要derived的副本。但是, derived的对象无法完全复制,因为类B可以发明不在其基类A其他成员变量。

因此,要调用wantAnA ,编译器将 “分片” 派生类的所有其他成员。结果可能是您不想创建的对象,因为

  • 可能不完整
  • 它的行为就像一个A (类B所有特殊行为都丢失了)。