2016-12-13 8 views
16
#include <iostream> 

using namespace std; 

struct Base 
{ 
    virtual ~Base() 
    { 
     cout << "~Base(): " << b << endl; 
    } 

    int b = 1; 
}; 

struct Derived : Base 
{ 
    ~Derived() override 
    { 
     cout << "~Derived(): " << d << endl; 
    } 

    int d = 2; 
}; 

int main() 
{ 
    Base* p = new Derived[4]; 
    delete[] p; 
} 

Выход в follws: (Visual Studio 2015 с Clang 3.8)Почему полиморфизм не применяется к массивам на C++?

~Base(): 1 
~Base(): 2 
~Base(): -2071674928 
~Base(): 1 

Почему полиморфизм не распространяется на массивы в C++?

+8

Полиморфизм работает только через указатели, а элементы массива не являются указателями, они являются типами значений. Вам понадобится массив указателей и их удаление по отдельности. (или вектор интеллектуальных указателей) – Galik

+2

Я согласен с комментарием @Galik, но он отлично работает для меня, правильное полиморфное поведение. sh-4.2 $ g ++ -std = C++ 11 -o main * .cpp ~ Производный(): 2 ~ Base(): 1 ~ Производный(): 2 ~ Base(): 1 ~ Производный(): 2 ~ Base(): 1 ~ Derived(): 2 ~ Base(): 1 – instance

+0

Это не будет работать, поскольку массивы используют указатель aritmetic для доступа к массивам, которые в вашем случае являются sizeof (Base), с другой стороны, один элемент в вашем array - sizeof (Derived), поэтому при доступе к массиву el вы попадете в неправильный адрес. – user3655463

ответ

14

Вы получаете неопределенное поведение, потому что оператор delete[] не имеет представления о том, какой объект хранится в массиве, поэтому он доверяет статическому типу, чтобы принять решение о смещениях отдельных объектов. В стандарте указано следующее:

Во втором альтернативе (удалить массив), если динамический тип подлежащего удалению объекта отличается от его статического типа, поведение не определено.

статический тип массива должен совпадать с типом элемента, который используется для выделения:

Derived* p = new Derived[4]; // Obviously, this works 

This Q&A переходит в более подробную информацию о причине, почему стандарт это требование.

Чтобы исправить это поведение, вам необходимо создать массив указателей, регулярных или интеллектуальных (желательно умный, чтобы упростить управление памятью).

+1

Фактически это выражение «удалить массив», основанное на статическом типе и динамическом типе. 'operator delete []' это что-то еще (хотя и что-то другое, которое используется выражением delete). – Peter

+1

Или вектор умных указателей, чтобы упростить его еще больше. – immibis

20

Учитывая,

Base* p = Derived[4]; 

C++ 11 Стандарт с делает

delete [] p; 

быть неопределенное поведение.

5.3.5 Удалить

...

2 ... Во второй альтернативе (удалить массив), если динамический тип объекта, который будет удален, отличается от его статического типа , поведение не определено.

С точки зрения макета памяти, также имеет смысл, почему delete [] p; приведет к неопределенному поведению.

sizeof(Derived) Если это N, new Derived[4] выделяет память, которая будет что-то вроде:

+--------+--------+--------+--------+ 
| N | N | N | N | 
+--------+--------+--------+--------+ 

В общем, sizeof(Base) < sizeof(Derived) =. В вашем случае sizeof(Base) < sizeof(Derived) с Derived имеет дополнительную переменную-член.

При использовании:

Base* p = new Derived[4]; 

у вас есть:

p 
| 
V 
+--------+--------+--------+--------+ 
| N | N | N | N | 
+--------+--------+--------+--------+ 

p+1 указывает на где-то в середине первого объекта после sizeof(Base) < sizeof(Derived).

 p+1 
     | 
     V 
+--------+--------+--------+--------+ 
| N | N | N | N | 
+--------+--------+--------+--------+ 

Когда деструктор вызывается p+1, указатель не указывает на начало объекта. Следовательно, в программе проявляются симптомы неопределенного поведения.


Проблема, связанная

