为什么在这种情况下需要指针?

Hud*_*den 15 c++ arrays polymorphism pointers

可能重复:
学习C++:多态和切片

这是我之前提出的一个问题.这些类看起来像这样:

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;
        }
};
Run Code Online (Sandbox Code Playgroud)

我是C++的新手,我很困惑为什么这只能用指针(忍者和怪物都来自敌人):

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;
}
Run Code Online (Sandbox Code Playgroud)

为什么我不能这样做呢?:

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;
}
Run Code Online (Sandbox Code Playgroud)

tem*_*def 23

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

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

Base* b = new Derived;
Run Code Online (Sandbox Code Playgroud)

那么静态类型bBase*,因为在源代码中是你为它声明的类型.同样,如果你写

Base myBases[5];
Run Code Online (Sandbox Code Playgroud)

静态类型myBasesBase[5],一个五秒Base的数组.

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

Base* b = new Derived;
Run Code Online (Sandbox Code Playgroud)

然后动态类型bDerived*,因为它实际上指向一个Derived对象.

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

  1. 对象的赋值始终基于对象的静态类型,而不是动态类型.
  2. 如果静态类型是指针或引用,则虚拟函数的调用仅调度到动态类型.

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

首先,第二版代码的问题之一是您执行以下操作:

Ninja ninja;
Monster monster;

Enemy enemies[2];

enemies[0] = monster;
enemies[1] = ninja;
Run Code Online (Sandbox Code Playgroud)

让我们来看看这里发生的事情.这首先创建一个新的NinjaMonster对象,然后创建的阵列Enemy对象,最后分配enemies数组的值ninjamonster.

这段代码的问题在于你写的时候

enemies[0] = monster;
Run Code Online (Sandbox Code Playgroud)

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

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

Ninja ninja;
Monster monster;

Enemy *enemies[2];

enemies[0] = &monster;
enemies[1] = &ninja;
Run Code Online (Sandbox Code Playgroud)

这是非常安全的,因为在这行代码中:

enemies[0] = &monster;
Run Code Online (Sandbox Code Playgroud)

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

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

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

Ninja ninja;
Monster monster;

Enemy enemies[2];

enemies[0] = monster;
enemies[1] = ninja;
Run Code Online (Sandbox Code Playgroud)

和写

enemies[0].attack();
Run Code Online (Sandbox Code Playgroud)

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

Ninja ninja;
Monster monster;

Enemy *enemies[2];

enemies[0] = &monster;
enemies[1] = &ninja;
Run Code Online (Sandbox Code Playgroud)

当你写作

enemies[0]->attack();
Run Code Online (Sandbox Code Playgroud)

然后因为enemies[0]有静态类型Enemy*,它可以指向一个Enemy或一个子类型Enemy.因此,C++将函数调度到对象的动态类型.

希望这可以帮助!


ant*_*oft 16

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

指针的大小都相同,因此您可以在空间中放置一个指向Enemy的派生对象的指针,以指向Enemy并使用它,就好像它是指向普通敌对象的指针一样.由于指向派生对象的指针指向派生对象的实际实例,因此对指针上的虚函数的调用将使用派生对象的vtable并为您提供所需的行为.