【发布时间】:2021-06-30 16:56:08
【问题描述】:
有人在 IRC 中提到它是切片问题。
【问题讨论】:
标签: c++ inheritance c++-faq object-slicing
有人在 IRC 中提到它是切片问题。
【问题讨论】:
标签: c++ inheritance c++-faq object-slicing
“切片”是将派生类的对象分配给基类的实例,从而丢失部分信息 - 其中一些信息被“切片”掉了。
例如,
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,其中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 的赋值运算符。这是因为,对于非虚函数,declared(正式:static)类型(即A&)决定调用哪个函数,而不是 actual(正式:dynamic)类型(应该是B,因为a_ref 引用了B 的一个实例)。现在,A 的赋值运算符显然只知道A 中声明的成员,所以它只会复制那些,而B 中添加的成员保持不变。
仅分配给对象的一部分通常没有什么意义,但不幸的是,C++ 没有提供禁止这种情况的内置方法。但是,您可以自己滚动。第一步是使赋值运算符虚拟。这将保证调用的总是 actual 类型的赋值运算符,而不是 declared 类型的。第二步是使用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 和这个(激烈,但前几篇文章定义了问题):http://bytes.com/forum/thread163565.html
因此,当您将子类的对象分配给超类时。超类对子类中的附加信息一无所知,并且没有空间存储它,因此附加信息被“切掉”。
如果这些链接没有提供足够的信息来获得“好的答案”,请编辑您的问题,让我们知道您还在寻找什么。
【讨论】:
切片问题很严重,因为它会导致内存损坏,并且很难保证程序不会受到它的影响。为了在语言之外设计它,支持继承的类应该只能通过引用访问(而不是通过值访问)。 D 编程语言具有此属性。
考虑 A 类和从 A 派生的 B 类。如果 A 部分具有指针 p,并且 B 实例将 p 指向 B 的附加数据,则可能发生内存损坏。然后,当额外的数据被切掉时,p 指向垃圾。
【讨论】:
Derived 可以隐式转换为Base。)这显然与开闭原则背道而驰,并且需要大量维护负担。
在 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 结构的兼容性。您需要使用显式引用或指针语法来实现在大多数其他处理对象的语言中发现的“正常”对象行为,即对象总是通过引用传递。
简短的回答是您通过将派生对象分配给基础对象按值对对象进行切片,即剩余对象只是派生对象的一部分。为了保留值语义,切片是一种合理的行为,并且使用相对较少,这在大多数其他语言中是不存在的。有些人认为它是 C++ 的一个特性,而许多人认为它是 C++ 的怪癖/错误之一。
【讨论】:
struct、兼容性或任何随机 OOP 牧师告诉你的其他废话有关。
Base 的堆栈变量必须在内存中准确占用 sizeof(Base) 字节,并且可能对齐,这就是“分配”的原因(on-stack-copy) 不会复制派生类成员,它们的偏移量在 sizeof 之外。为了避免“丢失数据”,只需像其他人一样使用指针,因为指针内存的位置和大小是固定的,而堆栈是非常易变的
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(后代类对象)指向的将丢失。另外,如果需要使用函数,函数必须是虚函数。
【讨论】:
Pet(breed 数据成员)的dog 的某些状态没有复制到变量pet 中,这有什么问题吗?代码只对Pet 数据成员感兴趣——显然。如果不需要,切片绝对是一个“问题”,但我在这里看不到。
((Dog *)ptrP)" 我建议使用static_cast<Dog*>(ptrP)
Dog::breed) 绝不是与 SLICING 相关的错误?
在我看来,除了您自己的类和程序的架构/设计不佳时,切片并不是什么大问题。
如果我将一个子类对象作为参数传递给一个方法,该方法接受一个超类类型的参数,我当然应该意识到这一点并且知道在内部,被调用的方法将与超类(又名基类)一起工作仅对象。
在我看来,只有在请求基类的地方提供子类会以某种方式导致子类特定的结果,会导致切片成为问题,这只是一种不合理的期望。它要么是使用方法的糟糕设计,要么是糟糕的子类实现。我猜这通常是为了获得权宜之计或性能提升而牺牲良好的 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。所以Action 有一些成员变量。为简单起见,我们只有 2 个,分别是 std::string name 和 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 现在容易出现未定义的行为。
我希望这个例子能让那些在谈论As 和Bs 以某种方式派生时无法真正想象事情的人有所启发。
【讨论】: