什么是对象切片?

c++什么是对象切片以及何时发生?

239287 次浏览

第三场比赛在谷歌“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的对象有两个数据成员,foobar

那么如果你要写这个:

B b;
A a = b;

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

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

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

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

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

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

它建议了一个稍微复杂的模式来完全处理这个问题:有一个受保护的复制构造函数,一个受保护的纯虚拟DoClone,以及一个带有断言的公共克隆,断言将告诉您(进一步的)派生类是否未能正确实现DoClone。

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

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

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

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

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

在我看来,只有不合理的期望,即在请求基类的地方提供一个子类,会以某种方式导致子类特定的结果,才会导致切片成为一个问题。要么是方法使用中的糟糕设计,要么是糟糕的子类实现。我猜这通常是牺牲良好的OOP设计以换取权宜之计或性能提升的结果。

1.切片问题的定义

如果D是基类B的派生类,那么您可以将Derive类型的对象分配给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" offreturn 0;}

这里的大多数答案都无法解释切片的实际问题是什么。它们只解释了切片的良性情况,而不是危险的情况。假设,像其他答案一样,你正在处理两个类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的副本。但是,唉,它是没有!如果你检查它,你会发现b2是一个弗兰肯斯坦式的生物,由b1的一些块(BA继承的块)和b2的一些块(只有B包含的块)制成。哎哟!

发生了什么?C++默认情况下不会将赋值操作符视为virtual。因此,第1行将调用A的赋值操作符,而不是B的赋值操作符。这是因为,对于非虚函数,a_ref = b11(正式:a_ref = b12)类型(即A&)决定调用哪个函数,而不是a_ref = b13(正式:a_ref = b14)类型(即B,因为a_ref引用了B的实例)。现在,A的赋值操作符显然只知道A中声明的成员,所以它只会复制那些,而B中添加的成员保持不变。

一个解决方案

只对一个对象的部分赋值通常没有什么意义,然而不幸的是,C++没有提供禁止这种做法的内置方法。但是,你可以自己滚动。第一步是制作赋值操作符虚拟。这将保证调用的总是实际类型的赋值操作符,而不是宣布类型的。第二步是使用dynamic_cast来验证分配的对象具有兼容的类型。第三步是在(protected!)成员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);elsethrow 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的实例。

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

说明:考虑以下类声明:

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

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

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

#include <iostream>
using namespace std;
// Base classclass A {public:A() {}A(const A& a) {cout << "'A' copy constructor" << endl;}virtual void run() const { cout << "I am an 'A'" << endl; }};
// Derived classclass 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 referenceI am a 'B'
Call by copy'A' copy constructorI 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。所以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以某种方式派生时无法真正想象事情的人带来光明。

在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 Ba1.Say(); // I am Aa2.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

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