什么是对象切片?
有人在IRC中提到过,但谷歌没有一个好的答案。
“切片”是将派生类的对象分配给基类的一个实例的地方,从而丢失了部分信息,其中一些信息被“切分”了。
例如,
class A { int foo; }; class B : public A { int bar; };
所以B
types的对象有两个数据成员, foo
和bar
。
那么如果你写这个:
B b; A a = b;
然后在关于成员bar
b
的信息丢失在a
。
这里的大多数答案都无法解释切片的实际问题。 他们只解释切片的良性情况,而不是奸诈的情况。 假设,像其他答案一样,你正在处理两个A
和B
类,其中B
从A
公开地派生出来。
在这种情况下,C ++可以让你将B
一个实例传递给A
的赋值操作符(也是拷贝构造函数)。 这是可行的,因为B
一个实例可以被转换为一个const A&
,这就是赋值运算符和复制构造函数期望它们的参数。
良性的情况
B b; A a = b;
没有什么不好的事情发生在那里 – 你要求一个A
的实例,它是B
一个副本,而这正是你得到的。 当然, a
不会包含一些b
的成员,但它应该如何? 毕竟,这不是一个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
从B
inheritance的块)和b2
( B
只包含的块)组成。 哎哟!
发生了什么? 那么,C ++默认不会将赋值运算符视为virtual
。 因此, a_ref = b1
行将调用A
的赋值运算符,而不是B
的赋值运算符。 这是因为对于非虚函数, 声明的types(即A&
)确定调用哪个函数,而不是实际的types(因为a_ref
引用了B
一个实例,这将是B
)。 现在, A
的赋值操作符显然只知道A
声明的成员,所以它只会复制那些成员,而使B
添加的成员不变。
一个办法
只分配给一个对象的部分通常没有什么意义,但C + +遗憾的是没有内置的方法来禁止这个。 不过,你可以推出自己的。 第一步是使赋值运算符虚拟 。 这将保证它始终是被调用的实际types的赋值运算符,而不是被声明的types。 第二步是使用dynamic_cast
来validation分配的对象是否具有兼容的types。 第三步是在一个(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
的operator=
协变地覆盖返回types,因为它知道它正在返回B
的一个实例。
如果您有基类A和派生类B,则可以执行以下操作。
void wantAnA(A myA) { // work with myA } B derived; // work with the object "derived" wantAnA(derived);
现在,方法wantAnA需要一个派生的副本。 但是, 派生的对象不能被完全复制,因为类B可以创build不在其基类A中的其他成员variables。
因此,要调用wantAnA ,编译器将“切断”派生类的所有其他成员。 结果可能是你不想创build的对象,因为
- 它可能不完整,
- 它performance得像一个A对象(B类的所有特殊行为都会丢失)。
在谷歌的“C ++切片”的第三场比赛给了我这个维基百科文章http://en.wikipedia.org/wiki/Object_slicing和这个(激烈,但前几个职位定义的问题):; http : //bytes.com/论坛/ thread163565.html
所以当你将一个子类的对象分配给超类的时候。 超类对子类中的附加信息一无所知,并且没有空间存储它,所以附加信息被“切断”。
如果这些链接没有给出足够的信息以获得“良好的答案”,请编辑您的问题,让我们知道您要找的更多。
切片问题是严重的,因为它可能导致内存损坏,并且很难保证程序不受其影响。 要devise出语言,支持inheritance的类应该只能通过引用来访问(而不能通过值来访问)。 D编程语言有这个属性。
考虑类A和类B从A派生。如果A部分具有指针p,而B实例指向p的B附加数据,则会发生内存损坏。 然后,当额外的数据被切掉时,p指向垃圾。
这些都是很好的答案。 我只想添加一个执行的例子,通过值与引用传递对象:
#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'
1.切片问题的定义
如果D是基类B的派生类,则可以将派生types的对象分配给types为Base的variables(或参数)。
例
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
尽pipe允许上述分配,但分配给variables宠物的值将丢失其繁殖字段。 这被称为切片问题 。
2.如何修复切片问题
为了解决这个问题,我们使用指向dynamicvariables的指针。
例
Pet *ptrP; Dog *ptrD; ptrD = new Dog; ptrD->name = "Tommy"; ptrD->breed = "Kangal Dog"; ptrP = ptrD; cout << ((Dog *)ptrP)->breed;
在这种情况下,ptrD(后代类对象)指向的dynamicvariables的数据成员或成员函数都不会丢失。 另外,如果你需要使用函数,函数必须是虚函数。
C ++中的切片问题来自其对象的值语义,这主要归因于与C结构的兼容性。 您需要使用显式引用或指针语法来实现在大多数其他语言中发现的“正常”对象行为,即执行对象的对象,即对象总是通过引用传递。
简短的答案是,你通过赋值派生的对象给一个基础对象分值对象,即剩余的对象只是派生对象的一部分。 为了保持价值语义,切片是一种合理的行为,并且有其相对罕见的用途,这在大多数其他语言中是不存在的。 有些人认为它是C ++的一个特性,而许多人认为它是C ++的怪癖/错误特征之一。
所以…为什么失去派生的信息不好? …因为派生类的作者可能已经改变了表示,从而切掉额外的信息改变了由对象表示的值。 如果派生类用于caching对某些操作更高效的表示,但转换回基表示forms则代价较高,则可能发生这种情况。
也有人应该也提到你应该做些什么来避免切片…获得一份C ++编码标准,101条规则指导和最佳实践。 处理切片是#54。
它提出了一个有点复杂的模式来完全处理这个问题:有一个受保护的拷贝构造函数,一个受保护的纯虚拟DoClone和一个带有断言的公共克隆,它会告诉你一个(进一步)派生类是否无法正确实现DoClone。 (“克隆”方法可以对多态对象进行适当的深层复制。)
如果需要的话,你也可以在基本显式的基础上标记拷贝构造函数,允许显式切片。
在我看来,切片并不是一个问题,除非你自己的课程和程序devise不当。
如果我将一个子类对象作为parameter passing给一个方法,该方法需要一个超types的参数,我当然应该知道这一点,并且知道内部方法,被调用的方法将只与超类(又名基类)对象一起工作。
在我看来,只有不合理的期望,即提供一个需要基类的子类,会以某种方式导致子类特定的结果,这会导致分片成为一个问题。 它要么使用该方法的devise较差,要么使用较差的子类实现。 我猜它通常是牺牲良好的面向对象devise的结果,以利于方便或提高性能。
好的,在阅读了解释对象切片的许多post之后,我会尝试一下,但不会如何变得有问题。
可能导致内存损坏的恶意情况如下:
- 类为多态基类提供了(意外的,可能是由编译器生成的)赋值。
- 客户端复制和切片派生类的实例。
- 客户端调用访问切片状态的虚拟成员函数。
在这里find类似的答案: 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; }
由于基类复制函数并不知道派生的任何内容,只有派生的基本部分被复制。 这通常被称为切片。
class A { int x; }; class B { B( ) : x(1), c('a') { } int x; char c; }; int main( ) { A a; B b; a = b; // bc == 'a' is "sliced" off return 0; }
当一个派生类对象被分配给一个基类对象时,派生类对象的附加属性被从基类对象中分离出来(放弃)。
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 }
当派生类Object被分配给基类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<<da<<"\n"; cout<<db<<"\n"; cout<<dc<<"\n"; cout<<dd<<"\n"; cout<<de<<"\n"; Base b = d; cout<<ba<<"\n"; cout<<bb<<"\n"; cout<<bc<<"\n"; cout<<bd<<"\n"; cout<<be<<"\n"; return 0; }
它会产生:
[Error] 'class Base' has no member named 'd' [Error] 'class Base' has no member named 'e'
我只是碰到了切片问题,并立即降落在这里。 所以,让我加我两分钱。
我们来看一个“生产代码”(或者类似的东西)的例子:
假设我们有一些派遣行动的东西。 以控制中心UI为例。
这个用户界面需要获取当前能够分派的东西的清单。 所以我们定义一个包含调度信息的类。 我们称之为Action
。 所以一个Action
有一些成员variables。 为了简单起见,我们只有2个,是一个std::string name
和一个std::function<void()> f
。 然后它有一个void activate()
,它只是执行f
成员。
所以UI得到一个std::vector<Action>
提供。 想象一下这样的function:
void push_back(Action toAdd);
现在我们已经从用户界面的angular度确定了它的外观。 到目前为止没有问题。 但是其他一些在这个项目上工作的人突然决定,在Action
对象中需要更多的信息。 为什么有理由。 这也可以用lambda捕获来解决。 这个例子不是从代码中取1-1。
所以这个家伙从Action
衍生出来,join他自己的味道。
他把自己酿造的一个实例传递给了push_back
但是随后这个程序就不了了之。
所以发生了什么事?
正如你可能已经猜到的那样:物体被切割了。
来自实例的额外信息已经丢失,并且f
现在倾向于未定义的行为。
我希望这个例子能够为那些在谈论A
和B
以某种方式推导出来的那些不能想象事物的人带来光明。