有人在IRC中提到它作为切片问题.
"切片"是指将派生类的对象分配给基类的实例,从而丢失部分信息 - 其中一些信息被"切片"掉.
例如,
class A { int foo; }; class B : public A { int bar; };
所以类型的对象B
有两个数据成员,foo
和bar
.
然后,如果你写这个:
B b; A a = b;
然后b
关于成员的信息bar
丢失了a
.
这里的大多数答案都无法解释切片的实际问题是什么.他们只解释切片的良性情况,而不是危险的情况.假设,像其他的答案,你正在处理两班A
和B
,其中B
导出(公开)的A
.
在这种情况下,C++,您可以通过一个实例B
来 A
的赋值运算符(同时也拷贝构造函数).这是有效的,因为一个实例B
可以转换为a 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
(B
从中继承的块A
)和一些b2
(仅B
包含的块)组成.哎哟!
发生了什么?好吧,默认情况下,C++不会将赋值运算符视为virtual
.因此,该行将a_ref = b1
调用赋值运算符A
,而不是B
.这是因为对于非虚函数,声明的类型(即A&
)确定调用哪个函数,而不是实际类型(B
自a_ref
引用实例以来B
).现在,A
的赋值运算符显然只知道声明的成员A
,因此它只会复制那些成员,使成员的添加B
不变.
仅分配给对象的某些部分通常没什么意义,但遗憾的是,C++没有提供禁止内容的内置方法.但是,您可以自己动手.第一步是使赋值运算符成为虚拟.这将保证它始终是被调用的实际类型的赋值运算符,而不是声明的类型.第二步是用于dynamic_cast
验证分配的对象是否具有兼容类型.第三步是在(受保护!)成员中进行实际任务assign()
,因为B
他们assign()
可能想要使用A
's 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(&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=
共同覆盖返回类型,因为它知道它正在返回一个实例B
.
如果您有基类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
都会丢失).
这些都是很好的答案.我想在按值和引用传递对象时添加一个执行示例:
#includeusing 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'
谷歌"C++切片"的第三场比赛给了我这篇维基百科文章http://en.wikipedia.org/wiki/Object_slicing和这个(加热,但前几个帖子定义了问题):http://bytes.com/论坛/ thread163565.html
所以当你将一个子类的对象分配给超类时.超类对子类中的附加信息一无所知,也没有足够的空间来存储它,因此附加信息会被"切掉".
如果这些链接没有为"正确答案"提供足够的信息,请编辑您的问题,让我们知道您还在寻找什么.
切片问题很严重,因为它可能导致内存损坏,并且很难保证程序不会受到影响.要使用该语言进行设计,支持继承的类应该只能通过引用访问(而不是通过值).D编程语言具有此属性.
考虑A类,从B派生B类.如果A部分有一个指针p,则会发生内存损坏,而B实例将p指向B的附加数据.然后,当附加数据被切掉时,p指向垃圾.
在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 }
当派生类对象被分配给基类对象时,会发生对象切片,派生类对象的其他属性将被切除以形成基类对象.
C++中的切片问题源于其对象的值语义,这主要是由于与C结构的兼容性.您需要使用显式引用或指针语法来实现在执行对象的大多数其他语言中找到的"正常"对象行为,即,对象始终通过引用传递.
简短的答案是通过按值将派生对象分配给基础对象来切片对象,即剩余对象只是派生对象的一部分.为了保留价值语义,切片是一种合理的行为,并且具有相对罕见的用途,这在大多数其他语言中是不存在的.有些人认为它是C++的一个特性,而许多人认为它是C++的怪癖/错误特征之一.
那么......为什么丢失衍生信息不好?...因为派生类的作者可能已经更改了表示,因此切掉额外信息会更改对象所表示的值.如果派生类用于缓存对某些操作更有效但对转换回基本表示而言代价高的表示,则会发生这种情况.
还想到有人还应该提到你应该做些什么以避免切片......获取C++编码标准,101规则指南和最佳实践的副本.处理切片是#54.
它提出了一个有点复杂的模式来完全处理这个问题:拥有一个受保护的复制构造函数,一个受保护的纯虚拟DoClone,以及一个带有断言的公共克隆,它将告诉您(某个)派生类是否未能正确实现DoClone.(克隆方法对多态对象进行了适当的深层复制.)
您还可以在基本显式标记复制构造函数,如果需要,可以显式切片.
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(后代类对象)指向的动态变量的数据成员或成员函数都不会丢失.另外,如果需要使用函数,该函数必须是虚函数.