Как удалить динамический массив c
Перейти к содержимому

Как удалить динамический массив c

  • автор:

Почему в С++ массивы нужно удалять через delete[]

Заметка рассчитана на начинающих C++ программистов, которым стало интересно, почему везде твердят, что нужно использовать delete[] для массивов, но вместо внятного объяснения – просто прикрываются магическим «undefined behavior». Немного кода, несколько картинок и взгляд под капот компиляторов – всех заинтересованных прошу под кат.

delete_or_delete_for_array_ru/image1.png

Введение

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

int *p = new SomeClass[42]; // Указываем количество delete[] p; // Не указываем количество

Это что, магия? Отчасти – да. Причём разработчики различных компиляторов видят и реализуют её по-разному.

delete_or_delete_for_array_ru/image2.png

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

  • Запись количества элементов перед самим массивом («Over-Allocation»)
  • Хранение количества элементов в обособленном ассоциативном контейнере («Associative Array»)

Over-Allocation

Первый способ, как понятно из названия, реализуется простой записью количества элементов перед массивом. Обратите внимание, что в таком случае указатель, который вы получите после выполнения оператора new, будет указывать на первый элемент массива, а не на его фактическое начало.

delete_or_delete_for_array_ru/image3.png

Такой указатель ни в коем случае нельзя передавать обычному оператору delete. Скорее всего, он просто удалит первый элемент массива, а остальные оставит нетронутыми. Заметьте, я не просто так написал «скорее всего» – ведь никто не может гарантировать, что произойдёт на самом деле и как дальше будет вести себя ваша программа. Всё зависит от того, какие объекты находились в массиве и делали ли они что-то важное в своих деструкторах. То есть получаем классическое неопределённое поведение. Согласитесь, это не то, чего вы ожидаете при попытке удалить массив.

Интересный факт: в большинстве реализаций стандартной библиотеки, оператор delete внутри себя просто вызывает функцию free. В случае передачи в неё указателя на массив мы получаем ещё одно неопределённое поведение. Это происходит из-за того, что на входе эта функция ожидает указатель, полученный в результате работы функций calloc, malloc или realloc. А как мы выяснили выше, этого не происходит из-за скрытия переменной в начале массива и сдвига указателя на начало массива.

Чем же отличается оператор delete[]? А он как раз считывает количество элементов в массиве, вызывает деструктор для каждого объекта и уже после этого очищает память (вместе со скрытой переменной).

Если кому будет интересно, то примерно в такой псевдокод превращается конструкция delete[] p; при использовании этой стратегии:

// Получаем количество элементов в массиве size_t n = * (size_t*) ((char*)p - sizeof(size_t)); // Для каждого из них вызываем деструктор while (n-- != 0) < p[n].~SomeClass(); >// И наконец подчищаем память operator delete[] ((char*)p - sizeof(size_t));

Этим способом пользуются компиляторы MSVC, GCC и Clang. В этом можно убедиться, взглянув на код работы с памятью в соответствующих репозиториях (GCC и Clang) или воспользовавшись сервисом Compiler Explorer.

delete_or_delete_for_array_ru/image4.png

Как видно на изображении выше (верхняя часть – код, нижняя – ассемблерный вывод компилятора), я набросал простенький код, в котором объявлена структура и функция для создания массива этих самых структур.

Примечание: пустой деструктор у структуры – это отнюдь не лишний код. Дело в том, что согласно Itanium CXX ABI, для массивов, состоящих из типов с тривиальным деструктором, компилятор должен использовать другой подход к управлению памятью. На самом деле, требований немного больше, и всех их можно посмотреть в разделе 2.7 «Array Operator new Cookies» Itanium CXX ABI. Там же перечислены требования к тому, где и как должна располагаться информация о количестве элементов в массиве.

Что же происходит с точки зрения ассемблера простым языком:

  • cтрока N3: запись требуемого количества памяти (20 байт на 5 объектов + 8 байт на размер массива) в регистр;
  • cтрока N4: вызов оператора new для выделения памяти;
  • cтрока N5: запись количества элементов в начало выделенной памяти;
  • cтрока N6: смещение указателя на начало массива на sizeof(size_t), полученный результат является возвращаемым значением.

