什么是对象切片?

时间:2008-11-08 11:10:14

标签: c++ inheritance c++-faq object-slicing

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

18 个答案:

答案 0 :(得分:560)

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

例如,

class A {
   int foo;
};

class B : public A {
   int bar;
};

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

然后,如果你写这个:

B b;

A a = b;

然后b中关于成员bar的{​​{1}}中的信息丢失了。{/ p>

答案 1 :(得分:426)

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

在这种情况下,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的副本。但是,唉,它!如果你检查它,你会发现b2是一个弗兰肯斯坦生物,由b1的一些块组成(BA继承的块),以及一些b2的块(仅B包含的块)。哎哟!

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

解决方案

仅分配给对象的某些部分通常没什么意义,但遗憾的是,C ++没有提供禁止内容的内置方法。但是,您可以自己动手。第一步是使赋值运算符虚拟。这将保证它始终是被调用的实际类型的赋值运算符,而不是声明的类型。第二步是使用dynamic_cast验证分配的对象是否具有兼容类型。第三步是在(受保护!)成员assign()中进行实际分配,因为B的{​​{1}}可能想要使用assign() {{1}复制A的成员。

assign()

请注意,为方便起见,A的{​​{1}}会共同覆盖返回类型,因为知道它正在返回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 } }; 的实例。

答案 2 :(得分:143)

如果您有基类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的所有特殊行为都会丢失)。

答案 3 :(得分:35)

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

#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'

答案 4 :(得分:31)

谷歌中的第三场比赛“C ++切片”给了我这篇维基百科文章http://en.wikipedia.org/wiki/Object_slicing和这个(加热,但前几篇文章定义了问题):http://bytes.com/forum/thread163565.html

所以当你将一个子类的对象分配给超类时。超类对子类中的附加信息一无所知,也没有足够的空间来存储它,因此附加信息会被“切掉”。

如果这些链接没有为“正确答案”提供足够的信息,请编辑您的问题,让我们知道您还在寻找什么。

答案 5 :(得分:28)

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

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

答案 6 :(得分:9)

在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
}

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

答案 7 :(得分:7)

那么......为什么输出的派生信息不好? ...因为派生类的作者可能已经更改了表示,因此切掉额外信息会更改对象所表示的值。如果派生类用于缓存对某些操作更有效的表示,但转换回基本表示的代价很高,则会发生这种情况。

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

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

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

答案 8 :(得分:7)

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

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

答案 9 :(得分:7)

<强> 1。解决问题的定义

如果D是基类B的派生类,则可以将Derived类型的对象分配给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

虽然允许上述分配,但分配给变量pet的值会丢失其品种字段。这称为切片问题

<强> 2。如何解决裁员问题

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

实施例

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

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

答案 10 :(得分:4)

在我看来,切片并不是一个问题,除非你自己的类和程序的架构/设计很差。

如果我将一个子类对象作为参数传递给一个方法,该方法接受类型为超类的参数,我当然应该知道并且在内部知道,被调用的方法将使用超类(也就是基类)仅对象。

在我看来,只有不合理的期望,提供一个子类,其中请求基类,将以某种方式导致子类特定的结果,将导致切片成为一个问题。它在使用该方法时设计较差或者子类实现较差。我猜它通常是牺牲良好的OOP设计而有利于权宜之计或性能提升的结果。

答案 11 :(得分:3)

好的,我会在阅读许多解释对象切片的帖子后尝试一下,但不会有问题。

可能导致内存损坏的恶性方案如下:

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

答案 12 :(得分:3)

在此处找到类似的答案:http://sickprogrammersarea.blogspot.in/2014/03/technical-interview-questions-on-c_6.html

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

<强>说明: 请考虑以下类声明:

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

由于基类复制函数不知道有关派生的任何内容,因此只复制派生的基本部分。这通常称为切片。

答案 13 :(得分:2)

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; 
}

答案 14 :(得分:1)

当切片数据成员时发生对象切片时,我看到了所有答案。在这里,我举一个例子,说明方法没有被覆盖:

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???
}
p(对象b)从A(对象a1和a2)派生。正如我们期望的那样,b和a1调用它们的成员函数。但是从多态性的角度来看,我们不希望b分配的a2不被覆盖。基本上,a2只保存b的A类部分,这就是C ++中的对象切片。

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

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

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

有关更多详细信息,请参见my post

答案 15 :(得分:-1)

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

以下是一个示例:

#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'

答案 16 :(得分:-1)

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

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
}

答案 17 :(得分:-2)

我刚遇到切片问题并立即降落在这里。所以,让我加上我的两分钱。

让我们举一个来自“生产代码”的例子(或者其他类似的东西):

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

因此UI提供了std::vector<Action>。想象一下像:

这样的功能
void push_back(Action toAdd);

现在我们已经从UI的角度确定了它的外观。到目前为止没问题。但是,在这个项目上工作的其他人突然决定在Action对象中需要更多信息的专门操作。出于什么原因。这也可以通过lambda捕获来解决。此示例未从代码中获取1-1。

所以这个家伙来自Action以增加他自己的味道 他将自酿家庭的一个实例传递给了push_back,但随后程序变得混乱。

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

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

我希望这个例子可以为那些在谈论AB以某种方式衍生出来时无法想象事物的人带来光明。