Какой оператор необходимо вызвать для удаления массива
Перейти к содержимому

Какой оператор необходимо вызвать для удаления массива

  • автор:

ArrayRemove

Удаляет из массива указанное число элементов начиная с указанного индекса.

[in] Индекс, начиная с которого удаляются элементы массива.

[in] Количество удаляемых элементов. Значение WHOLE_ARRAY означает удаление всех элементов с указанного индекса до конца массива.

Возвращает true в случае успеха, иначе false. Чтобы получить информацию об ошибке, необходимо вызвать функцию GetLastError(). Возможные ошибки:

  • 5052 – ERR_SMALL_ARRAY (значение start слишкое большое),
  • 5056 – ERR_SERIES_ARRAY (массив не может быть изменен, индикаторный буфер),
  • 4003 – ERR_INVALID_PARAMETER (значение count слишком большое),
  • 4005 — ERR_STRUCT_WITHOBJECTS_ORCLASS (массив фиксированного размера, который содержит сложные объекты с деструктором),
  • 4006 — ERR_INVALID_ARRAY (массив фиксированного размера, который содержит объекты структур или классов с деструктором).

Если функция используется для массива фиксированного размера, то сам размер массива не меняется: при этом происходит физическое копирование оставшегося «хвоста» в позицию start . Для точного понимания работы функции смотрите пример ниже. «Физическое» копирование означает, что копируемые объекты не создаются с помощью вызова конструктора или оператора копирования, а просто происходит копирование бинарного представления объекта. Именно по этой причине запрещается применять функцию ArrayRemove() к массиву фиксированного размера, содержащего объекты с деструктором (взводится ошибка ERR_INVALID_ARRAY или ERR_STRUCT_WITHOBJECTS_ORCLASS). Так как при удалении такого объекта деструктор должен быть вызван дважды – для первоначального объекта и его копии.

Нельзя удалять элементы из динамических массивов, назначенных в качестве индикаторных буферов функцией SetIndexBuffer(), это приведет к появлению ошибки ERR_SERIES_ARRAY. Для индикаторных буферов все операции по изменению размера производит исполняющая подсистема терминала.

//+——————————————————————+
//| Script program start function |
//+——————————————————————+
void OnStart ()
<
//— объявим массив фиксированного размера и заполним значениями
int array[10];
for ( int i=0;i <10;i++)
<
array[i]=i;
>
//— покажем массив до удаления элементов
Print ( «До вызова ArrayRemove()» );
ArrayPrint (array);
//— удалим 2 элемента из массива и покажем новый состав
ArrayRemove (array,4,2);
Print ( «После вызова ArrayRemove()» );
ArrayPrint (array);
/*
Результат выполнения:
До вызова ArrayRemove()
0 1 2 3 4 5 6 7 8 9
После вызова ArrayRemove()
0 1 2 3 6 7 8 9 8 9
*/

Почему в С++ массивы нужно удалять через 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++

Оператор delete (C++)

Аргумент cast-expression должен быть указателем на блок памяти, ранее выделенный для объекта, созданного с помощью нового оператора. Оператор delete имеет результат типа void и поэтому не возвращает значение. Например:

CDialog* MyDialog = new CDialog; // use MyDialog delete MyDialog; 

Использование delete указателя на объект, не выделенный с new непредсказуемыми результатами. Однако можно использовать delete указатель с значением 0. Эта подготовка означает, что при new возврате 0 при сбое удаление результата неудачной new операции является безвредным. Дополнительные сведения см. в разделе «Новые и удаленные операторы».

delete Операторы new также можно использовать для встроенных типов, включая массивы. Если pointer ссылается на массив, поместите пустые квадратные скобки ( [] ) перед pointer :

int* set = new int[100]; //use set[] delete [] set; 

delete Использование оператора в объекте освобождает память. Программа, которая разыменовывает указатель после удаления объекта, может создать непрогнозируемый результат или вызвать сбой.