Из-за различий в размерах и BaseDerived, вы не можете перебирать элементы динамически распределяемой массива с помощью p.

for (int i = 0; i < 4; ++i) 
{ 
    // Do something with p[i] 
    // will not work since p+i does not necessary point to an object 
    // boundary. 
} 
+0

приятное и понятное объяснение. – instance

+1

С точки зрения дизайна я не вижу, почему удаление не может использовать информацию из указателя массива. В Fortran полиморфные массивы завершены просто отлично. –

+1

@ VladimirF, у меня нет ответа на это. –

2

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

Полиморфизм может только Работаем, когда речь идет о соотношении класса и подкласса. Поэтому, когда мы имеем ситуацию, как вы закодировали мы имеем:

enter image description here

Мы говорим «Все экземпляры Derived также случаи Base». Обратите внимание, что это должно содержать или мы даже не можем даже говорить о полиморфизме. В этом случае он выполняется и, следовательно, мы можем использовать указатель на Derived, где код ожидает указатель на Base.

Но тогда вы пытаетесь сделать что-то другое:

enter image description here

И здесь у нас есть проблемы. Хотя в теории множеств можно сказать, что набор подкласса также является набором суперкласса, в программировании это неверно. Проблема несколько возрастает из-за того, что разница - «всего два символа». Но массив элементов в некотором роде совершенно другой, чем любой из этих элементов.

Может быть, если вы переписали код, используя std::array шаблон, который станет более ясным:

#include <iostream> 
#include <array> 

struct Base 
{ 
    virtual ~Base() 
    { 
     std::cout << "~Base()" << std::endl; 
    } 
}; 

struct Derived : Base 
{ 
    ~Derived() override 
    { 
     std::cout << "~Derived()" << std::endl; 
    } 
}; 

int main() 
{ 
    std::array<Base, 4>* p = new std::array<Derived, 4>; 
    delete[] p; 
} 

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

Надеюсь, это поможет некоторым людям, которые хотят более интуитивно понятное понимание происходящего, а не техническое объяснение.

4

Это связано с концепциями covariance and contravariance. Я приведу пример, который, надеюсь, прояснит ситуацию.

Начнем с простой иерархии. Предположим, мы имеем дело с созданием и уничтожением объектов в реальном мире. У нас есть 3D-объекты (например, деревянные блоки) и 2D-объекты (например, листы бумаги). 2D-объект можно рассматривать как трехмерный объект, за исключением незначительной высоты. Это правильное направление подкласса, поскольку обратное неверно; 2D-объекты можно сложить поверх друг друга, что очень сложно в лучшем случае сделать с любыми 3D-объектами.

Что-то, что производит объекты, такие как принтер, является covariant. Предположим, ваш друг хочет заимствовать 3D-принтер, взять десять объектов, которые он производит, и вставить их в коробку. Вы можете дать им 2D-принтер; он будет печатать десять страниц, а ваш друг будет вставлять их в коробку. Однако, если этот друг захотел взять десять 2D-объектов и вставить их в папку, вы не могли бы дать им 3D-принтер.

Что-то, что потребляет предметы, такие как измельчитель, это contravariant. У вашего друга есть папка со страницами, которые она печатала, но она не продавалась, поэтому он хочет ее испортить. Вы можете дать им 3D-измельчитель, например, промышленные, используемые для измельчения таких вещей, как автомобили, и он будет работать нормально; кормление страниц не вызывает никаких трудностей. С другой стороны, если бы он хотел уничтожить коробку предметов, которые он получил раньше, вы не могли бы дать ему 2D-измельчитель, так как объекты могут не вписаться в слот.

Массивы: virtualant; они оба потребляют объекты (с назначением) и производят объекты (с доступом к массиву). В качестве примера такого отношения возьмите какой-нибудь факсимильный аппарат. Если вашему другу нужен факсимильный аппарат, ему нужен точный вид, который они запрашивают; они не могут иметь супер- или подкласс, поскольку вы не можете вставлять 3D-объекты в слот для 2D-бумаги, и вы не можете привязывать объекты, созданные 3D-факсимильным аппаратом, в книгу.