К достоинствам этого способа можно отнести его лёгкость в реализации и скорость работы, ну а к недостаткам – то, что он не прощает ошибок с некорректным выбором оператора delete. В лучшем случае – сразу получите падение программы с ошибкой «Heap Corrupt», а в худшем – будете долго и мучительно искать причины странного поведения программы.

Associative Array

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

// Получаем размер массива из скрытого глобального хранилища size_t n = arrayLengthAssociation.lookup(p); // Вызываем деструкторы для каждого элемента while (n-- != 0) < p[n].~SomeClass(); >// Очищаем память operator delete[] (p);

Что ж, выглядит не так «магически», как прошлый вариант. Есть ли ещё какие различия? Да.

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

Данный подход использовался в компиляторе Cfront. Останавливаться на его реализации мы не будем, но если кому интересно покопаться во внутренностях одного из первых C++ компиляторов, то сделать это можно на GitHub.

Мини-послесловие

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

  • Использовать семейства функций std::make_*. Например: std::make_unique, std::make_shared.
  • Использовать средства статического анализа для раннего выявления ошибок, например PVS-Studio. ��

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

  • PVS-Studio. Лекция 11. Неопределённое поведение, или как выстрелить себе в ногу
  • Что каждый программист на C должен знать об Undefined Behavior. Часть 1/3
  • Что каждый программист на C должен знать об Undefined Behavior. Часть 2/3
  • Что каждый программист на C должен знать об Undefined Behavior. Часть 3/3
  • Блог компании PVS-Studio
  • Программирование
  • C++

Как удалить динамический массив c

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

int *numbers ; // динамический массив из 4 чисел // или так // int *numbers = new int[4];

Причем в этом случае оператор new также возвращает указатель на объект типа int — первый элемент в созданном массиве.

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

int *numbers1 >; // массив состоит из чисел 0, 0, 0, 0 int *numbers2 >; // массив состоит из чисел 1, 2, 3, 4 int *numbers3 >; // массив состоит из чисел 1, 2, 0, 0 // аналогичные определения массивов // int *numbers1 = new int[4]<>; // массив состоит из чисел 0, 0, 0, 0 // int *numbers1 = new int[4](); // массив состоит из чисел 0, 0, 0, 0 // int *numbers2 = new int[4]< 1, 2, 3, 4 >; // массив состоит из чисел 1, 2, 3, 4 // int *numbers3 = new int[4]< 1, 2 >; // массив состоит из чисел 1, 2, 0, 0

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

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

int *numbers >; // массив состоит из чисел 1, 2, 3, 4

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

int *numbers >; // получение элементов через синтаксис массивов std::cout 

Причем для доступа к элементам динамического массива можно использовать как синтаксис массивов ( numbers[0] ), так и операцию разыменования ( *numbers )

Соответственно для перебора такого массива можно использовать различные способы:

unsigned n< 5 >; // размер массива int* p < new int[n] < 1, 2, 3, 4, 5 >>; // используем индексы for (unsigned i<>; i < n; i++) < std::cout std::cout ; i < n; i++) < std::cout std::cout ; q != p + n; q++) < std::cout std::cout 

Обратите внимание, что для задания размера динамического массива мы можем применять обычную переменную, а не константу, как в случае со стандартными массивами.

Для удаления динамического массива и освобождения его памяти применяется специальная форма оператора delete :

delete [] указатель_на_динамический_массив;

#include int main() < unsigned n< 5 >; // размер массива int* p < new int[n] < 1, 2, 3, 4, 5 >>; // используем индексы for (unsigned i<>; i < n; i++) < std::cout std::cout

Чтобы после освобождения памяти указатель не хранил старый адрес, также рекомендуется обнулить его:

delete [] p; p = nullptr; // обнуляем указатель

Многомерные массивы

Также мы можем создавать многомерные динамические массивы. Рассмотрим на примере двухмерных массивов. Что такое по сути двухмерный массив? Это набор массив массивов. Соответственно, чтобы создать динамический двухмерный массив, нам надо создать общий динамический массив указателей, а затем его элементы - вложенные динамические массивы. В общем случае это выглядит так:

#include int main() < unsigned rows = 3; // количество строк unsigned columns = 2; // количество столбцов int** numbers>; // выделяем память под двухмерный массив // выделяем память для вложенных массивов for (unsigned i<>; i < rows; i++) < numbers[i] = new int[columns]<>; > // удаление массивов for (unsigned i<>; i < rows; i++) < delete[] numbers[i]; >delete[] numbers; >

Вначале выделяем память для массива указателей (условно таблицы):

int** numbers>;

Затем в цикле выделяем память для каждого отдельного массива (условно строки таблицы):

numbers[i] = new int[columns]<>;

Освобождение памяти идет в обратном порядке - сначала освобождаем память для каждого отдельного вложенного массива, а затем для всего массива указателей.

Пример с вводом и выводом данных двухмерного динамического массива:

#include int main() < unsigned rows = 3; // количество строк unsigned columns = 2; // количество столбцов int** numbers>; // выделяем память под двухмерный массив for (unsigned i<>; i < rows; i++) < numbers[i] = new int[columns]<>; > // вводим данные для таблицы rows x columns for (unsigned i<>; i < rows; i++) < std::cout ; j < columns; j++) < std::cout > numbers[i][j]; > > // вывод данных for (unsigned i<>; i < rows; i++) < // выводим данные столбцов i-й строки for (unsigned j<>; j < columns; j++) < std::cout std::cout for (unsigned i<>; i < rows; i++) < delete[] numbers[i]; >delete[] numbers; >

Пример работы программы:

Enter data for 1 row 1 column: 2 2 column: 3 Enter data for 2 row 1 column: 4 2 column: 5 Enter data for 3 row 1 column: 6 2 column: 7 2 3 4 5 6 7

Указатель на массив

От типа int** , который представляет указатель на указатель (pointer-to-pointer) следует отличать ситуацию "указатель на массив" (pointer to array). Например:

#include int main() < unsigned n; // количество строк int (*a)[2] = new int[n][2]; int k<>; // устанавливаем значения for (unsigned i<>; i < n; i++) < // устанавливаем данные для столбцов i-й строки for (unsigned j<>; j < 2; j++) < a[i][j] = ++k; >> // вывод данных for (unsigned i<>; i < n; i++) < // выводим данные столбцов i-й строки for (unsigned j<>; j < 2; j++) < std::cout std::cout // удаляем данные delete[] a; a = nullptr; >

Здесь запись int (*a)[2] представляет указатель на массив из двух элементов типа int. Фактически мы можем работать с этим объектом как с двухмерным массивом (таблицей), только количество столбцов в данном случае фиксировано - 2. И память для такого массива выделяется один раз:

int (*a)[2] = new int[n][2];

То есть в данном случае мы имеем дело с таблице из n строк и 2 столцов. Используя два индекса (для строки и столца), можно обращаться к определенному элементу, установить или получить его значение. Консольный вывод данной программы:

1 2 3 4 5 6

C++. Правильное удаление динамического двумерного массива

Всем привет. Я новичок в программировании и только начинаю многое осваивать(хотя и имею кое-какую подготовку, но считать её хоть сколь-либо серьезной смешно). Знакомлюсь с классами, пытаясь соорудить нечто для работы с матрицами. Код реализации не важен, важно то, что получаю ошибку: http://prntscr.com/e6y5on Ключевой вопрос в том, как её исправить. Matrix.h:

#ifndef MATRIX_H #define MATRIX_H class Matrix < private: int m_rows = 1; int m_cols = 1; int** m_matrix = new int* [m_rows]; public: Matrix(int rows, int cols); void SetSize(int rows, int cols); void Create(); int rows(); int cols(); ~Matrix(); >; #endif 
#include #include #include "matrix.h" using namespace std; Matrix::Matrix(int rows, int cols) < SetSize(rows, cols); Create(); >void Matrix::SetSize(int rows, int cols) < m_rows = rows; m_cols = cols; >void Matrix::Create() < for (int i = 0; i < m_rows; i++) m_matrix[i] = new int[m_cols]; cout int Matrix::rows() < return m_rows; >int Matrix::cols() < return m_cols; >Matrix::~Matrix()
int main()

