有人在 IRC 中将其称为切片问题。
18 回答
“切片”是将派生类的对象分配给基类的实例,从而丢失部分信息 - 其中一些信息被“切片”掉了。
例如,
class A {
int foo;
};
class B : public A {
int bar;
};
所以一个类型的对象B
有两个数据成员,foo
和bar
。
那么如果你要写这个:
B b;
A a = b;
然后b
about memberbar
中的信息丢失在a
.
这里的大多数答案都无法解释切片的实际问题是什么。他们只解释了切片的良性案例,而不是危险的案例。假设,与其他答案一样,您正在处理两个类A
和B
,其中B
(公开)派生自A
。
在这种情况下,C++ 允许您传递B
to A
的赋值运算符(以及复制构造函数)的实例。这是有效的,因为可以将 的实例B
转换为const A&
,这是赋值运算符和复制构造函数所期望的参数。
良性案例
B b;
A a = b;
那里没有什么不好的事情发生 - 你要求一个实例,A
它是 的副本B
,而这正是你得到的。当然,a
不会包含一些b
's 成员,但是应该怎么做呢?A
毕竟是a,不是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
'sassign()
可能想要使用A
'sassign()
来复制A
's, 成员。
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
'soperator=
协变地覆盖了返回类型,因为它知道它正在返回一个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
-object (类的所有特殊行为B
都丢失了)。
这些都是很好的答案。我想在按值与按引用传递对象时添加一个执行示例:
#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++ 切片”的第三场比赛给了我这篇维基百科文章http://en.wikipedia.org/wiki/Object_slicing和这个(激烈,但前几篇文章定义了问题):http ://bytes.com/论坛/thread163565.html
因此,当您将子类的对象分配给超类时。超类对子类中的附加信息一无所知,并且没有空间存储它,因此附加信息被“切掉”。
如果这些链接没有为“好的答案”提供足够的信息,请编辑您的问题,让我们知道您还在寻找什么。
切片问题很严重,因为它会导致内存损坏,并且很难保证程序不会受到影响。为了在语言之外设计它,支持继承的类应该只能通过引用访问(而不是通过值)。D 编程语言具有此属性。
考虑 A 类和从 A 派生的 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
}
当派生类对象分配给基类对象时,会发生对象切片,派生类对象的附加属性被切掉以形成基类对象。
当数据成员被切片时发生对象切片时,我看到所有提到的答案。这里我举一个例子,这些方法没有被覆盖:
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 B
a1.Say(); // I am A
a2.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
那么......为什么丢失派生信息不好?...因为派生类的作者可能已经更改了表示,因此切掉额外信息会更改对象所表示的值。如果派生类用于缓存对某些操作更有效的表示,但转换回基本表示的成本很高,则可能会发生这种情况。
还认为有人还应该提到你应该做些什么来避免切片......获取一份 C++ 编码标准、101 条规则指南和最佳实践的副本。处理切片是#54。
它提出了一个稍微复杂的模式来完全处理这个问题:有一个受保护的复制构造函数、一个受保护的纯虚拟 DoClone 和一个带有断言的公共克隆,它会告诉你(进一步的)派生类是否无法正确实现 DoClone。(Clone 方法对多态对象进行适当的深拷贝。)
您还可以在基显式上标记复制构造函数,如果需要,它允许显式切片。
C++ 中的切片问题源于其对象的值语义,这主要是由于与 C 结构的兼容性。您需要使用显式引用或指针语法来实现在大多数其他处理对象的语言中发现的“正常”对象行为,即对象总是通过引用传递。
简短的回答是,您通过按 value将派生对象分配给基础对象来对对象进行切片,即剩余对象只是派生对象的一部分。为了保留值语义,切片是一种合理的行为,并且使用相对较少,这在大多数其他语言中是不存在的。有些人认为它是 C++ 的一个特性,而许多人认为它是 C++ 的怪癖/错误之一。
一、切片问题的定义
如果 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(后代类对象)指向的动态变量的任何数据成员或成员函数都不会丢失。另外,如果需要使用函数,函数必须是虚函数。
在我看来,除了您自己的类和程序的架构/设计不佳之外,切片并不是什么大问题。
如果我将一个子类对象作为参数传递给一个方法,该方法接受一个超类类型的参数,我当然应该意识到这一点并且在内部知道,被调用的方法将只与超类(又名基类)对象一起使用。
在我看来,只有不合理的期望是,在请求基类的地方提供子类,会以某种方式导致子类特定的结果,会导致切片成为问题。它要么是使用该方法的糟糕设计,要么是糟糕的子类实现。我猜它通常是牺牲良好的 OOP 设计以换取权宜之计或性能提升的结果。
好的,在阅读了许多解释对象切片的帖子后,我会尝试一下,但不知道它是如何成为问题的。
可能导致内存损坏的恶性场景如下:
- 类在多态基类上提供(偶然地,可能是编译器生成的)赋值。
- 客户端复制和切片派生类的实例。
- 客户端调用访问切片状态的虚拟成员函数。
切片意味着当子类的对象通过值或从期望基类对象的函数传递或返回时,子类添加的数据将被丢弃。
说明: 考虑以下类声明:
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; // b.c == '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
}
当派生类对象分配给基类对象时,派生类对象的所有成员都被复制到基类对象中,但基类中不存在的成员除外。这些成员被编译器切掉。这称为对象切片。
这是一个例子:
#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
。所以 anAction
有一些成员变量。为简单起见,我们只有 2,即 astd::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 以某种方式派生时无法真正想象事物的人带来启发。