在c++中,什么是对象切片,什么时候发生?


当前回答

在我看来,除了你自己的类和程序架构/设计不好之外,切片并不是什么问题。

如果我将一个子类对象作为参数传递给一个方法,该方法接受一个超类类型的参数,那么我当然应该意识到这一点,并且知道在内部,被调用的方法只能使用超类(也称为基类)对象。

在我看来,提供一个请求基类的子类会导致子类特定的结果,这似乎只是一个不合理的期望,会导致切片成为一个问题。它要么在方法的使用上设计糟糕,要么子类实现糟糕。我猜这通常是牺牲了良好的OOP设计,而为了方便或提高性能的结果。

其他回答

这里的大多数答案都无法解释切片的实际问题。它们只解释了切片的良性情况,而不是不可靠的情况。与其他答案一样,假设您处理的是两个类A和B,其中B(公开)来自A。

在这种情况下,C++允许您将B的实例传递给A的赋值运算符(以及复制构造函数)。这之所以有效,是因为B的实例可以转换为常量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的复制品。但是,唉,这不是!如果你检查它,你会发现b2是一种弗兰肯斯坦生物,由b1的一些块(B从a继承的块)和b2的一些块组成(只有B包含的块)。哎哟

怎么搞的?默认情况下,C++不会将赋值运算符视为虚拟运算符。因此,行a_ref=b1将调用a的赋值运算符,而不是B的赋值运算符。这是因为,对于非虚拟函数,声明的(形式上:静态)类型(即a&)决定调用哪个函数,而不是实际的(形式:动态)类型(由于a_ref引用了B的实例,因此将是B)。现在,A的赋值运算符显然只知道A中声明的成员,因此它将只复制那些成员,而保留B中添加的成员不变。

解决方案

只分配给对象的一部分通常意义不大,但不幸的是,C++没有提供内置的方法来禁止这种情况。不过,你可以自己动手。第一步是使赋值运算符虚拟化。这将确保调用的始终是实际类型的赋值运算符,而不是声明的类型。第二步是使用dynamic_cast验证指定的对象是否具有兼容类型。第三步是在(protected!)成员assign()中进行实际赋值,因为B的assign(()可能希望使用a的assign)复制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
  }
};

注意,为了方便起见,B的运算符=协变地重写返回类型,因为它知道它返回的是B的一个实例。

在C++中,派生类对象可以分配给基类对象,但另一种方式是不可能的。

class Base { int x, y; };

class Derived : public Base { int z, w; };

int main() 
{
    Derived d;
    Base b = d; // Object Slicing,  z and w of d are sliced off
}

当派生类对象分配给基类对象时,对象切片发生,派生类对象的其他属性被切片以形成基类对象。

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

例如

class A {
   int foo;
};

class B : public A {
   int bar;
};

因此,类型B的对象有两个数据成员,foo和bar。

那么如果你要写这个:

B b;

A a = b;

然后b中关于成员栏的信息在a中丢失。

class A 
{ 
    int x; 
};  

class B 
{ 
    B( ) : x(1), c('a') { } 
    int x; 
    char c; 
};  

int main( ) 
{ 
    A a; 
    B b; 
    a = b;     // b.c == 'a' is "sliced" off
    return 0; 
}

如果您有一个基类a和一个派生类B,那么您可以执行以下操作。

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

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

现在,方法wantAnA需要派生的副本。然而,派生的对象不能完全复制,因为类B可以创建不在其基类A中的其他成员变量。

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

它可能是不完整的,它的行为类似于A对象(B类的所有特殊行为都丢失了)。