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

时间:2011-07-31 18:52:12

标签: c++ arrays pointers polymorphism

  

可能重复:
  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和怪物都来自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;
}

10 个答案:

答案 0 :(得分:23)

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

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

Base* b = new Derived;

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

Base myBases[5];

myBases的静态类型为Base[5],是一个包含五个Base的数组。

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

的内容
Base* b = new Derived;

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

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

  1. 对象的赋值始终基于对象的静态类型,而不是动态类型。
  2. 如果静态类型是指针或引用,则虚拟函数的调用仅调度到动态类型。
  3. 让我们依次解决这些问题。

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

    Ninja ninja;
    Monster monster;
    
    Enemy enemies[2];
    
    enemies[0] = monster;
    enemies[1] = ninja;
    

    让我们来看看这里发生的事情。这首先创建一个新的NinjaMonster对象,然后创建一个Enemy个对象的数组,最后为enemies数组赋值ninja和{ {1}}。

    此代码的问题在于您编写

    monster

    lhs的静态类型是enemies[0] = monster; ,rhs的静态类型是Enemy。在确定如何进行赋值时,C ++只查看对象的静态类型,而不是动态类型。这意味着因为Monster被静态类型化为enemies[0],所以它必须保存精确类型为Enemy的内容,而不是任何派生类型。这意味着,当您执行上述分配时,C ++会将此解释为“获取Enemy对象,仅识别monster的部分,然后将该部分复制到Enemy “。换句话说,虽然enemies[0]是一个额外添加的Monster,但只有Enemy的{​​{1}}部分会被复制到Enemy这一行代码这称为切片,因为您正在切掉对象的一部分并留下Monster基本部分。

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

    enemies[0]

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

    Enemy

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

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

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

    monster

    写下

    Enemy*

    然后因为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*的子类型。因此,C ++将函数调度到对象的动态类型。

    希望这有帮助!

答案 1 :(得分:16)

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

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

答案 2 :(得分:3)

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

Enemy()创建一个敌人对象。如果您要调用Enemy()。attack(),它将不会打印任何内容,因为该方法为空。

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

答案 3 :(得分:1)

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

答案 4 :(得分:1)

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

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

在另一种情况下,你有实际的敌人对象。当您指定 Ninja Monster 对象时,只会复制公共成员(其他不属于Enemy的成员将会丢失)。<登记/> 然后,当你打电话给attack()时,它将是 Enemy 攻击()(因为它们是 Enemy 对象)

答案 5 :(得分:1)

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

如何处理可能包含其他数据的派生类?它没有。

另一方面,根据指示,它根本不重要。指针将指向“某事物”(vtable加上数据),虚拟继承机制以某种方式计算出什么是什么以及在哪里。可能有相同的功能,重载的功能,附加的数据字段,它仍然可以工作。

答案 6 :(得分:1)

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

如果你注意到,你在Enemy中宣称你的攻击功能是虚拟的。这允许什么在多态性中是必不可少的。通过将该函数声明为虚拟,您允许您的敌人的子类对象(例如,Monster和Ninja)在运行时确定在使用敌人指针时使用哪个版本的函数攻击。这允许您使用通用的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.

答案 7 :(得分:1)

写作

enemies[0] = monster;

您正在将Monster对象转换为Enemy对象。每个派生类对象都可以自动转换为基类对象。这称为对象切片。一旦发生了转换,Enemy对象就不再有任何方法可以记住它曾经曾经是一个怪物对象,它只是一个简单的敌人对象,就像任何其他对象一样。所以当你召唤攻击时,你会召唤Enemy :: attack。

Java中不会出现此问题,因为在Java中,所有内容都是自动指针。

答案 8 :(得分:1)

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

简短版本:父类的实例可能具有比子类实例少的状态,因此对父类实例的操作必须假定它们是为父类定义的那些。指针消除了这一点,因为确实存在具有完整状态的子类实例。

答案 9 :(得分:1)

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