【问题标题】:Why do you need pointers in this situation? [duplicate]为什么在这种情况下需要指针? [复制]
【发布时间】:2011-07-31 18:52:12
【问题描述】:

可能重复:
Learning C++: polymorphism and slicing

这是基于我之前提出的一个问题。 类如下所示:

class Enemy
{
    public:
        void sayHere()
        {
            cout<<"Here"<<endl;
        }
        virtual void attack()
        {
        }
};

class Monster: public Enemy
{

    public:
        void attack()
        {
            cout<<"RAWR"<<endl;
        }

};
class Ninja: public Enemy
{

    public:
        void attack()
        {

            cout<<"Hiya!"<<endl;
        }
};

我是 C++ 新手,我很困惑为什么这只适用于指针(Ninja 和 Monster 都来自 Enemy):

int main()
{
    Ninja ninja;
    Monster monster;

    Enemy *enemies[2];

    enemies[0] = &monster;
    enemies[1] = &ninja;

    for (int i = 0; i < 2; i++)
    {
        enemies[i]->attack();
    }

    return 0;
}

为什么我不能这样做?:

int main()
{
    Ninja ninja;
    Monster monster;

    Enemy enemies[2];

    enemies[0] = monster;
    enemies[1] = ninja;

    for (int i = 0; i < 2; i++)
    {
        enemies[i].attack();
    }

    return 0;
}

【问题讨论】:

  • +1,这个问题其实很有趣。当然,我知道为什么第二个示例不起作用,但是如果有人知道为什么 C++ 标准根本不支持第二个示例,我会很感兴趣(毕竟,“自动”复制所有必需的值从派生类型理论上不应该那么难)。
  • @heishe,这叫切片,你google一下,有点烂
  • “公敌”的奖励积分 ;)
  • 我喜欢忍者在攻击时友好地向你打招呼。

标签: c++ arrays pointers polymorphism


【解决方案1】:

这是一个很好的问题,它触及了 C++ 继承的一些棘手问题的核心。由于静态类型动态类型之间的差异,以及 C++ 为对象分配存储空间的方式,造成了混淆。

首先,让我们讨论一下静态类型和动态类型之间的区别。 C++ 中的每个对象都有一个静态类型,它是源代码中描述的对象的类型。例如,如果您尝试编写

Base* b = new Derived;

那么b 的静态类型是Base*,因为在源代码中这是您为其声明的类型。同样,如果你写

Base myBases[5];

myBases 的静态类型是Base[5],一个由五个Bases 组成的数组。

对象的动态类型是对象在运行时实际具有的类型。例如,如果你写类似

Base* b = new Derived;

那么b 的动态类型是Derived*,因为它实际上指向一个Derived 对象。

静态和动态类型之间的区别在 C++ 中很重要,原因有两个:

  1. 对象的分配始终基于对象的静态类型,而不是动态类型。
  2. 仅当静态类型是指针或引用时,虚函数调用才会分派给动态类型。

让我们依次解决这些问题。

首先,第二版代码的一个问题是你做了以下事情:

Ninja ninja;
Monster monster;

Enemy enemies[2];

enemies[0] = monster;
enemies[1] = ninja;

让我们追溯这里发生的事情。这首先创建一个新的NinjaMonster 对象,然后创建一个Enemy 对象数组,最后将ninjamonster 的值分配给enemies 数组。

这段代码的问题是当你写的时候

enemies[0] = monster;

lhs 的静态类型是Enemy,rhs 的静态类型是Monster。在确定如何进行赋值时,C++ 只查看对象的静态类型,从不查看动态类型。这意味着因为enemies[0] 被静态类型化为Enemy,所以它必须精确地保存Enemy 类型的东西,而不是任何派生类型。这意味着当您执行上述分配时,C++ 将其解释为“获取monster 对象,仅识别它的Enemy 部分,然后将该部分复制到enemies[0] 中。”换句话说,虽然Monster 是带有一些额外添加的Enemy,但只有MonsterEnemy 部分会被这行代码复制到enemies[0] 中。这称为slicing,因为您要切掉对象的一部分,只留下Enemy 基础部分。

在您发布的第一段代码中,您有:

Ninja ninja;
Monster monster;

Enemy *enemies[2];

enemies[0] = &monster;
enemies[1] = &ninja;

这是绝对安全的,因为在这行代码中:

enemies[0] = &monster;

lhs 具有静态类型Enemy*,rhs 具有类型Monster*。 C++ 合法地允许您将指向派生类型的指针转​​换为指向基类型的指针,而不会出现任何问题。因此,rhs monster 指针可以无损地转换为 lhs 类型 Enemy*,因此对象的顶部不会被切掉。

更一般地,将派生对象分配给基础对象时,您可能会切分对象。将指向派生对象的指针存储在指向基对象类型的指针中总是更安全、更可取,因为不会执行切片。

这里还有第二点。在 C++ 中,每当您调用虚函数时,如果接收者是指针或引用类型,则仅在对象的动态类型(对象在运行时的真实对象类型)上调用该函数。也就是说,如果你有原始代码:

Ninja ninja;
Monster monster;

Enemy enemies[2];

enemies[0] = monster;
enemies[1] = ninja;

然后写

enemies[0].attack();

那么因为enemies[0] 具有静态类型Enemy,编译器不会使用动态调度来确定要调用哪个版本的attack 函数。这样做的原因是,如果对象的静态类型是 Enemy,它总是 在运行时引用 Enemy,仅此而已。但是,在第二版代码中:

Ninja ninja;
Monster monster;

Enemy *enemies[2];

enemies[0] = &monster;
enemies[1] = &ninja;

当你写作时

enemies[0]->attack();

然后因为enemies[0] 具有静态类型Enemy*,它可以指向EnemyEnemy 的子类型。因此,C++ 将函数分派给对象的动态类型。

希望这会有所帮助!

【讨论】:

  • +1 尤其是这个位,“因为enemies[0]有静态类型Enemy,编译器不会使用动态调度来决定调用哪个版本的attack函数”。
  • +1。我读过的关于切片的最佳答案之一。
【解决方案2】:

没有指针,你的敌人[] 数组代表堆栈上的一个空间,足以存储两个“敌人”对象——这意味着存储它们的所有字段(可能加上 vtable 指针和对齐的开销)。 Enemy 的派生类可能具有额外的字段,因此更大,因此它不允许您将 Enemy 的派生对象存储在为实际 Enemy 对象保留的空间中。当您像示例中那样进行赋值时,它使用赋值运算符(在这种情况下,隐式定义) - 它将左侧对象字段中的值设置为右侧对象中相应字段的值,保持左侧对象的类型(以及 vtable 指针)不变。这称为“对象切片”,通常应避免。

指针的大小都相同,因此您可以将指向 Enemy 的派生对象的指针放在指向 Enemy 的指针的空间中,并将其用作指向普通敌人对象的指针。由于指向派生对象的指针指向派生对象的实际实例,因此对指针的虚函数调用将使用派生对象的 vtable 并为您提供所需的行为。

【讨论】:

  • 是的,这被称为“切片”对象,这很糟糕!
  • 但是 vtable 不是存储在与 Enemy 对象相同的内存“块”中吗?然后,当我执行new Derived 时,vtable 中的指针更改为指向Derived::attack 而不是Enemy::attack。 vtable 是否存储在其他地方?
  • @Mat - 我已经扩展并澄清了我的回复,以反映您(和其他人)的 cmets。
【解决方案3】:

在 C++ 中,这称为切片。

Enemy() 创建一个 Enemy 对象。如果你调用 Enemy().attack(),它不会打印任何东西,因为那个方法是空的。

在 C++ 中获得多态行为的唯一方法是使用指针或引用。

【讨论】:

    【解决方案4】:

    使用指针是在 C++ 中实现多态性的方式(参见here)。如果您尝试将monsterninja 对象放入enemies 的数组中,则会出现类型不匹配错误。但是“指向派生类的指针与指向其基类的指针类型兼容。”

    【讨论】:

      【解决方案5】:

      这会给你一个完全不同的结果。

      在第一个使用指针的场景中,您将拥有 Enemy 指针,这些指针将指向您的 NinjaMonster 对象。对象将是完整的,并且在运行时,attack() 调用将调用对象的 attack() 方法。

      在另一种情况下,您有实际的 Enemy 对象。当您分配 NinjaMonster 对象时,只会复制普通成员(不属于 Enemy 的其余成员将丢失)。 然后,当您调用 attack() 时,它将是 Enemy attack() (因为它们是 Enemy 对象)

      【讨论】:

        【解决方案6】:

        Enemy enemies[2]; 创建一个具体类型的对象数组 (Enemy)。这意味着除其他外,该数组的所有元素都具有已知大小。

        这如何与可能包含其他数据的派生类一起使用?没有。

        另一方面,给定指针,这根本不重要。指针将指向“某物”(vtable 加上数据),并且虚拟继承机制以某种方式确定什么是什么,什么是什么。可能有相同的功能,重载的,额外的数据字段,它仍然可以工作。

        【讨论】:

          【解决方案7】:

          将怪物和忍者分配给你的 Enemy 数组是可行的,但是,当你对每一个调用函数攻击时,它会调用基类的攻击函数。为什么?首先,当您将对象分配到 Enemy 数组时,您实际上是在对这些类进行类型转换,因此当您与其对象交互时,它们的行为就像 Enemy 的,而不是它们原来的样子。

          如果你注意到了,你在 Enemy 中将你的攻击函数声明为虚拟的。这允许什么在多态性中是必不可少的。通过将该函数声明为虚拟函数,您允许 Enemy 的子类对象(例如 Monster 和 Ninja)在运行时确定使用 Enemy 指针时使用哪个版本的函数攻击。这允许您使用通用的 Enemy 指针来访问不同的子类对象,并且仍然可以正确使用正确的函数:

          Enemy * ptr;
          Enemy copy;
          Monster m;
          
          copy = (Enemy)m;
          ptr = &m;
          
          copy.attack(); // Calls Enemy's definition of attack, which is undefined.
          ptr->attack(); // Even though this is an Enemy pointer, the Monster's definition of attack is used.
          

          【讨论】:

            【解决方案8】:

            通过写作

            enemies[0] = monster;
            

            您正在将 Monster 对象转换为 Enemy 对象。每个派生类对象都可以自动转换为基类对象。这称为对象切片。一旦发生这种转换,Enemy 对象就不再有任何方式记住它曾经是 Monster 对象,它只是一个普通的 Enemy 对象,就像任何其他对象一样。所以当你调用攻击时,你调用 Enemy::attack。

            这个问题在 Java 中不会出现,因为在 Java 中一切都是自动的指针。

            【讨论】:

              【解决方案9】:

              不支持它,因为当您为超类实例分配子类实例的值时,将剔除不在超类中的子类信息。因此,一些依赖于子类的方法——甚至是多态的方法——并不适用于所有情况。在编译时保证类型安全的唯一通用方法是使用父类的实现。

              短版:父类实例的状态可能比子类实例少,因此对父类实例的操作必须假设它们是为父类定义的。指针避免了这种情况,因为具有完整状态的子类实例确实存在。

              【讨论】:

                【解决方案10】:

                因为实现这样的功能是不可能的(也许非常困难,也许可以使用指针来完成)。主要原因是基础对象和派生对象可以有不同的大小(sizeof(Enemy) != sizeof(Monster)),将怪物存储在敌人中只会丢失一些数据。

                【讨论】:

                  猜你喜欢
                  • 2016-08-08
                  • 1970-01-01
                  • 2016-07-17
                  • 1970-01-01
                  • 2012-05-25
                  • 1970-01-01
                  • 2013-01-25
                  • 1970-01-01
                  • 1970-01-01
                  相关资源
                  最近更新 更多