什么是对象切片?


Answers:


608

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

例如,

class A {
   int foo;
};

class B : public A {
   int bar;
};

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

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

B b;

A a = b;

然后有关中的b成员信息bar会丢失a


66
内容非常丰富,但是请参见stackoverflow.com/questions/274626#274636,以获取有关在方法调用期间如何进行切片的示例(与传统的赋值示例相比,这种方法强调了危险)。
布莱尔·康拉德

55
有趣。我从事C ++编程已有15年了,这个问题从未出现在我身上,因为出于效率和个人风格的考虑,我一直以引用的方式传递对象。展示良好的生活习惯如何帮助您。
Karl Bielefeldt

10
@Felix谢谢,但是我不认为强制回退(因为不是指针算术)将起作用,A a = b; a现在是A具有的副本的类型的对象B::foo。我认为现在将其退回将是错误的。

37
这不是“切片”,也不是它的良性变体。如果这样做,就会出现真正的问题B b1; B b2; A& b2_ref = b2; b2 = b1。您可能会认为您已复制b1b2,但是还没有!你复制一个部分b1b2(部分b1B从继承A),并留下的其他部分b2保持不变。b2现在是一种弗兰肯斯坦的生物,由少量的b1跟随着的大块的组成b2。啊! 拒绝投票是因为我认为答案非常有误导性。
fgp

24
@fgp您的注释应为B b1; B b2; A& b2_ref = b2; b2_ref = b1如果您 ...派生自具有非虚拟赋值运算符的类,则会发生真正的问题。是A即使用于推导?它没有虚拟功能。如果从类型派生,则必须处理其成员函数可以被调用的事实!
curiousguy 2013年

508

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

在这种情况下,C ++允许您将的实例传递BA的赋值运算符(以及传递给副本构造函数)。之所以可行,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!

您可能会认为这b2b1以后的副本。但是,可惜不是!如果检查它,您会发现这b2是一个科学怪人的生物,它是由b1B继承自的块A)和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


11
恕我直言,问题在于继承可能暗含两种不同的可替代性:derived可以给base期望值的代码赋予任何值,或者可以将任何派生的引用用作基础引用。我想看到一种带有类型系统的语言,可以分别解决这两个概念。在很多情况下,派生的引用应该可以代替基本引用,但是派生的实例不可以替换为基本引用。在许多情况下,实例应该是可转换的,但引用不能替代。
2013年

16
我不明白您的“奸诈”案件有什么坏处。您声明要:1)获取对A类对象的引用,以及2)将对象b1强制转换为A类并将其内容复制到A类的引用。实际上,这里的错误是背后的正确逻辑给定的代码。换句话说,您拍摄了一个较小的图像框(A),将其放置在较大的图像(B)上,然后通过该框进行绘制,后来抱怨您的较大图像现在看起来很丑陋:)但是,如果我们仅考虑该框框区域,看起来很不错,就如画家所愿,对吗?:)
Mladen B.

12
换句话说,问题在于C ++默认情况下假设一种非常强的可替换性 -它要求基类的操作在子类实例上正确起作用。甚至对于编译器自动生成的操作(如赋值)。因此,仅在这方面加紧自己的操作是不够的,还必须显式禁用编译器生成的错误操作。或者,当然,远离公共继承,这通常是一个好建议;-)
fgp

14
另一种常见的方法是简单地禁用复制和赋值运算符。对于继承层次结构中的类,通常没有理由使用值代替引用或指针。
任思远2014年

13
什么啊 我不知道运营商可以打上虚拟的
paulm

153

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

40
C ++ 不是 Java!如果wantAnA(顾名思义!)想要一个A,那么它就会得到。而且的实例A将表现为A。这有多令人惊讶?
fgp

82
@fgp:令人惊讶,因为您没有将A传递给该函数。
黑色

10
@fgp:行为类似。但是,对于一般的C ++程序员而言,它可能不那么明显。据我了解的问题,没有人在“抱怨”。这只是关于编译器如何处理这种情况。恕我直言,最好通过传递(const)引用完全避免切片。
2013年

8
@ThomasW不,我不会抛出继承,而是使用引用。如果wantAnA的签名为void wantAnA(const A&myA),则表示没有切片。而是传递对调用方对象的只读引用。
黑色

