有人在IRC中提到它是切片问题。
"切片"是指将派生类的一个对象分配给基类的一个实例,从而丢失一部分信息,其中一些信息被"切片"掉。
例如,
1 2 3 4 5 6 7
| class A {
int foo;
};
class B : public A {
int bar;
}; |
因此,B类型的对象有两个数据成员:foo和bar。
如果你要写这封信:
然后,B中关于成员bar的信息在a中丢失。
- 信息非常丰富,但请参阅stackoverflow.com/questions/274626謾274636,以了解方法调用期间如何发生切片(这比简单的赋值示例更强调了危险性)。
- 有趣。我已经在C++编程了15年,这个问题从来没有发生过,因为我总是通过引用来传递效率和个人风格。去展示好习惯是如何帮助你的。
- @大卫:你能详细解释一下你的台词吗?分配后foo数据是否损坏?但是为什么呢?
- @哈迪斯:没有数据被破坏。不可能说a.bar,因为编译器认为a是a类型。如果您将其转换回类型B,那么您可以检索所有隐藏的("切片")字段。
- @费利克斯谢谢,但我认为回传(因为不是指针算术)是不可行的,A a = b;a现在是a类型的对象,它有B::foo的副本。我想现在把它扔回去是错误的。
- @哈迪斯:我明白你的意思了,我在考虑指点。你是对的,分配当然不是强制转换——事实上,一个新的对象被分配到堆栈上。那么,B中的bar完全没有损坏,只是没有被编译器生成的赋值运算符复制,所以a现在是一个类型为a的全新对象,成员a.foo设置为与b.foo相同的值。
- @卡尔:在分配任务的情况下没有帮助。B& b = xxx; b = someDerivedClass();仍然引起切片。只是问题通常没有被注意到。
- 另一个解释切片但不是问题的答案。
- 这不是"切片",或者至少是它的良性变体。如果你这样做,真正的问题就出现了。你可能认为你已经复制了b1到b2,但你没有!您复制了b1的一部分给b2的一部分(b1的一部分,B继承了a的一部分),并保留了b2的其他部分不变。b2现在是一种法兰克人的生物,由一些b1的碎片和一些b2的碎片组成。呸!投反对票是因为我认为答案是错误的。
- @fgp你的评论应该读到B b1; B b2; A& b2_ref = b2; b2_ref = b1"真正的问题是如果你"…从具有非虚拟赋值运算符的类派生。a是否用于派生?它没有虚拟功能。如果从类型派生,则必须处理这样一个事实:可以调用它的成员函数!
- 包含几个示例的链接,说明如何使用指针或引用避免切片问题。
- 回答很好+1。
- @这就是为什么你不把B放在a的参考名称中的原因。
- 这个答案是错误的。这里没有信息丢失。a只能保存一个字段,并从B获取该字段的副本。它不应该处理B可能拥有的任何其他字段。
这里的大多数答案都无法解释切片的实际问题。它们只解释了良性的切片,而不是危险的切片。假设,和其他答案一样,您正在处理两个类:A和B,其中B从A派生(公开)。
在这种情况下,C++允许您将EDCOX1 OR 1的实例传递给EDCOX1·0的赋值运算符(以及复制构造函数)。这是因为可以将B的一个实例转换为const A&,这就是赋值运算符和复制构造函数希望它们的参数是什么。
良性案例
没有什么不好的事情发生——你找了一个A的例子,它是B的副本,这正是你得到的。当然,A不会包含B的一些成员,但是应该怎么做呢?毕竟,它是一个A,而不是一个B,所以它甚至没有听说过这些成员,更不用说能够存储它们了。
险情
1 2 3 4 5
| 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++不把赋值操作符当作EDCOX1(22)来对待。因此,行a_ref = b1将调用A的赋值运算符,而不是B的赋值运算符。这是因为对于非虚拟函数,声明的类型(即A&决定调用哪个函数,而不是实际类型(即B,因为a_ref引用B的实例)。现在,A的赋值操作符显然只知道A中声明的成员,所以它只复制这些成员,保留B中添加的成员不变。
解决方案
只分配对象的一部分通常没有什么意义,但是C++不幸地没有提供内置的方式来禁止这个。不过,你可以自己滚。第一步是使分配运算符成为虚拟的。这将确保它始终是调用的实际类型的赋值运算符,而不是声明的类型。第二步是使用dynamic_cast验证分配的对象是否具有兼容的类型。第三步是在(受保护的!)成员assign(),因为B的assign()可能希望使用A的assign()复制A的成员。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| 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的一个实例。
- 当对象类型为B时,不允许对A对象执行某些操作。
- imho,问题是有两种不同的可替换性可以通过继承来暗示:要么将任何derived值赋给期望base值的代码,要么将任何派生引用用作基引用。我想看到一种具有类型系统的语言,它分别处理这两个概念。在许多情况下,派生引用应可替换为基引用,但派生实例不应替换为基引用;在许多情况下,实例应可转换,但引用不应替换。
- 从概念上讲,在.NET中,如果函数返回KeyValuePair,那么应该能够将结果存储到KeyValuePair类型的存储位置,这不是因为前者的实例是后者的实例,而是因为将返回值解释为KVP会有效地将其转换为一个实例。不幸的是,这将需要一个与普通继承类型不同的层次结构,因为前一类型的装箱实例绝对不等同于后者的装箱实例。
- 我不明白你的"背信弃义"案有什么不好。你说你想:1)得到一个对A类对象的引用,2)把对象b1转换到A类,并把它的内容复制到A类的引用中。这里实际的错误是给定代码背后的正确逻辑。换句话说,你拿了一个小的图像框(A),把它放在一个大的图像上(B),然后你画了这个框,后来抱怨你的大图像现在看起来很难看:)但是如果我们只考虑这个框区域,它看起来很好,就像画家想要的那样,对吧?:)
- 问题是,不同的是,C++默认情况下具有非常强的可替代性,它要求基类的操作正确地在子类实例上工作。即使对于编译器自动生成的类似赋值的操作也是如此。因此,在这方面不搞砸自己的操作是不够的,您还必须显式地禁用编译器生成的错误操作。或者,当然,远离公共继承,这通常是安威的一个好建议;-)
- @Supercat我对你的区别有点不清楚。它是1)透明地传递派生作为基的替换,还是2)不透明地传递对派生的引用,就好像它实际上是对基的引用一样?
- 在.NET中,每个结构类型都有一个关联的堆对象类型,它可以隐式转换为该类型。如果结构类型实现了接口,则该接口的方法可以在结构"就地"上调用。如果一个具有T类型的字段的FooStruct结构,实现包含T类型的属性Foo的IFoo接口,可以读写该字段,那么如果FooStruct继承自FooStruct结构,则应实现IFoo结构,即应能存储Animal。对它。
- 将FooStruct存储到FooStruct类型的变量中会将其转换为FooStruct,该变量可以在其字段中保存Animal,但将其转换为IFoo将其转换为与FooStruct关联的堆对象类型,该类型必须实现IFoo,但不能接受存储到它的性质。
- 另一种常见的方法是简单地禁用复制和分配操作符。对于继承层次结构中的类,通常没有理由使用值而不是引用或指针。
- @supercat"如果foostruct继承自foostruct"。呃,什么?结构如何在.NET中从自身继承?你写了一些看起来不真实的奇怪的东西。
- @阿肯昆:我试图指出如果允许这样的事情发生的问题。如果有一种方法可以声明一个不能"装箱"为其自身类型的结构类型[它仍然可以"装箱"存储到单个元素数组中],这将使.NET能够支持继承方案,因为对于每个结构类型,运行时还有效地定义了一个具有相同继承关系的AP对象类型。任何不能处理堆对象的关系也不能处理结构。
- @它不会从自己那里继承,以东十一〔0〕和以东十一〔1〕不是同一类型。在Java中你不能这么做,因为这两个在运行时都是相同的类型。但是在C++中,这两种是完全不同的类型。您应该能够使用模板专门化让一个专门化的类型(在本例中是FooStruct)从另一个(在本例中是FooStruct)变为另一个。不确定.NET,但它确实有模板特化AFAIK,但不像一般的C++那样有,我想。
- 那是什么?我不知道操作员可以标记为虚拟的
- 这个"危险"的案子确实是危险的,我以前没有考虑过。但是你所说的"良性"一点都不是良性的——几乎100%的时候,编写这段代码是错误的,而且语言的设计应该能够产生编译错误。LSP意味着当将派生对象视为基类对象时,它的行为应该是正确的,但是"行为"只意味着"对公共方法调用的响应"。LSP允许在B中任意重新定义内部状态(哪些operator=()份),因此将其复制到A实例甚至可以生成ub。
- @随机黑客,我不同意。C++的行为对于具有复杂的值类型和继承的语言是非常敏感的。我同意,如果你来自一种语言,在这种语言中,对象总是通过Java或.NET之类的引用行为来学习的话,这可能会令人惊讶,但是学习这些类型的东西只是学习C++中有效代码的一部分。我不知道A a = b如何产生同样不可能发生在B b2 = b上的UB。
- @ J2Read黑客为一个可能更令人信服的论点,为什么C++必须阻止良性的情况,考虑A赋值运算符的签名——它的EDCOX1×17。因此,避免该运算符的参数为b的实例的唯一方法是不自动将b的实例强制转换为const A&。但这会干扰LSP…
- 如果a的一个或多个数据成员被b重新调整了用途,ub可以很容易地从A a = b跟踪。(您可以说这是一个糟糕的设计/实现,但它不会违反LSP,前提是公共方法继续"正常运行",并且通常很有用。)作为一个具体示例,假设a包含两个int成员x和y,一个int*p,在其ctor中设置p=&;x,并要求表达式*p始终有效。(也许A中的其他代码有时会p=&y。)b添加第三个int成员z,并在其ctor中设置p=&z。在A a = b和b的生存期结束后,a.p不再指向任何东西。
- 我同意这是个棘手的问题。我认为正确的方法应该是在类型级别区分只允许公共访问对象的公共方法(因此必须遵守LSP)的类型和允许访问内部状态的类型。大多数方法只需要将前者作为参数(因此可以用于整个类层次结构),而赋值运算符和复制ctor则需要后者作为参数(而不遵守LSP)。但我可能忽略了一些事情。
- 你是如何重新分配样板客户的?
- @变形如果a是一个引用,那么a = b不会生成"a"引用对象"b"(就像"a=&;b",如果"a"是指针)!它调用"a"引用的对象的赋值操作符,该操作符(通常)将继续用对象"b"的内容覆盖被引用的对象。
- 啊,我明白了,是的,这个答案也解释得很好:stackoverflow.com/a/9293732/3454889
- 这里有很多答案,特别是这个,很好地解释了什么是对象切片。但是考虑到这个Q&A的流行,也可以适当地提到,通过确定非叶基类应该是抽象的(例如,参见史葛迈尔斯更有效的C++中的项目33),可以完全避免这个问题。
- 我在MSVC 2015上进行了一次测试,结果a_-ref正好等于b1。如果一个"参考"是一个法兰克斯廷的对象,很难说,因为它看起来像是对一个具有b1内容的类型A的对象的完全合法的引用。
- @用户2286810当然,如果您将a_ref和b1作为A的实例进行比较,您不会注意到任何问题,因为它们的A部分确实是相同的。但是,如果比较b1和b2,您会发现它们的A部分是相同的,但是由B添加的成员仍然具有其原始值。
- 我最近想创建一个状态结构(statusb),它包含来自结构statusa的状态以及一些附加状态。我决定从statusa中派生statusb,但在不编写自己的复制构造函数的情况下,无法想出一种用statusa中的数据更新statusb的方法。这似乎是解决我问题的好办法!
- 这应该是公认的答案。
- "这是因为对于非虚拟函数,声明的类型(即&;)决定调用哪个函数"-但仅对于重写基的虚拟函数的函数,表单T& op=(T&)中的赋值运算符不是其中之一。
如果您有一个基类a和一个派生类B,那么您可以执行以下操作。
1 2 3 4 5 6 7 8
| void wantAnA(A myA)
{
// work with myA
}
B derived;
// work with the object"derived"
wantAnA(derived); |
现在,方法wantAnA需要一份derived的副本。但是,对象derived不能完全复制,因为类B可以发明不在其基类a中的附加成员变量。
因此,要调用wantAnA,编译器将"切掉"派生类的所有附加成员。结果可能是您不想创建的对象,因为
- 可能不完整,
- 它的行为类似于a对象(类B的所有特殊行为都将丢失)。
- C++不是Java!如果是wantAnA(顾名思义!)想要一个a,那就是它得到的。一个关于a的例子,会,呃,表现得像a一样。这有什么奇怪的?
- @fgp:这很奇怪,因为您没有将a传递给函数。
- @布莱克:但是万塔纳说它想要一个A,所以这就是它得到的。这与声明一个函数接受一个int,传递0.1,然后抱怨函数收到0…
- @女性生殖器切割:行为相似。然而,对于普通C++程序员来说,它可能不那么明显。据我所知,没有人会"抱怨"。这只是关于编译器如何处理这种情况。imho,最好完全避免通过传递(const)引用进行切片。
- @布莱克:这是个危险的建议。如果稍后复制该值,则通过引用传递将阻止移动语义介入,因为即使该值最初是右值,它也将是函数内的左值。
- 那么,为了支持"移动"语义,您会放弃继承吗?坦率地说,我会惊讶于这被认为是一种普遍的"设计偏好"。
- @不,我不会放弃继承,而是使用引用。如果wantana的签名是void wantana(const a&mya),那么就没有切片。相反,将传递对调用方对象的只读引用。
- 问题主要在于编译器执行的从derived到A类型的自动转换。隐式铸造一直是C++中意外行为的根源,因为从局部看代码是很难理解的。
- @黑色,你的意思是"imho,最好不要通过传递(const)指针来切片",而不是"引用"?gcc&clang打印基本类两次…#includeusing namespace std;struct baseclass void print()const cout<"baseclass";struct derivedclass:public baseclass void print()const cout<"derivedclass";void myfunction(const baseclass&;theclass)theclass.print();int main()baseclass base;myfunction(base);derivedclass derived;myfunction(deRiver);
- @user1902689对于要在派生类中重载的函数,必须将其设置为"虚拟",例如,virtual void print()const….
- @PQnet不是演员。在呼叫点通过呼叫A::A(const A &)建立一个A。
- @caleth i指的是自动将const derived&转换成const A&,当你复制初始化A的一个实例时,这种转换是隐含的。我的法律在5年前没有达到标准,所以我经常写"cast"来标识所有转换,而不仅仅是显式的cast表达式
- @pqnet没有B&,没有转换,(复制构造函数)引用(参数)直接绑定到B的A子对象。
- @caleth表达式wantAnA(derived)中的表达式derived是一个左值引用,类型为B&,因为derived是对应于B类型的非常量变量的标识符。该语言自动将从B&到const A&的引用转换为const_cast(static_cast(derived))中的引用,然后允许它调用A(A const &)复制A类的构造函数。如果复制构造函数知道类B并试图推断其参数的实际类型(即通过尝试dynamic_cast其参数),则它可以访问整个B对象。
这些都是很好的答案。我只想在按值传递对象和按引用传递对象时添加一个执行示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| #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());
} |
输出是:
1 2 3 4 5 6
| Call by reference
I am a 'B'
Call by copy
'A' copy constructor
I am an 'A' |
- 这是避免对象切片的最简单解决方案,+1。
- 很好的例子,+1。
- 你好。回答很好,但我有一个问题。如果我这样做**dev d;base*b=&d;**切片也会发生吗?
谷歌在"C++切片"中的第三次匹配给了我这篇维基百科文章HTTP://E.WiKiTo.Org/WiKi/Objista切片,这个(被加热了,但前几篇文章定义了问题):HTTP://ByTest.COM/FUMUM/THEADR163565.HTML
所以当你将一个子类的对象赋给这个超类时。超类对子类中的附加信息一无所知,并且没有存储空间,因此附加信息被"切掉"。
如果这些链接没有提供足够的信息来回答"好答案",请编辑您的问题,让我们知道您还需要什么。
切片问题很严重,因为它会导致内存损坏,而且很难保证程序不受此影响。为了用语言设计它,支持继承的类应该只能通过引用(而不是通过值)来访问。D编程语言具有这个特性。
考虑类A和从A派生的类B。如果A部分具有指针P,并且B实例指向P到B的附加数据,则可能发生内存损坏。然后,当额外的数据被切掉时,P指向垃圾。
- 请解释如何发生内存损坏。
- 我不相信他
- 给定:class a virtual void foo()class b:a int*p;void foo()*p=3;现在,当分配给a时,切片a b,调用foo(),调用b::foo()和voila!通过垃圾值为B::P分配内存损坏。
- 调用foo()将在切片后调用a::foo(),而不是b::foo()。
- 是的,沃尔特混合了指针分配(其中a*指向b对象,所以b::p不会丢失)和对象分配(之后调用a::foo)。
- 我忘了复制ctor会重置vptr,我的错误。但是,如果a有一个指针,b将其设置为指向被切掉的b的部分,那么仍然会出现损坏。
- 这个问题不仅仅局限于切片。任何包含指针的类都将使用默认的赋值运算符和复制构造函数具有可疑的行为。
- @weeble——这就是为什么在这些情况下重写默认的析构函数、赋值运算符和复制构造函数。
- 请说明内存损坏的发生方式。
- 多态性需要一个虚拟函数或CRTP(模板使其在编译时发生)。
- @weeble:使对象切片比一般的指针修正更糟的是,为了确保防止切片发生,基类必须为每个派生类提供转换构造函数。(为什么?任何被遗漏的派生类都容易被基类的copy-ctor拾取,因为Derived隐式地可转换为Base。这显然与开放-关闭原则背道而驰,并且维护负担很大。
- 这是C++上最令人沮丧的答案之一。这没有例子,真的很模糊,只是错误的:切片通常不会导致"内存损坏"(你能,详细说明这个术语吗?),我看不到会损坏堆栈或任何对执行敏感的内容的情况。然而,在他自己的应用程序中,它可以是一个程序员错误,但它不是"损坏"。最后,像这样的大多数分配都将通过指针完成,而不进行切片,在大多数流行的引用中从未讨论过op的模式。
在C++中,派生类对象可以被分配给基类对象,但另一种方式是不可能的。
1 2 3 4 5 6 7 8 9
| 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
} |
当派生类对象分配给基类对象时,将发生对象切片,派生类对象的其他属性将被切片以形成基类对象。
1。切片问题的定义
如果d是基类b的派生类,则可以将派生类型的对象赋给基类型的变量(或参数)。
例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| 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。如何解决切片问题
为了解决这个问题,我们使用指向动态变量的指针。
例子
1 2 3 4 5 6 7
| Pet *ptrP;
Dog *ptrD;
ptrD = new Dog;
ptrD->name ="Tommy";
ptrD->breed ="Kangal Dog";
ptrP = ptrD;
cout << ((Dog *)ptrP)->breed; |
在这种情况下,动态变量的数据成员或成员函数都不是被ptrd(后代类对象)指向的将丢失。此外,如果需要使用函数,则该函数必须是虚拟函数。
- 我理解"切片"部分,但我不理解"问题"。不属于类Pet的dog的某些状态(breed数据成员)没有复制到变量Pet中,这是一个什么问题?代码只对Pet数据成员感兴趣—显然。如果不需要切片,那么切片肯定是一个"问题",但我在这里看不到这一点。
- "((Dog *)ptrP)"我建议使用static_cast(ptrP)。
- 我建议指出,当通过"ptrp"删除时,将使字符串"breed"最终在没有虚拟析构函数的情况下泄漏内存(不会调用"string"的析构函数)。为什么你展示的东西有问题?修正主要是适当的类设计。在这种情况下,问题在于,在继承时写下构造函数来控制可见性是繁琐且容易忘记的。由于没有涉及或甚至提到多态性(切片将截断对象,但不会使程序崩溃,这里),所以使用代码无法接近危险区。
- -1这完全无法解释实际问题。C++有价值语义,而不是像Java那样的引用语义,所以这完全是可以预期的。而"修复"真的是一个真正可怕的C++代码的例子。通过使用动态分配来修复"这种类型的切片等不存在的问题"是错误代码、泄漏内存和糟糕性能的一个秘诀。请注意,有些情况下切片不好,但这个答案没有指出它们。提示:如果通过引用分配,则问题将开始。
- 您是否知道,尝试访问未定义的类型(Dog::breed的成员)不可能是与切片相关的错误?
所以…为什么丢失派生信息很糟糕?…因为派生类的作者可能已经更改了表示方式,从而将额外的信息切片会更改对象所表示的值。如果派生类用于缓存对某些操作更有效但转换回基表示形式代价高昂的表示形式,则可能发生这种情况。
还认为应该有人提到你应该做什么,以避免切片…获取C++编码标准、101条规则指南和最佳实践的副本。处理切片是54。
它提出了一个相当复杂的模式来完全处理这个问题:拥有一个受保护的复制构造函数、一个受保护的纯虚拟doclone和一个带有断言的公共克隆,断言将告诉您(进一步的)派生类是否无法正确地实现doclone。(克隆方法对多态对象进行适当的深度复制。)
您还可以在基显式上标记复制构造函数,如果需要,它允许显式切片。
- "您还可以在基显式上标记复制构造函数",这一点都没有帮助。
C++中的切片问题源于其对象的语义语义,这主要是由于与C结构的兼容性。您需要使用显式引用或指针语法来实现"正常"的对象行为,这种行为在大多数执行对象的其他语言中都存在,即对象总是通过引用传递。
简短的回答是,通过将派生对象按值分配给基对象来分割对象,即,剩余的对象只是派生对象的一部分。为了保留价值语义,切片是一种合理的行为,它的使用相对较少,在大多数其他语言中并不存在。有些人认为它是C++的一个特性,而许多人认为它是C++的怪癖/错误特征之一。
- "正常"的"对象行为"不是"正常的对象行为",即引用语义。任何一个随机的OOP牧师都告诉你,这与C.struct没有任何关系,也没有兼容性或其他的非理性。
- @好奇的家伙阿门,兄弟。很遗憾,当C++中的C++语义如此强大时,C++会受到来自Java的冲击。
- 这不是特征,也不是怪癖/错误的特征。堆栈复制行为是正常的,因为调用带有arg的函数或(相同的)分配类型为Base的堆栈变量必须在内存中精确地取sizeof(Base)字节,并且可能对齐,这就是为什么"赋值"(在堆栈复制时)不会复制派生类成员,它们的偏移量在sizeof之外。为了避免"丢失数据",只需像其他任何人一样使用指针,因为指针内存固定在适当的位置和大小,而堆栈则非常随意。
在我看来,切片并不是什么问题,除非你自己的类和程序的架构/设计不好。
如果我将一个子类对象作为一个参数传递给一个方法,这个方法接受一个超类类型的参数,我当然应该知道这一点并知道它的内部,被调用的方法将只处理超类(也就是baseclass)对象。
在我看来,在请求基类的情况下提供子类会导致子类特定的结果,从而导致切片成为一个问题,这只是不合理的期望。它要么是方法使用中的设计不好,要么是子类实现不好。我猜这通常是牺牲良好的OOP设计而有利于方便或提高性能的结果。
- 但是记住,米诺克,你不会传递这个对象的引用。您正在传递该对象的新副本,但在进程中使用基类来复制它。
- 在基类上进行受保护的复制/分配,解决了这个问题。
- 你说得对。好的做法是使用抽象基类或限制对复制/分配的访问。然而,一旦它在那里就不那么容易被发现,而且很容易忘记照顾。使用sliced*调用虚拟方法,如果您不违反访问规则而离开,可能会发生神秘的事情。
- 我记得我在大学中的C++编程课程中,对于我们创建的每一类都有最好的实践,我们需要编写默认构造函数、复制构造函数和赋值运算符以及析构函数。这样,在编写类时,您就可以确保复制结构等发生在您需要它的时候。而不是后来出现一些奇怪的行为。
好吧,我会在阅读了很多文章解释对象切片之后尝试一下,但不会解释它是如何变得有问题的。
可能导致内存损坏的恶性情况如下:
- 类对多态基类提供(意外地,可能是由编译器生成的)赋值。
- 客户端复制并切片派生类的实例。
- 客户机调用一个虚拟成员函数,该函数访问被切掉的状态。
在这里找到类似的答案:http://sickprogrammerarea.blogspot.in/2014/03/technical-interview-questions-on-c_6.html
切片意味着当子类的对象通过值传递或返回时,或从期望基类对象的函数返回时,子类添加的数据将被丢弃。
说明:考虑以下类声明:
1 2 3 4 5 6 7 8 9 10 11
| class baseclass
{
...
baseclass & operator =(const baseclass&);
baseclass(const baseclass&);
}
void function( )
{
baseclass obj1=m;
obj1=m;
} |
因为基类复制函数对派生函数一无所知,所以只复制派生函数的基部分。这通常被称为切片。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| 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;
} |
- 你介意提供一些额外的细节吗?你的答案与已经发布的有什么不同?
- 我想更多的解释也不错。
将派生类对象分配给基类对象时,派生类对象的其他属性将从基类对象中剥离(放弃)。
1 2 3 4 5 6 7 8 9 10 11 12 13
| 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
} |
将派生类对象分配给基类对象时,派生类对象的所有成员都将复制到基类对象,但不在基类中的成员除外。这些成员被编译器分割开。这叫做对象切片。
下面是一个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| #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<<"
";
cout<<d.b<<"
";
cout<<d.c<<"
";
cout<<d.d<<"
";
cout<<d.e<<"
";
Base b = d;
cout<<b.a<<"
";
cout<<b.b<<"
";
cout<<b.c<<"
";
cout<<b.d<<"
";
cout<<b.e<<"
";
return 0;
} |
它将产生:
1 2
| [Error] 'class Base' has no member named 'd'
[Error] 'class Base' has no member named 'e' |
- 投反对票是因为这不是一个好例子。如果不是将d复制到b,而是使用一个指针,在这种情况下d和e仍然存在,但base没有这些成员,那么它也不会工作。您的示例只显示您不能访问类没有的成员。
我刚刚遇到了切片问题,很快就到了这里。所以让我在这上面加上我的两分钱。
让我们举一个"生产代码"的例子(或者类似的东西):
比如说,我们有一些东西可以分派行动。例如,Control Center UI。此UI需要获取当前可以调度的内容的列表。所以我们定义了一个包含调度信息的类。我们称之为Action。所以Action有一些成员变量。为了简单起见,我们只有2个,分别是一个std::string name和一个std::function f。然后它有一个void activate()来执行f成员。
所以用户界面得到一个std::vector。想象一下一些功能,比如:
1
| void push_back(Action toAdd); |
现在,我们已经从用户界面的角度确定了它的外观。到目前为止没问题。但是在这个项目上工作的其他人突然决定,在Action对象中有需要更多信息的专门操作。因为什么原因。这也可以通过lambda捕获来解决。这个例子不是从代码中1-1得到的。
所以这个家伙从Action衍生来添加他自己的味道。他将自己的自制类的一个实例传递给了push_back,但随后程序就失控了。
那发生了什么?正如您可能已经猜到的:对象已经被切片了。
来自实例的额外信息已丢失,并且f现在容易出现未定义的行为。
我希望这个例子能让那些在讨论A和B的时候不能真正想象的人有所启发。