При пошаговой проверке в Visual Studio программа сыплется на удалении внешнего указателя delete[] m_matrix; Собственно, прошу помочь =)

Отслеживать
44.8k 3 3 золотых знака 38 38 серебряных знаков 89 89 бронзовых знаков
задан 10 фев 2017 в 13:08
43 1 1 золотой знак 1 1 серебряный знак 3 3 бронзовых знака
Было бы лучше, если бы вы добавили текст ошибки в вопрос не как ссылку
10 фев 2017 в 13:23
Буду иметь ввиду. Новичок на stackoverflow)
10 фев 2017 в 16:20

5 ответов 5

Сортировка: Сброс на вариант по умолчанию

int m_rows = 1; int m_cols = 1; int** m_matrix = new int* [m_rows]; 

Итак, у вас сразу выделяется память и создается матрица 1x1. Поскольку в конструкторе

Matrix::Matrix(int rows, int cols) 

у вас нет инициализации членов, они инициализированы по умолчанию этой матрицей.
Но что вы делаете дальше? Вы переписываете m_rows и m_cols , и в уже выделенный массив для ОДНОГО указателя записываете их m_rows , т.е. в данном конкретном случае - два.

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

Вот как по-хорошему должна выглядеть ваша матрица:

class Matrix < private: int m_rows; int m_cols; int** m_matrix; public: Matrix(int rows, int cols); void SetSize(int rows, int cols); void Create(); int rows(); int cols(); ~Matrix(); >; Matrix::Matrix(int rows, int cols) :m_rows(rows),m_cols(cols) < m_matrix = new int*[m_rows]; for (int i = 0; i < m_rows; i++) m_matrix[i] = new int[m_cols]; cout Matrix::~Matrix()

C++ Динамический массив. Удаление элемента

P.S.
То, что ты написал - это не С++, а просто С.

#3
16:05, 28 янв 2008

Почему нет? Можно удалить элемент, потом вручную сдвинуть все элементы с удаленной позиции на единицу. Примерно это же и будет делать std::vector, но удалять из середины массива (вектора) есть вселенское зло.

#4
16:18, 28 янв 2008

mirat
> Почему нет?
Потому, что исходный вопрос автора был "можно мне как-нибудь так хитро вызвать delete, чтобы массив сам сделал всё как надо ?"
И с этой позиции вариант
> Можно удалить элемент, потом вручную сдвинуть все элементы с удаленной позиции на единицу.
не катит.
Вот вектор - да, целостная сущность, обладающая нужным функционалом.

#5
16:30, 28 янв 2008

по поводу
стоимость операции удаления буде линейно зависить от кол-ва элементов?

#6
16:59, 28 янв 2008

HolyDel
Из середины да, из конца - константное время.

Nikopol
Не телепат, не знаю, что автор имел в виду :)

#7
17:03, 28 янв 2008

Все понятно. Всем спасибо

#8
17:17, 28 янв 2008

если удалять, то уж лучше std::list использовать.

#9
17:33, 28 янв 2008

замечу только что если порядок элементов в массиве не важен то делать своп с последним элементом + pop_back => константное время удаления

  • TaurenChief
  • Постоялец

#10
17:42, 28 янв 2008
#11
17:52, 28 янв 2008
#12
18:24, 28 янв 2008

JokerR
>замечу только что если порядок элементов в массиве не важен то делать своп с последним элементом + pop_back => константное время удаления

Но размер вектора не уменьшится (резервированный в памяти)

#13
19:18, 28 янв 2008

О. Федор
А что должен? Он у вектора не уменьшится в любом случае, кроме хака со swap'ом

#14
19:21, 28 янв 2008

вообще-то это implementation-depended

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *