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


谷歌第三次匹配“C++切片”给了我这篇维基百科文章http://en.wikipedia.org/wiki/Object_slicing这(很热,但前几篇文章定义了问题):http://bytes.com/forum/thread163565.html

因此,当您将子类的对象分配给超级类时。超类对子类中的附加信息一无所知,并且没有空间存储它,因此附加信息被“分割”。

如果这些链接没有提供足够的信息来获得“好答案”,请编辑您的问题,让我们知道您还需要什么。


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

例如

class A {
   int foo;
};

class B : public A {
   int bar;
};

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

那么如果你要写这个:

B b;

A a = b;

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


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

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

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

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

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

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


切片问题很严重,因为它会导致内存损坏,而且很难保证程序不会受到这种问题的困扰。要用语言设计它,支持继承的类应该只能通过引用(而不是通过值)访问。D编程语言具有此属性。

考虑从A派生的类A和类B。如果A部分有一个指针p,而B实例指向B的附加数据,则可能发生内存损坏。然后,当附加数据被切片时,p指向垃圾。


所以为什么丢失派生信息很糟糕。。。因为派生类的作者可能已经改变了表示方式,使得分割额外信息改变了对象所表示的值。如果派生类用于缓存对某些操作更有效但转换回基表示代价高昂的表示,则会发生这种情况。

还认为有人应该提到你应该做什么来避免切片。。。获取C++编码标准、101条规则指南和最佳实践的副本。处理切片是#54。

它提出了一种有点复杂的模式来完全解决这个问题:有一个受保护的复制构造函数、一个受受保护的纯虚拟DoClone,以及一个带有断言的公共克隆,如果(进一步)派生类未能正确实现DoClone,该断言将告诉您。(克隆方法对多态对象进行适当的深度复制。)

您还可以在基显式上标记复制构造函数,如果需要,它允许显式切片。


C++中的切片问题源于其对象的值语义,这主要是由于与C结构的兼容性。您需要使用显式引用或指针语法来实现大多数其他语言中的“正常”对象行为,即对象总是通过引用传递。

简单的答案是,通过按值将派生对象分配给基础对象来切片对象,即,剩余对象只是派生对象的一部分。为了保持值语义,切片是一种合理的行为,其用途相对较少,这在大多数其他语言中都不存在。有些人认为它是C++的一个特性,而许多人则认为这是C++的怪癖/错误特性之一。


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

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

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


1.切片问题的定义

如果D是基类B的派生类,则可以将派生类型的对象分配给base类型的变量(或参数)。

例子

class Pet
{
 public:
    string name;
};
class Dog : public Pet
{
public:
    string breed;
};

int main()
{   
    Dog dog;
    Pet pet;

    dog.name = "Tommy";
    dog.breed = "Kangal Dog";
    pet = dog;
    cout << pet.breed; //ERROR

尽管上面的赋值是允许的,但赋值给变量宠物的值将丢失其品种字段。这被称为切片问题。

2.如何解决切片问题

为了解决这个问题,我们使用指向动态变量的指针。

例子

Pet *ptrP;
Dog *ptrD;
ptrD = new Dog;         
ptrD->name = "Tommy";
ptrD->breed = "Kangal Dog";
ptrP = ptrD;
cout << ((Dog *)ptrP)->breed; 

在这种情况下,没有动态变量的数据成员或成员函数被ptrD(后代类对象)指向的对象将丢失。此外,如果需要使用函数,则函数必须是虚拟函数。


好的,在阅读了许多解释对象切片的文章后,我会尝试一下,但不知道它是如何变得有问题的。

可能导致内存损坏的恶性场景如下:

类在多态基类上提供(意外地,可能是编译器生成的)赋值。客户端复制并切片派生类的实例。客户端调用一个虚拟成员函数,该函数访问切片状态。


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,其中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的一个实例。


切片意味着当子类的对象通过值或从期望基类对象的函数传递或返回时,子类添加的数据将被丢弃。

说明:考虑以下类声明:

           class baseclass
          {
                 ...
                 baseclass & operator =(const baseclass&);
                 baseclass(const baseclass&);
          }
          void function( )
          {
                baseclass obj1=m;
                obj1=m;
          }

由于基类复制函数不知道派生的任何信息,因此只复制派生的基部分。这通常被称为切片。


这些都是很好的答案。我只想在按值传递对象与按引用传递对象时添加一个执行示例:

#include <iostream>

using namespace std;

// Base class
class A {
public:
    A() {}
    A(const A& a) {
        cout << "'A' copy constructor" << endl;
    }
    virtual void run() const { cout << "I am an 'A'" << endl; }
};

// Derived class
class B: public A {
public:
    B():A() {}
    B(const B& a):A(a) {
        cout << "'B' copy constructor" << endl;
    }
    virtual void run() const { cout << "I am a 'B'" << endl; }
};

void g(const A & a) {
    a.run();
}

void h(const A a) {
    a.run();
}

int main() {
    cout << "Call by reference" << endl;
    g(B());
    cout << endl << "Call by copy" << endl;
    h(B());
}

输出为:

Call by reference
I am a 'B'

Call by copy
'A' copy constructor
I am an 'A'

当派生类对象被分配给基类对象时,派生类对象的其他属性将从基类对象中切下(丢弃)。

class Base { 
int x;
 };

class Derived : public Base { 
 int z; 
 };

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

当派生类对象分配给基类对象时,派生类对象的所有成员都将复制到基类对象,但基类中不存在的成员除外。这些成员被编译器切片。这称为对象切片。

下面是一个示例:

#include<bits/stdc++.h>
using namespace std;
class Base
{
    public:
        int a;
        int b;
        int c;
        Base()
        {
            a=10;
            b=20;
            c=30;
        }
};
class Derived : public Base
{
    public:
        int d;
        int e;
        Derived()
        {
            d=40;
            e=50;
        }
};
int main()
{
    Derived d;
    cout<<d.a<<"\n";
    cout<<d.b<<"\n";
    cout<<d.c<<"\n";
    cout<<d.d<<"\n";
    cout<<d.e<<"\n";


    Base b = d;
    cout<<b.a<<"\n";
    cout<<b.b<<"\n";
    cout<<b.c<<"\n";
    cout<<b.d<<"\n";
    cout<<b.e<<"\n";
    return 0;
}

它将产生:

[Error] 'class Base' has no member named 'd'
[Error] 'class Base' has no member named 'e'

我刚刚遇到了切片问题,很快就到了这里。所以让我再加上两美分。

让我们来举一个“生产代码”(或类似代码)的例子:


假设我们有一个可以调度动作的东西。例如,控制中心UI。此UI需要获取当前可以调度的事物的列表。因此,我们定义了一个包含分派信息的类。让我们称之为行动。因此,Action有一些成员变量。为了简单起见,我们只有2,即std::string名称和std::function<void()>f。然后它有一个void activate(),它只执行f成员。

因此,UI得到了一个std::vector<Action>。设想一些功能,如:

void push_back(Action toAdd);

现在,我们已经从UI的角度确定了它的外观。到目前为止没有问题。但是另一个从事这个项目的人突然决定,Action对象中有一些特殊的动作需要更多的信息。无论出于什么原因。这也可以通过lambda捕获来解决。此示例并非取自代码1-1。

所以这家伙从《行动》中派生出来,以增添自己的味道。他将自己制作的课程的一个实例传递给push_back,但随后程序就失控了。

那发生了什么?正如您可能已经猜到的:对象已被切片。

实例中的额外信息已经丢失,f现在容易出现未定义的行为。


我希望这个例子能给那些在谈论以某种方式派生的A和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{
public:
    virtual void Say(){
        std::cout<<"I am A"<<std::endl;
    }
};

class B: public A{
public:
    void Say() override{
        std::cout<<"I am B"<<std::endl;
    }
};

int main(){
   B b;
   A a1;
   A a2=b;

   b.Say(); // I am B
   a1.Say(); // I am A
   a2.Say(); // I am A   why???
}

B(对象B)从A(对象a1和a2)导出。正如我们所期望的,b和a1调用它们的成员函数。但从多态性的角度来看,我们不期望由b赋值的a2不会被重写。基本上,a2只保存b的A类部分,即C++中的对象切片。

要解决此问题,应使用引用或指针

 A& a2=b;
 a2.Say(); // I am B

or

A* a2 = &b;
a2->Say(); // I am B