Если delete используется для освобождения памяти для объекта класса C++, деструктор объекта вызывается до освобождения памяти объекта (если объект имеет деструктор).

Если операнду оператору delete является изменяемым l-значением, его значение не определено после удаления объекта.

Если указан параметр компилятора /sdl (включение дополнительных проверка безопасности), операнду delete оператору присваивается недопустимое значение после удаления объекта.

Использование оператора delete

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

// expre_Using_delete.cpp struct UDType < >; int main() < // Allocate a user-defined object, UDObject, and an object // of type double on the free store using the // new operator. UDType *UDObject = new UDType; double *dObject = new double; // Delete the two objects. delete UDObject; delete dObject; // Allocate an array of user-defined objects on the // free store using the new operator. UDType (*UDArr)[7] = new UDType[5][7]; // Use the array syntax to delete the array of objects. delete [] UDArr; >

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

Пример

Примеры использования delete см . в разделе «Новый оператор».

Принцип работы delete

Оператор удаления вызывает удаление оператора функции.

Для объектов не типа класса (класса, структуры или объединения) вызывается глобальный оператор удаления. Для объектов типа класса имя функции deallocation разрешается в глобальных область если выражение удаления начинается с унарного оператора разрешения область ( :: ). В противном случае перед освобождением памяти оператор удаления вызывает деструктор объекта (если указатель не имеет значения null). Оператор удаления можно определять отдельно для каждого класса; если для некоторого класса такое определение отсутствует, вызывается глобальный оператор удаления. Если выражение удаления используется для освобождения объекта класса, статический тип которого имеет виртуальный деструктор, функция освобождение разрешается через виртуальный деструктор динамического типа объекта.

new оператор (C++)

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

Синтаксис

new-expression :
:: opt new new-placement opt new-type-id new-initializer opt
:: opt new new-placement opt ( type-id ) new-initializer opt

new-placement :
( expression-list )

new-declarator :
ptr-operator new-declarator необ.
noptr-new-declarator

noptr-new-declarator :
[ expression ] attribute-specifier-seq необ.
noptr-new-declarator [ constant-expression ] attribute-specifier-seq необ.

new-initializer :
( expression-list необ. )
braced-init-list

Замечания

Если ошибка, new возвращается ноль или вызывает исключение. Дополнительные сведения см. в разделе «Операторы» и delete » new Операторы». Это поведение по умолчанию можно изменить, написав настраиваемую подпрограмму обработки исключений и вызвав _set_new_handler функцию библиотеки времени выполнения с именем функции в качестве аргумента.

Сведения о создании объекта в управляемой куче в C++/CLI и C++/CX см . в разделе gcnew.

Расширения компонентов Microsoft C++ (C++/CX) обеспечивают поддержку new ключевое слово добавления записей слотов vtable. Дополнительные сведения см. в разделе new (новый слот в vtable)

Если new используется для выделения памяти для объекта класса C++, конструктор объекта вызывается после выделения памяти.

delete Используйте оператор, чтобы освободить память, выделенную оператором new . delete[] Используйте оператор для удаления массива, выделенного оператором new .

В следующем примере выделяется и затем освобождается двумерный массив символов размером dim на 10. При выделении многомерного массива все измерения, кроме первого, должны быть константными выражениями, которые оцениваются положительными значениями. Самое левое измерение массива может быть любым выражением, которое оценивается положительным значением. При выделении массива new с помощью оператора первое измерение может быть равно нулю; new оператор возвращает уникальный указатель.

char (*pchar)[10] = new char[dim][10]; delete [] pchar; 

Не type-id удается содержать const , volatile объявления классов или объявления перечисления. Следующее выражение является плохо сформированным:

volatile char *vch = new volatile char[20]; 

Оператор new не выделяет ссылочные типы, так как они не объекты.

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

int (**p) () = new (int (*[7]) ()); delete p; 

