好的,我正在阅读this entry in the FQA处理有关将Derived**
转换为Base**
以及为何被禁止的问题,我得知问题是您可以分配Base*
某个不是Derived*
的东西,所以我们禁止这样做。
到目前为止,非常好。
但是,如果我们深入应用这一原则,为什么我们不禁止这样的例子呢?
void nasty_function(Base *b)
{
*b = Base(3); // Ouch!
}
int main(int argc, char **argv)
{
Derived *d = new Derived;
nasty_function(d); // Ooops, now *d points to a Base. What would happen now?
}
我同意nasty_function
做一些愚蠢的事情,所以我们可以说让这种转换很好,因为我们启用了有趣的设计,但我们可以说这也是为了双重间接:你有一个{{ 1}},但你不应该为它的顺序分配任何东西,因为你真的不知道Base **
的来源,就像Base **
一样。
所以,问题是:间接的额外级别有什么特别之处?也许重点是,只有一个间接层,我们可以使用虚拟Base *
来避免这种情况,而普通指针上没有相同的机制吗?
答案 0 :(得分:14)
nasty_function(d); // Ooops, now *d points to a Base. What would happen now?
不,它没有。它指向Derived
。该函数只是更改了现有Base
对象中的Derived
子对象。考虑:
#include <cassert>
struct Base {
Base(int x) : x(x) {}
int x;
};
struct Derived : Base {
Derived(int x, int y) : Base(x), y(y) {}
int y;
};
int main(int argc, char **argv)
{
Derived d(1,2); // seriously, WTF is it with people and new?
// You don't need new to use pointers
// Stop it already
assert(d.x == 1);
assert(d.y == 2);
nasty_function(&d);
assert(d.x == 3);
assert(d.y == 2);
}
d
不会神奇地成为Base
,是吗?它仍然是Derived
,但它的Base
部分发生了变化。
在图片中:))
这是Base
和Derived
个对象的样子:
当我们有两个间接层时,它不起作用,因为被分配的东西是指针:
请注意,未尝试更改有问题的Base
或Derived
对象:只有中间指针是。
但是,当你只有一个间接层时,代码以对象允许的方式修改对象本身(它可以通过私有,隐藏或删除{{1}中的赋值运算符来禁止它。 }):
注意这里没有改变指针。这就像改变对象的一部分的任何其他操作一样,如Base
。
答案 1 :(得分:7)
不,nasty_function()
并不像听起来那么令人讨厌。由于指针b
指向是-a Base
的内容,因此为其分配Base
- 值是完全合法的。
注意:您的“Ooops”评论不正确:d
仍然指向与通话前相同的Derived
!只是,它的Base
部分被重新分配(按值!)。如果这导致整个Derived
不一致,则需要通过使Base::operator=()
虚拟来重新设计。然后,在nasty_function()
中,实际上将调用Derived
赋值运算符(如果已定义)。
所以,我认为,你的例子与指针到指针的情况没那么多。
答案 2 :(得分:2)
*b = Base(3)
调用Base::operator=(const Base&)
,它实际存在于Derived
中,因为成员函数(包含运算符)是继承的。
然后会发生什么(调用Derived::operator=(const Base&)
)有时会被称为 "slicing" ,是的,它很糟糕(通常)。这是C ++中“变成类似”运算符(=
)纯粹无所不在的悲惨结果。
(请注意,大多数OO语言中都不存在“变形”运算符,如Java,C#或Python;对象上下文中的=
表示引用赋值,类似于C ++中的指针赋值;)
总结:
投射Derived**
- &gt; Base**
被禁止,因为它们可能会导致类型错误,因为这样您最终会得到一个类型为Derived*
的指针指向Base
类型的对象
您提到的问题不是类型错误;这是一种不同类型的错误:错误使用derived
对象的接口,这源于它继承了其父类的“变形”运算符的遗憾事实。
(是的,我故意将对象上下文中的op =称为“变得像”,因为我觉得“赋值”并不是一个好名字来展示这里发生的事情。)
答案 3 :(得分:0)
你提供的代码是有道理的。实际上,赋值运算符不能覆盖特定于Derived的数据,而只能覆盖基数。虚函数仍来自Derived而不是Base。
答案 4 :(得分:0)
*b = Base(3); // Ouch!
此处*b
处的对象确实是B
,它是*d
的基础子对象。只有那个基础子对象被修改,派生对象的其余部分才不会改变,d
仍然指向派生类型的同一个对象。
您可能不希望允许修改基础,但就类型系统而言,它是正确的。 Derived
是 Base
。
对于非法指针案例,情况并非如此。 Derived*
可转换为Base*
但不是同一类型。它违反了类型系统。
允许您提出的转换与此无异:
Derived* d;
Base b;
d = &b;
d->x;
答案 5 :(得分:0)
通过我的问题的好答案,我认为我得到了问题的重点,它来自OO中的第一原则,与子对象和运算符重载无关。
关键是,只要需要Derived
(替换原则),您就可以使用Base
,但只要{{>> ,就<{1}}由于可以为其分配派生类实例的指针,因此需要{1}}。
使用此原型获取功能:
Derived*
Base*
可以使用void f(Base **b)
执行大量操作,并将其解除引用:
f
如果我们转到b
void f(Base **b)
{
Base *pb = *b;
...
}
,则表示我们使用f
作为Derived**
,这是不正确的,因为我们可能会指定{{1} } Derived*
但不是Base*
。
另一方面,请使用此功能:
OtherDerived*
如果Base*
取消引用Derived*
,那么我们会使用void f(Base *b)
代替f
,这完全没问题(前提是您正确实施了类层次结构):
b
用另一种方式说:替换原则(使用派生类而不是基类)适用于实例,而不是指针,因为“概念“指向Derived
的指针”指向继承Base
的任何类的实例“,继承void f(Base *b)
{
Base pb = *b; // *b is a Derived? No problem!
}
的类集严格包含继承{{1}的类集合}}