Зачем вам нужны указатели в этой ситуации?

Возможный дубликат:
Изучение С++: полиморфизм и нарезка

Это строит вопрос, который я задал раньше. Классы выглядят следующим образом:

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;
        }
};

Я новичок в С++, и я смущен, почему это будет работать только с указателями (оба ниндзя и монстр происходят от 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

Это отличный вопрос, который попадает в суть некоторых из более сложных точек наследования С++. Путаница возникает из-за разницы между статическими типами и динамическими типами, а также тем, как С++ выделяет хранилище для объектов.

Для начала обсудим разницу между статическими и динамическими типами. Каждый объект в С++ имеет статический тип, который является типом объекта, который описан в исходном коде. Например, если вы попытаетесь написать

Base* b = new Derived;

Тогда статический тип b равен Base*, так как в исходном коде тип, который вы объявили для него. Аналогично, если вы пишете

Base myBases[5];

статическим типом myBases является Base[5], массив из пяти Base s.

Динамический тип объекта - это тип, который фактически имеет объект во время выполнения. Например, если вы пишете что-то вроде

Base* b = new Derived;

Тогда динамический тип b равен Derived*, так как он фактически указывает на объект Derived.

Различие между статическими и динамическими типами важно в С++ по двум причинам:

  • Присвоения объектам всегда основаны на статическом типе объекта, а не на динамическом типе.
  • Вызовы виртуальных функций отправляются только динамическому типу, если статический тип является указателем или ссылкой.

Позвольте адресовать каждый из них по очереди.

Во-первых, одна из проблем со второй версией кода заключается в том, что вы делаете следующее:

Ninja ninja;
Monster monster;

Enemy enemies[2];

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

Проследите, что здесь происходит. Сначала создается новый объект Ninja и Monster, затем создается массив объектов Enemy и, наконец, присваивается массиву enemies значения Ninja и Monster.

Проблема с этим кодом заключается в том, что когда вы пишете

enemies[0] = monster;

Статический тип lhs равен Enemy, а статический тип rhs равен Monster. При определении того, как выполнять назначение, С++ рассматривает только статические типы объектов, а не динамические типы. Это означает, что поскольку enemies[0] статически типизирован как Enemy, он должен содержать что-то точно типа Enemy, никогда никакого производного типа. Это означает, что, когда вы выполняете указанное выше назначение, С++ интерпретирует это как означающее "взять объект 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;

Это совершенно безопасно, потому что в этой строке кода:

enemies[0] = &monster;

Lhs имеет статический тип Enemy*, а rhs имеет тип Monster*. С++ юридически позволяет конвертировать указатель на производный тип в указатель на базовый тип без каких-либо проблем. В результате, указатель rhs Monster можно без потерь преобразовать в тип lhs 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*, он может указывать либо на Enemy, либо на подтип Enemy. Следовательно, С++ отправляет функцию динамическому типу объекта.

Надеюсь, это поможет!

Ответ 2

Без указателей массив ваших врагов [] представляет собой пространство в стеке, достаточное для хранения двух объектов "Враг", что означает сохранение всех их полей (плюс, возможно, накладные расходы для указателя и выравнивания vtable). Производные классы Enemy могут иметь дополнительные поля и поэтому быть больше, поэтому он не позволяет вам хранить производный объект Enemy в пространстве, зарезервированном для реального объекта Enemy. Когда вы выполняете присваивание, как в примере, он использует оператор присваивания (в данном случае, неявно) - который устанавливает значения в полях объекта левой стороны значениям соответствующих полей в объекте правой стороны, оставляя левый тип объекта (и, следовательно, указатель vtable) неизменным. Это называется "срезом объектов" и обычно его следует избегать.

Указатели имеют одинаковый размер, поэтому вы можете поместить указатель на производный объект Enemy в пространство для указателя на Enemy и использовать его так же, как если бы он был указателем на простой объект противника. Поскольку указатель на производный объект указывает на фактический экземпляр производного объекта, вызовы виртуальных функций на указателе будут использовать производный объект vtable и дать вам желаемое поведение.

Ответ 3

В С++ это называется slicing.

Враг() создает объект Enemy. Если вы должны были вызвать Enemy(). Attack(), он ничего не печатал, потому что этот метод пуст.

Единственный способ получить полиморфное поведение на С++ - это использовать указатели или ссылки.

Ответ 4

Использование указателей - как полиморфизм реализован в С++ (см. здесь). Вы получите ошибку несоответствия типа, если попытаетесь поместить объект monster или ninja в массив enemies. Но "указатель на производный класс совместим со шрифтом с указателем на его базовый класс".

Ответ 5

Это даст вам совершенно другой результат.

В первом сценарии с указателями у вас будут указатели Enemy, которые будут указывать на объекты Ninja и Monster. Объекты будут неповрежденными, и во время выполнения вызов attack() вызовет метод object().

В другом сценарии у вас есть реальные объекты Enemy. Когда вы назначите объекты Ninja и Monster, будут скопированы только общие члены (остальные члены, которые не принадлежат Enemy, будут потеряны). < ш > Тогда, когда вы назовете атаку(), это будет атака Enemy (поскольку они являются объектами Enemy)

Ответ 6

Enemy enemies[2]; создает массив объектов конкретного типа (Enemy). Это означает, среди прочего, что все элементы этого массива имеют известный размер.

Как это работает с производными классами, которые могут включать другие данные? Это не так.

С другой стороны, данные указатели, это вообще не имеет значения. Указатель укажет на "что-то" (vtable плюс данные), а механизм виртуального наследования каким-то образом выяснит, что и где. Там могут быть одни и те же функции, перегруженные, дополнительные поля данных, они все равно будут работать.

Ответ 7

Присвоение монстра и ниндзя вашему массиву Enemy будет работать, однако, когда вы вызываете атаку функции на каждом из них, она вызывает функцию атаки базового класса. Зачем? Во-первых, когда вы назначаете объекты в массив Enemy, вы по существу приписываете эти классы, поэтому, когда вы взаимодействуете со своими объектами, они действуют как враг, в отличие от того, кем они изначально были.

Если вы заметили, вы объявили свою функцию атаки в Enemy виртуальной. То, что это позволяет, имеет важное значение в полиморфизме. Объявляя эту функцию виртуальной, вы позволяете подклассу объектов (например, Monster и Ninja) вашего Enemy определять во время выполнения, какую версию атаки функции использовать, если используется указатель Enemy. Это позволяет использовать общий указатель Enemy для доступа к различным объектам подкласса и по-прежнему правильно использовать правильную функцию:

Enemy * ptr;
Enemy copy;
Monster m;

copy = (Enemy)m;
ptr = &m;

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

Ответ 8

Написав

enemies[0] = monster;

вы конвертируете свой объект Monster в объект Enemy. Каждый объект производного класса может быть преобразован автоматически в объект базового класса. Это называется нарезкой объектов. Как только это преобразование произошло, объект Enemy больше не имеет способа запомнить, что он когда-то использовался как объект Monster, он просто простой объект Enemy, как и любой другой. Поэтому, когда вы вызываете атаку, вы вызываете Enemy:: attack.

Эта проблема не возникает в Java, потому что в Java все автоматически указывается.

Ответ 9

Он не поддерживается, потому что, когда вы назначаете экземпляр суперкласса значение экземпляра подкласса, информация подкласса, не входящая в суперкласс, отбирается. Ergo, некоторые методы - даже полиморфные - которые зависят от подкласса, не будут работать во всех ситуациях. Единственный универсальный способ гарантировать безопасность типа во время компиляции - использовать реализацию родительского класса.

Краткая версия: экземпляры родительского класса могут иметь меньше состояний, чем экземпляры дочерних классов, поэтому операции с экземплярами родительского класса должны предполагать, что они определены для родительского класса. Указатели избегают этого, поскольку экземпляры дочерних классов с полным состоянием действительно существуют.

Ответ 10

Потому что это невозможно (возможно, очень сложно и может быть сделано с помощью указателей) для реализации такой функциональности. Основная причина заключается в том, что базовые и производные объекты могут иметь разные размеры (sizeof(Enemy) != sizeof(Monster)) и хранить монстров у врагов, вы просто потеряете некоторые данные.