Если оператор new используется без дополнительных аргументов и компилируется с /GX параметром , /EHa или /EHs параметром, компилятор создает код для вызова оператора delete , если конструктор создает исключение.

В следующем списке описываются элементы грамматики new :

new-placement
Предоставляет способ передачи дополнительных аргументов при перегрузке new .

type-id
Указывает тип, который нужно выделить; это может быть встроенный или определяемый пользователем тип. Если спецификация типа является сложной, она может быть окружена круглыми скобками, чтобы принудительно реализовать порядок привязки. Тип может быть заполнителем ( auto ), тип которого определяется компилятором.

new-initializer
Предоставляет значение для инициализированного объекта. Инициализаторы не могут быть указаны для массивов. Оператор new создаст массивы объектов только в том случае, если класс имеет конструктор по умолчанию.

noptr-new-declarator
Задает границы массива. При выделении многомерного массива все измерения, кроме первого, должны быть константными выражениями, которые оцениваются в положительные значения, преобразуемые std::size_t в . Самое левое измерение массива может быть любым выражением, которое оценивается положительным значением. Применяется attribute-specifier-seq к связанному типу массива.

Пример. Выделение и освобождение массива символов

В следующем примере кода выделяется и освобождается массив символов и объект класса CName .

// expre_new_Operator.cpp // compile with: /EHsc #include class CName < public: enum < sizeOfBuffer = 256 >; char m_szFirst[sizeOfBuffer]; char m_szLast[sizeOfBuffer]; public: void SetName(char* pszFirst, char* pszLast) < strcpy_s(m_szFirst, sizeOfBuffer, pszFirst); strcpy_s(m_szLast, sizeOfBuffer, pszLast); >>; int main() < // Allocate memory for the array char* pCharArray = new char[CName::sizeOfBuffer]; strcpy_s(pCharArray, CName::sizeOfBuffer, "Array of characters"); // Deallocate memory for the array delete [] pCharArray; pCharArray = NULL; // Allocate memory for the object CName* pName = new CName; pName->SetName("Firstname", "Lastname"); // Deallocate memory for the object delete pName; pName = NULL; > 

Пример: new оператор

Если используется форма размещения оператора (форма с большим количеством аргументов, чем размер), компилятор не поддерживает форму new delete размещения оператора, если конструктор создает исключение. Например:

// expre_new_Operator2.cpp // C2660 expected class A < public: A(int) < throw "Fail!"; >>; void F(void) < try < // heap memory pointed to by pa1 will be deallocated // by calling ::operator delete(void*). A* pa1 = new A(10); >catch (. ) < >try < // This will call ::operator new(size_t, char*, int). // When A::A(int) does a throw, we should call // ::operator delete(void*, char*, int) to deallocate // the memory pointed to by pa2. Since // ::operator delete(void*, char*, int) has not been implemented, // memory will be leaked when the deallocation can't occur. A* pa2 = new(__FILE__, __LINE__) A(20); >catch (. ) < >> int main()

Инициализация объектов, выделенных с помощью new

Необязательное new-initializer поле включается в грамматику для new оператора. Это поле позволяет инициализировать новые объекты с помощью определяемых пользователем конструкторов. Дополнительные сведения о том, как выполняется инициализация, см. в разделе «Инициализаторы». В следующем примере показано, как использовать выражение инициализации с оператором new :

// expre_Initializing_Objects_Allocated_with_new.cpp class Acct < public: // Define default constructor and a constructor that accepts // an initial balance. Acct() < balance = 0.0; >Acct( double init_balance ) < balance = init_balance; >private: double balance; >; int main() < Acct *CheckingAcct = new Acct; Acct *SavingsAcct = new Acct ( 34.98 ); double *HowMuch = new double < 43.0 >; // . > 

