Записная книжка разработчика

Мои проекты

Проблемы с Delete[]?

| Comments

Недавно в блоге Алёны (http://alenacpp.blogspot.com/2010/06/delete.html) был опубликован пост "Проблемы с delete[]", где был приведён пример вот такого кода:

A* a = new B[T];
delete[] a;

представляющего собой отрывок более длинного кода, приведённого в другом блоге (под девизом "Программирование - это просто!"): http://easy-coding.blogspot.com/2010/06/delete.html.
Адепты, достигшие просветления, уже поняли, что это работать не будет, а остальные могут читать дальше, и убедиться в том, что программирование - это не всегда просто.

Вот код из начального поста:

Листинг 1.

#define T 2
class A {
  public:
    virtual ~A() {
      p = 0;
    }
    int p;
  };
class B: public A {
  int a;
};
int main() {
  A* a = new B[T];
  delete[] a;
  return 0;
}

При запуске программа падает практически на любом компиляторе, кроме VisualStudio (и некоторых других).

Давайте разбёремся в том, что здесь неправильного, почему программа падает, и, самое главное, в том, почему она кое-где НЕ падает (хотя должна).

Примечание: в комментариях к блогу Алёны некто soonts пишет:

Как обычно, нормальные ОС + IDE этой проблемой не затронуты: в мире Windows всё работает.

Вот вам и объяснение: программа работает исключительно благодаря магической силе MS Windows и лично Билла Гейтса. Я надеюсь, что среди моих читателей не будет столь малообразованных молодых людей, которые делают такие обобщения, не разобравшись в сути вопроса.

Итак, давайте по порядку.

1. Что здесь не так?

Здесь не так то, что мы создаём массив объектов класса А, а фактически записываем в него объекты класса В:

A* a = new B[T];

При этом В > А по размеру, так как в В добавлена переменная-член int а, но компилятор записывает объекты класса В по адресам, вычисленным исходя из размера объекта класса А (так как массив объявлен как A*a). То есть проблема вовсе не с delete[] a, проблема именно с A* a = new B[T]. Давайте посмотрим, что происходит. Для этого перепишем код, переименовав для большего удобства некоторые переменные и увеличив массив до 5 элементов (пока делаем "правильно", создавая массив объектов класса А: A* a = new A[5]):

Листинг 2.
#include "stdafx.h"
#include
  static int i = 0; //вспомогательная переменная
class A {
  public:
    A(){foo = i++;}; //"нумеруем" объекты при их создании
    virtual ~A() {
      foo = 0;
    }
    int foo;
};
class B: public A {
  public:
    int bar;
};
int _tmain(int argc, _TCHAR* argv[])
{
  using namespace std;
  int* pc; //вспомогательный указатель
  A* a = new A[5];
  //выводим в консоль содержимое памяти, с начала массива а
  pc = (int*)a;
  hex(cout);
  for(int i = 0; i < 20; i++)
    cout << *(pc + i) << ' ';
  system("PAUSE");
  return 0;
}

Запускаем программу в Microsoft Visual C++ 2010.

Вывод программы:

41783c 0 41783c 1 41783c 2 41783c 3 41783c 4 fdfdfdfd abababab abababab 0 0 d000
a 1c0710 347a68 345430 104822b0 Для продолжения нажмите любую клавишу . . .

Здесь:
41783c - адрес таблицы виртуальных функций a[0]
0 - значение a[0].foo
41783c - адрес таблицы виртуальных функций a[1]
1 - значение a[1].foo
...
41783c - адрес таблицы виртуальных функций a[4]
4 - значение a[4].foo
дальше в памяти идёт "мусор"

Пока всё нормально и логично. Теперь заменим строку A* a = new A[5]; на A* a = new В[5], и посмотрим, что будет:

41783c 0 cdcdcdcd 41783c 1 cdcdcdcd 41783c 2 cdcdcdcd 41783c 3 cdcdcdcd 41783c 4
cdcdcdcd fdfdfdfd abababab abababab feeefeee 0 Для продолжения нажмите любую кл
авишу . . .

41783c - адрес таблицы виртуальных функций a[0]
0 - значение foo
cdcdcdcd - значение bar
41783c - адрес таблицы виртуальных функций a[1]
1 - значение a[1].foo
cdcdcdcd - значение bar
...
41783c - адрес таблицы виртуальных функций a[4]
4 - значение a[4].foo
cdcdcdcd - значение bar

дальше в памяти идёт "мусор" (тот же самый, что и в прошлый раз, что наводит на мысль, что это вовсе не мусор).

2. Почему она падает?

Проделаем ещё один эксперимент, удалим конструктор класса А (в исходной задаче он и был без конструктора), и поместим в main() цикл, в котором нумеруютя поля foo объектов:

Листинг 3.

#include "stdafx.h"
#include
  static int i = 0;
class A {
  public:
    virtual ~A() {
      foo = 0;
    }
  int foo;
};
class B: public A {
  public:
    int bar;
};
int _tmain(int argc, _TCHAR* argv[])
{
  using namespace std;
  int* pc;
  A* a = new B[5];
  for(int i = 0; i < 5; ссi++)
    a[i].foo = i;
  pc = (int*)a;
  hex(cout);
  for(int i = 0; i < 20; i++)
    cout << *(pc + i) << ' ';
  system("PAUSE");
  return 0;
}
Результат:
41783c 0 cdcdcdcd 1 cdcdcdcd 2 41783c 3 cdcdcdcd 4 cdcdcdcd cdcdcdcd 41783c cdcd
cdcd cdcdcdcd fdfdfdfd abababab abababab feeefeee 0 Для продолжения нажмите любу
ю клавишу . . .

Без вызова конструктора класса А произошла настоящая катастрофа: адреса виртуальных таблиц объектов произвольным образом затёрты полями bar (принявшими, по прихоти компилятора, значение cdcdcdcd).

Теперь понятно, что вызов любого виртуального метода приведёт к немедленному падению программы. Добавим в класс А виртуальный метод и попытаемся вызвать его из main():

Листинг 4.
#include "stdafx.h"
#include
  static int i = 0;
class A {
  public:
    virtual void DoSomething() // <--- добавляем виртуальный метод
    {};
    virtual ~A() {
      foo = 0;
    }
  int foo;
};
class B: public A {
  public:
    int bar;
};
int _tmain(int argc, _TCHAR* argv[])
{
  using namespace std;
  int* pc;
  A* a = new B[5];
  for(int i = 0; i < 5; i++)
    a[i].DoSomething(); // <--- вызываем виртуальный метод
  pc = (int*)a;
  hex(cout);
  for(int i = 0; i < 20; i++)
    cout << *(pc + i) << ' ';
  system("PAUSE");
  return 0;
}

Результат:

Вот так. A* a = new B[5] - это неверно, вне зависимости от того, под Windows мы работаем или нет.

Но delete[] же работает! Внутри delete[] вызываются деструкторы объектов, и правильно вызываются, я проверял! Как это возможно при затёртых адресах таблиц виртуальных функций? Ведь деструктор тоже виртуальный. его адрес хранится в той же таблице!

Во-первых, не всё так радужно и с деструкторами. Если мы добавляем виртуальный деструктор к объекту В, то программа рушится, как ей и положено.

3. Почему она НЕ падает?

А теперь мы подобрались к самой сути вопроса: почему не падает программа, приведённая на листинге 1. Посмотрим ещё раз на результат работы программы на листинге 3:

41783c 0 cdcdcdcd 1 cdcdcdcd 2 41783c 3 cdcdcdcd 4 cdcdcdcd cdcdcdcd 41783c cdcd
cdcd cdcdcdcd fdfdfdfd abababab abababab feeefeee 0

Как он образуется? Сначала в памяти размещаются объекты типа В:

41783c cdcdcdcd cdcdcdcd 41783c cdcdcdcd cdcdcdcd 41783c cdcdcdcd cdcdcdcd 41783c cdcdcdcd cdcdcdcd 41783c cdcdcdcd cdcdcdcd

где 41783c - адрес виртуальной таблицы В
cdcdcdcd - поле foo
сdcdcdcd - поле bar

После этого код

for(int i = 0; i < 5; ссi++)
a[i].foo = i;

переписывает поля foo по адресам, соответствующим класс А, а не В:

41783c 0 cdcdcdcd 1 cdcdcdcd 2 41783c 3 cdcdcdcd 4 cdcdcdcd cdcdcdcd 41783c cdcdcdcd cdcdcdcd

И, так как А меньше В по размеру, "хвост" массива оказался неиспорчен, и адрес виртуальной таблицы последнего объекта а[4] оказался цел и невридим

41783c 0 cdcdcdcd 1 cdcdcdcd 2 41783c 3 cdcdcdcd 4 cdcdcdcd cdcdcdcd 41783c cdcdcdcd cdcdcdcd

А уничтожение массива оператор delete[] начинает с конца!

Перепишем программу так, чтобы видеть память после каждого вызова деструктора ~A()

Листинг 5

// Console WIN32.cpp: определяет точку входа для консольного приложения.
//
#include "stdafx.h"
#include
using namespace std;
static int i = 0;
static int* pc;
class A {
  public:
    virtual ~A() {
      foo = 0;
      for(int i = 0; i < 20; i++)
        cout << *(pc + i) << ' ';
      cout << endl << endl;
    }
    int foo;
};
class B: public A {
  public:
  int bar;
};
int _tmain(int argc, _TCHAR* argv[])
{
  A* a = new B[5];
  for(int i = 0; i < 5; i++) // <--- намеренно "портим" память
    a[i].foo = i;
  pc = (int*)a;
  hex(cout);
  for(int i = 0; i < 20; i++)
    cout << *(pc + i) << ' ';
  cout << endl << endl;
  delete[] a;
  for(int i = 0; i < 20; i++)
    cout << *(pc + i) << ' ';
  cout << endl << endl;
  system("PAUSE");
  return 0;
}

Результат (для удобства чтения выровнен и снабжён комментариями)

41783c 0 cdcdcdcd 1 cdcdcdcd 2 41783c 3 cdcdcdcd 4 cdcdcdcd cdcdcdcd 41783c cdcdcdcd cdcdcdcd // до вызова delete[]
41783c 0 cdcdcdcd 1 cdcdcdcd 2 41783c 3 cdcdcdcd 4 cdcdcdcd cdcdcdcd 417848 0 cdcdcdcd // вызов деструктора ~A[4]
41783c 0 cdcdcdcd 1 cdcdcdcd 2 41783c 3 cdcdcdcd 417848 0 cdcdcdcd 417848 0 cdcdcdcd// вызов деструктора ~A[3]
41783c 0 cdcdcdcd 1 cdcdcdcd 2 417848 0 cdcdcdcd 417848 0 cdcdcdcd 417848 0 cdcdcdcd // вызов деструктора ~A[2]
41783c 0 cdcdcdcd 417848 0 2 417848 0 cdcdcdcd 417848 0 cdcdcdcd 417848 0 cdcdcdcd // вызов деструктора ~A[1]
417848 0 cdcdcdcd 417848 0 2 417848 0 cdcdcdcd 417848 0 cdcdcdcd 417848 0 cdcdcdcd // вызов деструктора ~A[0]
feeefeee feeefeee feeefeee feeefeee feeefeee feeefeee feeefeee feeefeee feeefeee feeefeee feeefeee feeefeee feeefeee feeefeee feeefeee // после delete[]

Здесь хорошо виден механизм вызова деструкторов объектов в массиве: начиная с последненего объекта, вызывается его деструктор, который заменяет содержимое ячейки памяти, соответствующей адресу виртуальной таблицы предыдущего объекта на верный адрес таблицы, и из неё вызывается следующий по счёту деструктор!

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

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

A* a = new B[T];
delete[] a;

будет работать, причём именно в MS Visual C++.

Так писать нельзя! В литературе ясно написано: Arrays are evil. Массивы - зло! Массивы являются злом, но, во-первых, только в С++ (в С им нет альтернативы), во-вторых, их всё же можно применять, но не для объектов с полиморфизмом, в любом случае.

4. А как нужно?

Применяйте контейнеры STL! Но это совсем другая история.