Answers:
在“切片”中,您将派生类的对象分配给基类的实例,从而丢失了部分信息-其中一些信息被“切片”了。
例如,
class A {
int foo;
};
class B : public A {
int bar;
};
因此,类型的对象B
具有两个数据成员,foo
和bar
。
然后,如果您要编写此代码:
B b;
A a = b;
然后有关中的b
成员信息bar
会丢失a
。
B b1; B b2; A& b2_ref = b2; b2 = b1
。您可能会认为您已复制b1
到b2
,但是还没有!你复制一个部分的b1
到b2
(部分b1
是B
从继承A
),并留下的其他部分b2
保持不变。b2
现在是一种弗兰肯斯坦的生物,由少量的b1
跟随着的大块的组成b2
。啊! 拒绝投票是因为我认为答案非常有误导性。
B b1; B b2; A& b2_ref = b2; b2_ref = b1
“ 如果您 ...派生自具有非虚拟赋值运算符的类,则会发生真正的问题。是A
即使用于推导?它没有虚拟功能。如果从类型派生,则必须处理其成员函数可以被调用的事实!
这里的大多数答案都无法解释切片的实际问题是什么。他们只说明切片的良性案例,而不说明危险的切片。假设,像其他的答案,你正在处理两班A
和B
,其中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
(B
继承自的块A
)和b2
(仅B
包含的块)一些块组成的。哎哟!
发生了什么?好吧,默认情况下,C ++不会将赋值运算符视为virtual
。因此,该行将a_ref = b1
调用的赋值运算符A
,而不是B
。这是因为,对于非虚函数,声明的(正式:静态)类型(即A&
)确定调用哪个函数,而不是实际(正式:动态)类型(即B
,因为a_ref
引用的实例B
) 。现在,A
的赋值运算符显然只知道在中声明的成员A
,因此它将仅复制那些成员,而添加的成员B
保持不变。
仅分配给对象的一部分通常没有什么意义,但是不幸的是,C ++没有提供内置的方式来禁止这种情况。但是,您可以自己滚动。第一步是使赋值运算符为virtual。这样可以保证始终调用的是实际类型的赋值运算符,而不是声明的类型。第二步dynamic_cast
用于验证分配的对象是否具有兼容类型。第三步是做一个(受保护的!)成员的实际分配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=
协变量会覆盖返回类型,因为它知道它正在返回的实例B
。
derived
可以给base
期望值的代码赋予任何值,或者可以将任何派生的引用用作基础引用。我想看到一种带有类型系统的语言,可以分别解决这两个概念。在很多情况下,派生的引用应该可以代替基本引用,但是派生的实例不可以替换为基本引用。在许多情况下,实例应该是可转换的,但引用不能替代。
如果您有基类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
丢失)。wantAnA
(顾名思义!)想要一个A
,那么它就会得到。而且的实例A
将表现为A
。这有多令人惊讶?
derived
对类型的自动转换A
。隐式强制转换始终是C ++中意外行为的来源,因为从本地查看代码通常很难理解强制转换是发生的。
这些都是很好的答案。我只想在按值与按引用传递对象时添加一个执行示例:
#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'
谷歌中针对“ C ++切片”的第三次匹配使我在Wikipedia上看到了这篇文章,网址为http://en.wikipedia.org/wiki/Object_slicing,并且这篇文章很热烈,但前几篇文章定义了问题:Forum / thread163565.html
这样就可以将子类的对象分配给超类了。超类对子类中的附加信息一无所知,并且没有足够的空间来存储它,因此附加信息将被“分割”。
如果这些链接没有提供足够的信息来提供“良好答案”,请编辑您的问题,以使我们知道您还在寻找什么。
切片问题很严重,因为它可能导致内存损坏,并且很难保证程序不会遭受该问题的困扰。为了用语言进行设计,支持继承的类应该只能通过引用(而不是通过值)进行访问。D编程语言具有此属性。
考虑从A派生的类A和类B。如果A部分具有指针p,并且B实例将p指向B的其他数据,则可能发生内存损坏。然后,当其他数据被分割时,p指向垃圾。
Derived
可以隐式转换为Base
。)这显然与“开放式封闭原则”背道而驰,并且负担沉重。
C ++中的切片问题源于其对象的值语义,而该语义大部分仍归因于与C结构的兼容性。您需要使用显式引用或指针语法来实现在大多数其他执行对象的语言中发现的“正常”对象行为,即对象总是通过引用传递。
简短的答案是通过按值将派生对象分配给基础对象来对对象进行切片,即剩余对象仅是派生对象的一部分。为了保留值语义,切片是一种合理的行为,并且具有相对较少的用途,这在大多数其他语言中并不存在。有些人认为它是C ++的功能,而许多人则认为它是C ++的怪癖/错误特征之一。
struct
意义的无关。
Base
必须精确地sizeof(Base)
占用内存中的字节,并可能对齐,这也许就是为什么“赋值”(on-stack-copy) )将不会复制派生的类成员,它们的偏移量不在sizeof范围内。为避免“丢失数据”,只需使用指针,就像其他任何指针一样,因为指针的内存在位置和大小上都是固定的,而堆栈却非常易变
那么……为什么丢失派生信息不好呢?...因为派生类的作者可能已更改了表示形式,因此切下多余的信息会更改对象表示的值。如果派生类用于缓存对某些操作更有效的表示形式,但转换回基本表示形式的开销很大,则会发生这种情况。
还认为有人也应该提到避免切片的方法...获取C ++编码标准,101条规则指南和最佳实践的副本。处理切片是#54。
它建议使用某种复杂的模式来完全解决该问题:拥有受保护的副本构造函数,受保护的纯虚拟DoClone以及带有断言的公共Clone,该断言将告诉您(其他)派生类是否无法正确实现DoClone。(Clone方法对多态对象进行适当的深层复制。)
您还可以在显式基础上标记副本构造函数,如果需要,可以允许显式切片。
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(后代类对象)指向的动态变量的数据成员或成员函数。另外,如果需要使用功能,则该功能必须是虚拟功能。
dog
不属于类Pet
(breed
数据成员)的某些状态没有复制到变量中pet
怎么办?该代码仅对Pet
数据成员感兴趣-显然。如果不需要,切片绝对是一个“问题”,但我在这里看不到。
((Dog *)ptrP)
”我建议使用static_cast<Dog*>(ptrP)
Dog::breed
)的成员绝不是与SLICING相关的错误?
在我看来,除了您自己的类和程序的架构/设计不佳之外,切片并不是什么大问题。
如果我将子类对象作为参数传递给采用超类类型参数的方法,则我当然应该意识到这一点并在内部知道,所调用的方法将仅与超类(aka基类)对象一起使用。
在我看来,只有不合理的期望,即在请求基类的情况下提供子类会以某种方式导致特定于子类的结果,从而导致切片成为问题。它要么使用该方法的设计不佳,要么使用子类的实现不佳。我猜想这通常是为了权宜之计或性能提升而牺牲好的OOP设计的结果。
当派生类对象分配给基类对象时,派生类对象的其他属性会从基类对象中切下(丢弃)。
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需要获取当前可以调度的事物的列表。因此,我们定义了一个包含调度信息的类。叫它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
现在容易出现不确定的行为。
我希望这个例子能为那些在谈论A
s和B
s以某种方式派生时无法真正想象事物的人们带来启发。