提问人:Frankomania 提问时间:11/8/2008 最后编辑:2240Frankomania 更新时间:4/5/2022 访问量:257392
什么是对象切片?
What is object slicing?
答:
谷歌中“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
foo
bar
然后,如果你要写这个:
B b;
A a = b;
然后,关于成员的信息在 中丢失。b
bar
a
评论
A a = b;
a
A
B::foo
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
void wantAnA(A myA)
{
// work with myA
}
B derived;
// work with the object "derived"
wantAnA(derived);
现在,该方法需要 的副本。但是,该对象无法完全复制,因为该类可能会发明其他成员变量,这些变量不在其基类中。wantAnA
derived
derived
B
A
因此,要调用 ,编译器将“切除”派生类的所有其他成员。结果可能是您不想创建的对象,因为wantAnA
- 它可能不完整,
- 它的行为类似于一个对象(类的所有特殊行为都丢失了)。
A
B
评论
wantAnA
A
A
A
derived
A
切片问题很严重,因为它会导致内存损坏,并且很难保证程序不会受到影响。为了从语言中设计它,支持继承的类应该只能通过引用(而不是通过值)来访问。D 编程语言具有此属性。
考虑类 A 和派生自 A 的类 B。如果 A 部件具有指针 p,并且 B 实例将 p 指向 B 的附加数据,则可能会发生内存损坏。然后,当其他数据被切掉时,p 指向垃圾。
评论
Derived
Base
所以。。。为什么丢失派生信息是不好的?...因为派生类的作者可能更改了表示形式,因此切除额外信息会更改对象所表示的值。如果派生类用于缓存对某些操作更有效的表示形式,但转换回基本表示形式的成本很高,则可能会发生这种情况。
还认为有人也应该提到你应该做些什么来避免切片...... 获取 C++ 编码标准、101 条规则指南和最佳实践的副本。处理切片是#54。
它提出了一个稍微复杂的模式来完全处理这个问题:有一个受保护的复制构造函数,一个受保护的纯虚拟DoClone,以及一个带有断言的公共克隆,该断言将告诉你(进一步的)派生类是否未能正确实现DoClone。(Clone 方法可对多态对象进行适当的深度复制。
您还可以在基本显式上标记复制构造函数,这允许在需要时进行显式切片。
评论
C++ 中的切片问题源于其对象的值语义,这主要是由于与 C 结构的兼容性。您需要使用显式引用或指针语法来实现大多数其他执行对象的语言中发现的“正常”对象行为,即对象始终通过引用传递。
简短的回答是,通过按值将派生对象分配给基对象来对对象进行切片,即剩余对象只是派生对象的一部分。为了保留值语义,切片是一种合理的行为,并且有其相对罕见的用途,这在大多数其他语言中是不存在的。有些人认为它是 C++ 的一个特性,而许多人认为它是 C++ 的怪癖/错误特性之一。
评论
struct
Base
sizeof(Base)
在我看来,切片并不是一个大问题,除非你自己的类和程序架构/设计不佳。
如果我将子类对象作为参数传递给一个方法,该方法采用超类类型的参数,我当然应该意识到这一点并知道内部,被调用的方法将仅与超类(又名基类)对象一起使用。
在我看来,只有一种不合理的期望,即提供请求基类的子类会以某种方式导致子类特定的结果,这会导致切片成为一个问题。它要么在使用方法时设计不佳,要么子类实现不佳。我猜这通常是为了权宜之计或性能提升而牺牲了良好的 OOP 设计的结果。
评论
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 将丢失其 breed 字段。这称为切片问题。
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)
好的,在阅读了许多解释对象切片的帖子后,我会尝试一下,但不是它如何成为问题。
可能导致内存损坏的恶性方案如下:
- 类在多态基类上提供(意外的,可能是编译器生成的)赋值。
- 客户端复制派生类的实例并对其进行切片。
- 客户端调用访问切片状态的虚拟成员函数。
评论
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;
}
评论
这里的大多数答案都无法解释切片的实际问题是什么。他们只解释了切片的良性情况,而不是危险的情况。假设,像其他答案一样,你正在处理两个类和 ,其中(公开)派生自 。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++没有提供内置的方法来禁止这样做。但是,您可以自己滚动。第一步是使赋值运算符虚拟化。这将保证调用的始终是实际类型的赋值运算符,而不是声明类型的赋值运算符。第二步是用于验证分配的对象是否具有兼容类型。第三步是在 (protected!) 成员中执行实际赋值,因为 可能想要使用 's 来复制 's, 成员。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
}
};
请注意,为了方便起见,'s 会协变覆盖返回类型,因为它知道它返回的是 的实例。B
operator=
B
评论
derived
base
切片意味着当子类的对象按值传递或返回时,或从需要基类对象的函数传递或返回时,将丢弃子类添加的数据。
解释:请考虑以下类声明:
class baseclass
{
...
baseclass & operator =(const baseclass&);
baseclass(const baseclass&);
}
void function( )
{
baseclass obj1=m;
obj1=m;
}
由于基类复制函数对派生函数一无所知,因此只复制派生的基部分。这通常称为切片。
这些都是很好的答案。我只想添加一个按值传递对象与引用传递对象的执行示例:
#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'
评论
当派生类对象分配给基类对象时,派生类对象的其他属性将从基类对象中切除(丢弃)。
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 需要获取当前能够调度的事物列表。因此,我们定义了一个包含调度信息的类。我们称之为 .所以 an 有一些成员变量。为简单起见,我们只有 2,分别是 a 和 a .然后它有一个 which 只是执行成员。Action
Action
std::string name
std::function<void()> f
void activate()
f
因此,UI得到了一个。想象一下一些函数,例如:std::vector<Action>
void push_back(Action toAdd);
现在,我们已经确定了它从 UI 的角度来看是什么样子的。到目前为止没有问题。但是从事这个项目的其他一些人突然决定,在对象中有一些专门的操作需要更多信息。出于什么原因。这也可以通过 lambda 捕获来解决。此示例不是从代码中 1-1 获取的。Action
所以这家伙从中衍生出来,添加他自己的味道。
他将他自制的课程的一个实例传递给了 ,但随后程序就失控了。Action
push_back
那么发生了什么?
您可能已经猜到了:对象已被切片。
来自实例的额外信息已丢失,现在容易出现未定义的行为。f
我希望这个例子能为那些在谈论 s 和 s 以某种方式派生时无法真正想象事物的人带来启示。A
B
在 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
评论
But from polymorphism viewpoint we don’t expect a2, which is assigned by b, to not be overridden. Basically, a2 only saves A-class part of b and that is object slicing in C++.
这听起来不对,也不清楚。的静态类型是 这就是被调用的原因 - 我认为它与对象切片无关。a2
A
A::Say()
评论