4

好的,我正在阅读FQA 中关于将 a 转换为 a以及为什么禁止它的问题的这个条目,我知道问题是你可以分配一个不是 a 的东西,所以我们禁止这样做。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做是愚蠢的,所以我们可以说让这种转换很好,因为我们启用了有趣的设计,但我们也可以说对于双重间接:你有 a Base **,但你不应该给它分配任何东西尊重,因为你真的不知道它Base **来自哪里,就像Base *.

那么问题来了:这个额外的间接层级有什么特别之处?也许关键是,只有一层间接,我们可以玩 virtualoperator=来避免这种情况,而相同的机制在普通指针上不可用?

4

6 回答 6

16
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不会神奇地变成 a Base,是吗?它仍然是Derived,但它的Base一部分发生了变化。


在图片中:)

这是对象BaseDerived样子:

布局

当我们有两个间接级别时,它不起作用,因为分配的东西是指针:

分配指针 - 类型不匹配

请注意如何没有尝试更改所讨论的Base或对象:只有中间指针是。Derived

但是,当您只有一个间接级别时,代码会以对象允许的方式修改对象本身(它可以通过将赋值运算符设为私有、隐藏或删除 a 来禁止它Base):

仅使用一级间接分配

注意这里没有指针改变。这就像任何其他更改对象部分的操作一样,例如d.y = 42;.

于 2012-06-29T15:35:36.100 回答
7

不,nasty_function()并不像听起来那么讨厌。由于指针b指向is-a Base,因此为其分配 -value 是完全合法的Base

注意:您的“哎呀”评论不正确:d仍然指向与Derived通话前相同!只是,Base它的一部分被重新分配(按价值!)。如果这让你的整体Derived失去一致性,你需要通过Base::operator=()虚拟化来重新设计。然后,在 中nasty_function(),实际上Derived将调用赋值运算符(如果已定义)。

因此,我认为,您的示例与指针对指针的情况没有太大关系。

于 2012-06-29T15:36:33.647 回答
2

*b = Base(3)调用Base::operator=(const Base&),它实际上Derived作为成员函数(包括运算符)存在于被继承。

然后会发生的事情(调用Derived::operator=(const Base&))有时被称为“切片”,是的,这很糟糕(通常)。=这是C++ 中“变得类似”运算符 (the ) 无处不在的一个可悲的结果。

(请注意,大多数 OO 语言(如 Java、C# 或 Python)中不存在“become-like”运算符;=在对象上下文中,这意味着引用赋值,类似于 C++ 中的指针赋值;)。


加起来:

强制转换Derived**->Base**是被禁止的,因为它们会导致类型错误,因为这样你最终可能会得到一个指向类型Derived*对象的类型指针Base

您提到的问题不是类型错误;这是一种不同类型的错误:对象接口的误用derived,源于它继承了其父类的“变得类似”运算符的遗憾事实。


(是的,我故意在对象上下文中将 op= 称为“变得类似”,因为我觉得“赋值”不是一个显示这里发生的事情的好名字。)

于 2012-06-29T15:36:51.827 回答
0

好吧,您提供的代码很有意义。实际上,赋值运算符不能覆盖特定于 Derived 的数据,而只能覆盖基数。虚函数仍然来自 Derived 而不是来自 Base。

于 2012-06-29T15:38:46.033 回答
0
*b = Base(3); // Ouch!

这里的对象*b真的是 a B,它是 的基础子对象*d。只有基础子对象被修改,派生对象的其余部分没有改变,d仍然指向派生类型的同一个对象。

您可能不希望修改基数,但就类型系统而言,它是正确的。ADerived 是一个 Base

对于非法指针情况,情况并非如此。ADerived*可转换为Base*但不是同一类型。它违反了类型系统。

允许您询问的转换与此没有什么不同:

Derived* d;
Base b;
d = &b;
d->x;
于 2012-06-29T15:40:25.043 回答
0

通读我的问题的好答案,我想我明白了问题的重点,它来自 OO 中的第一原则,与子对象和运算符重载无关。

关键是您可以在需要 aDerived时使用 a Base(替换原则),但您不能Derived*在需要 a时使用 a Base*,因为可能会将派生类实例的指针分配给它。

使用此原型获取一个函数:

void f(Base **b)

f可以用 做很多事情b,除其他外取消引用它:

void f(Base **b)
{
  Base *pb = *b;
  ...
}

如果我们传递给fa Derived**,则意味着我们将 aDerived*用作 a Base*,这是不正确的,因为我们可以将 a 分配OtherDerived*Base*而不是给Derived*

另一方面,采用此功能:

void f(Base *b)

如果fdereferences b,那么我们将使用 aDerived代替 a Base,这完全可以(前提是您给出了类层次结构的正确实现):

void f(Base *b)
{
  Base pb = *b; // *b is a Derived? No problem!
}

换句话说:替换原则(使用派生类而不是基类)适用于实例,而不是指针,因为指针的“概念”A是“指向继承的任何类的实例A”,并且继承的类Base集严格包含继承的类集Derived

于 2012-06-29T16:48:48.150 回答