В этом примере объект CheckingAcct выделяется с помощью new оператора, но инициализация по умолчанию не указана. Поэтому вызывается конструктор по умолчанию для класса Acct() . Затем объект SavingsAcct выделяется таким же образом, за исключением того, что он явно инициализирован до 34,98. Так как 34.98 имеет тип double , конструктор, принимаюющий аргумент этого типа, вызывается для обработки инициализации. Наконец, тип HowMuch , отличный от класса, инициализирован до 43.0.

Если объект имеет тип класса и этот класс имеет конструкторы (как и в предыдущем примере), объект можно инициализировать оператором new , только если выполняется одно из этих условий:

  • Аргументы, предоставленные в инициализаторе, соответствуют аргументам конструктора.
  • Класс имеет конструктор по умолчанию (конструктор, который можно вызвать без аргументов).

Явное инициализация для каждого элемента не может выполняться при выделении массивов с помощью new оператора; вызывается только конструктор по умолчанию, если он присутствует. Дополнительные сведения см. в разделе «Аргументы по умолчанию».

Если выделение памяти завершается сбоем ( operator new возвращает значение 0), инициализация не выполняется. Это поведение защищает от попыток инициализации данных, которые не существуют.

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

Время существования объектов, выделенных с помощью new

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

// expre_Lifetime_of_Objects_Allocated_with_new.cpp // C2541 expected int main() < // Use new operator to allocate an array of 20 characters. char *AnArray = new char[20]; for( int i = 0; i < 20; ++i ) < // On the first iteration of the loop, allocate // another array of 20 characters. if( i == 0 ) < char *AnotherArray = new char[20]; >> delete [] AnotherArray; // Error: pointer out of scope. delete [] AnArray; // OK: pointer still in scope. > 

После того как указатель AnotherArray в этом примере вышел за пределы области видимости, объект невозможно удалить.

Как работает new

Выражение new-expression , new содержащее оператор, выполняет три действия:

  • Находит и резервирует хранилище для объекта или объектов, которым нужно выделить память. По завершении этого этапа выделяется правильный объем хранилища, но он еще не является объектом.
  • Инициализирует объекты. После завершения инициализации имеется достаточно информации, чтобы выделенная память являлась объектом.
  • Возвращает указатель на объекты типа указателя, производные от new-type-id или type-id . Программа использует этот указатель для доступа к новому объекту, которому выделена память.

Оператор new вызывает функцию operator new . Для массивов любого типа и для объектов, которые не class являются , struct или union типами, глобальной функцией, ::operator new вызывается для выделения хранилища. Объекты типа класса могут определять собственную operator new статическую функцию-член на основе каждого класса.

Когда компилятор обнаруживает new оператор для выделения объекта типа T , он выдает вызов T::operator new( sizeof(T) ) или, если пользователь не определен operator new . ::operator new( sizeof(T) ) Это то, как new оператор может выделить правильный объем памяти для объекта.

Аргумент operator new типа std::size_t . Этот тип определяется в , , search.h>>, , stdlib.h>, и

Параметр в грамматике разрешает спецификацию new-placement (см. грамматику оператора new ). Параметр new-placement можно использовать только для определяемых пользователем реализаций operator new ; он позволяет передавать operator new дополнительные сведения. Выражение с таким полем new-placement , как T *TObject = new ( 0x0040 ) T; преобразование T *TObject = T::operator new( sizeof( T ), 0x0040 ); в значение, в которое входит operator new класс T, в противном случае — в T *TObject = ::operator new( sizeof( T ), 0x0040 ); .

Первоначальное new-placement намерение поля заключается в том, чтобы разрешить аппаратным зависимым объектам выделяться по указанным пользователем адресам.

Хотя в предыдущем примере показано только один аргумент в new-placement поле, нет ограничений на то, сколько дополнительных аргументов можно передать таким operator new образом.

Даже если operator new для типа T класса определен тип класса, можно явно использовать глобальный оператор new , как в следующем примере:

T *TObject = ::new TObject; 

Оператор область разрешения ( :: ) принудительно использует глобальный new оператор.

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

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