14
问题主要在于编译器derived对类型的自动转换A。隐式强制转换始终是C ++中意外行为的来源,因为从本地查看代码通常很难理解强制转换是发生的。
pqnet 2014年

41

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

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

你好。好的答案,但我有一个问题。如果我做这样的事情** dev d; base * b =&d; **还可以切片吗?
阿德里安

@Adrian如果在派生类中引入一些新的成员函数或成员变量,则无法直接从基类指针访问这些成员函数或成员变量。但是,您仍然可以从重载的基类虚拟函数内部访问它们。看到这个:godbolt.org/z/LABx33
Vishal Sharma

30

谷歌中针对“ C ++切片”的第三次匹配使我在Wikipedia上看到了这篇文章,网址为http://en.wikipedia.org/wiki/Object_slicing,并且这篇文章很热烈,但前几篇文章定义了问题:Forum / thread163565.html

这样就可以将子类的对象分配给超类了。超类对子类中的附加信息一无所知,并且没有足够的空间来存储它,因此附加信息将被“分割”。

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


29

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

考虑从A派生的类A和类B。如果A部分具有指针p,并且B实例将p指向B的其他数据,则可能发生内存损坏。然后,当其他数据被分割时,p指向垃圾。


3
请说明如何发生内存损坏。
foraidt

4
我忘记了复制ctor将重置vptr,这是我的错误。但是,如果A有一个指针,并且B将其设置为指向B的被分割的部分,则仍然会损坏。
Walter Bright

18
这个问题不仅限于切片。任何包含指针的类在默认的赋值运算符和复制构造函数中都将具有可疑行为。
2009年

2
@Weeble-这就是在这种情况下为什么要覆盖默认析构函数,赋值运算符和copy-constructor的原因。
Bjarke Freund-Hansen,2009年

7
@Weeble:使对象切片比常规指针修复更糟糕的是,要确保您防止切片发生,基类必须为每个派生类提供转换的构造函数。(为什么?任何遗漏的派生类都易于被基类的副本ctor拾取,因为它Derived可以隐式转换为Base。)这显然与“开放式封闭原则”背道而驰,并且负担沉重。
j_random_hacker 2012年

10

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

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


8

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

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


5
正常对象行为 ”不是“正常对象行为”,而是参考语义。而且它与C ,兼容性或任何其他无意义的OOP牧师都毫无struct意义的无关
curiousguy

4
@curiousguy阿们,兄弟。这是可悲的,看看多久C ++得到来自不是Java的,撞坏当值语义是使C ++如此强大的疯狂的事情之一。
fgp

这不是功能,也不是古怪/错误的功能。这是正常的堆栈复制行为,因为调用带有arg或(相同)分配类型的堆栈变量的函数Base必须精确地sizeof(Base)占用内存中的字节,并可能对齐,这也许就是为什么“赋值”(on-stack-copy) )将不会复制派生的类成员,它们的偏移量不在sizeof范围内。为避免“丢失数据”,只需使用指针,就像其他任何指针一样,因为指针的内存在位置和大小上都是固定的,而堆栈却非常易变
Croll

绝对是C ++的缺点。应该禁止将派生对象分配给基对象,而将派生对象绑定到基类的引用或指针应该是可以的。
John Z. Li,

7

那么……为什么丢失派生信息不好呢?...因为派生类的作者可能已更改了表示形式,因此切下多余的信息会更改对象表示的值。如果派生类用于缓存对某些操作更有效的表示形式,但转换回基本表示形式的开销很大,则会发生这种情况。

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

它建议使用某种复杂的模式来完全解决该问题:拥有受保护的副本构造函数,受保护的纯虚拟DoClone以及带有断言的公共Clone,该断言将告诉您(其他)派生类是否无法正确实现DoClone。(Clone方法对多态对象进行适当的深层复制。)

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


3
您也可以在显式的基础上标记复制构造函数 ”完全没有帮助。
curiousguy 2012年

6

1.切片问题的定义

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

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(后代类对象)指向的动态变量的数据成员或成员函数。另外,如果需要使用功能,则该功能必须是虚拟功能。


7
我理解“切片”部分,但我不理解“问题”。dog不属于类Petbreed数据成员)的某些状态没有复制到变量中pet怎么办?该代码仅对Pet数据成员感兴趣-显然。如果不需要,切片绝对是一个“问题”,但我在这里看不到。
curiousguy 2012年

4
((Dog *)ptrP)”我建议使用static_cast<Dog*>(ptrP)
curiousguy 2012年

我建议指出,通过“ ptrP”删除时,您将使字符串“品种”最终泄漏内存而没有虚拟析构函数(不会调用“字符串”的析构函数)...为什么出现问题?解决方法主要是适当的类设计。在这种情况下的问题是,在继承时写下构造函数以控制可见性非常乏味且容易被遗忘。您的代码不会在危险区域附近到达任何地方,因为这里没有涉及甚至提到多态性(切片会截断对象,但不会使程序崩溃,在这里)。
杜德(Dude)2012年

24
-1这完全无法解释实际问题。C ++具有值语义,而不具有Java之类的引用语义,因此这完全是可以预期的。而且“修复”确实是真正可怕的 C ++代码的一个示例。通过动态分配来解决这种不存在的问题,例如这种类型的切片,是错误代码,内存泄漏和性能糟糕的秘诀。请注意,在某些情况下切片效果不好,但是此答案无法指出。提示:如果通过引用进行分配,麻烦就开始了。
fgp

您甚至不理解尝试访问未定义类型(Dog::breed)的成员绝不是与SLICING相关的错误?
克罗尔

4

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

如果我将子类对象作为参数传递给采用超类类型参数的方法,则我当然应该意识到这一点并在内部知道,所调用的方法将仅与超类(aka基类)对象一起使用。

在我看来,只有不合理的期望,即在请求基类的情况下提供子类会以某种方式导致特定于子类的结果,从而导致切片成为问题。它要么使用该方法的设计不佳,要么使用子类的实现不佳。我猜想这通常是为了权宜之计或性能提升而牺牲好的OOP设计的结果。


3
但是请记住,Minok,您没有传递该对象的引用。您正在传递该对象的新副本,但使用基类在该过程中复制它。
Arafangion 2010年

受保护的副本/赋值在基类上,此问题已解决。
杜德(Dude)2012年

1
你是对的。优良作法是使用抽象基类或限制对复制/赋值的访问。但是,一旦发现就不那么容易了,而且容易忘记照顾它。如果您在没有访问冲突的情况下离开,则使用切片的*调用虚拟方法可能会使神秘的事情发生。
杜德(Dude)2012年

1
我从大学的C ++编程课程中回想起,对于我们创建的每个类都有一些最佳的常规做法,我们需要编写默认构造函数,复制构造函数和赋值运算符以及一个析构函数。这样,您可以在编写类时确保复制构造等以所需的方式发生,而不是稍后出现某些奇怪的行为。
Minok

3

好的,在阅读了很多解释对象切片的文章之后,我将尝试一下,但不会出现问题。

可能导致内存损坏的恶劣情况如下:

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

3

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

说明: 考虑以下类声明:

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

由于基类复制函数对派生类一无所知,因此仅复制了派生类的基础部分。这通常称为切片。


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

4
您介意提供一些其他详细信息吗?您的答案与已发布的答案有何不同?
亚历克西斯·皮金

2
我想更多的解释还不错。
Looper

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

-1

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

这是一个例子:

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

投反对票,因为这不是一个好例子。如果不使用d而不是将d复制到b,而是使用指针,在这种情况下d和e仍然存在,但Base没有那些成员,则该方法也不起作用。您的示例仅显示您无法访问该班级没有的成员。
Stefan Fabian

-2

我刚遇到切片问题,并立即降落在这里。因此,我在此加两分钱。

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


假设我们有一些可以调度动作的东西。例如控制中心用户界面。
此UI需要获取当前可以调度的事物的列表。因此,我们定义了一个包含调度信息的类。叫它Action。因此,an Action具有一些成员变量。为简单起见,我们只有2个,分别是a std::string name和a std::function<void()> f。然后它具有一个void activate()仅执行f成员的对象。

因此,UI获得了std::vector<Action>供应。想象一些函数,例如:

void push_back(Action toAdd);

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

所以这家伙就是从Action自己的口味中衍生出来的。
他将自己酿制的课程的一个实例传递给,push_back但是程序变得混乱了。

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

来自实例的额外信息已经丢失,并且f现在容易出现不确定的行为。


我希望这个例子能为那些在谈论As和Bs以某种方式派生时无法真正想象事物的人们带来启发。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.