Зачем нам вообще нужен оператор delete []?

Это вопрос, который не дает мне покоя в течение некоторого времени. Я всегда считал, что C++ должен быть спроектирован так, чтобы оператор delete (без скобок) работал даже с оператором new[].

На мой взгляд, пишу это:

int* p = new int;

должен быть эквивалентен размещению массива из 1 элемента:

int* p = new int[1];

Если бы это было так, оператор delete всегда мог бы удалять массивы, и нам бы не понадобился оператор delete[].

Есть ли причина, по которой оператор delete[] был введен в C++? Единственная причина, по которой я могу думать, заключается в том, что выделение массивов требует небольшого объема памяти (вы должны где-то хранить размер массива), поэтому различие delete и delete[] было небольшой оптимизацией памяти.

Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
36
0
6 124
7
Перейти к ответу Данный вопрос помечен как решенный

Ответы 7

Ответ принят как подходящий

Это так, что будут вызываться деструкторы отдельных элементов. Да, для массивов POD нет большой разницы, но в C++ вы можете иметь массивы объектов с нетривиальными деструкторами.

Теперь ваш вопрос: почему бы не заставить new и delete вести себя как new[] и delete[] и не избавиться от new[] и delete[]? Я бы вернулся к книге Страуструпа «Дизайн и эволюция», где он сказал, что если вы не используете функции C++, вам не придется за них платить (по крайней мере, во время выполнения). В нынешнем виде new или delete будут вести себя так же эффективно, как malloc и free. Если бы delete имел значение delete[], были бы некоторые дополнительные накладные расходы во время выполнения (как указал Джеймс Карран).

На самом деле, когда вы используете new int [1], он сохраняет в заголовке массива перед его первыми данными размер. Таким образом, использование delete вместо delete [] не освободит эту часть памяти.

Rodrigo 24.04.2009 17:35

Существует ли на самом деле современная реализация, которая не может определить (если стандарт позволяет) правильную вещь во время выполнения, независимо от того, используется ли delete или delete []?

Pete Fordham 28.05.2020 05:30

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

Вот хорошее чтение: http://www.informit.com/guides/content.aspx?g=cplusplus&seqNum=287

И нет, размеры массивов нигде в C++ не хранятся. (Спасибо всем за указание на неточность этого утверждения.)

Не согласен с вашим последним утверждением. Компилятор должен знать размер массива, чтобы вызывать деструктор для каждого объекта в массиве. Я думаю, вы путаете это с тем фактом, что C++ не выполняет проверку границ массивов.

Mike Spross 31.10.2008 06:39

О, правда. Я думал, вы предлагали сохранить размер как часть структуры данных массива (буфера). Да, компилятору, вероятно, придется где-то хранить информацию о размере ...

Ates Goral 31.10.2008 07:07

Один из подходов - сохранить размер и количество элементов в слове перед началом массива. Это называется файлом cookie.

Dynite 31.10.2008 12:16

Кроме того, delete вызывает деструктор - для одного элемента.

peterchen 31.10.2008 13:11

У Маршалла Клайна есть информация по этой теме.

Эта ссылка не объясняет, почему для языка были разработаны отдельные операторы delete и delete[].

jamesdlin 29.11.2014 02:48

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

Единственное, о чем я могу думать, это то, что есть очень крошечные дополнительные накладные расходы на обработку одного объекта как массива (ненужный "for(int i=0; i<1; ++i)").

Плюс крошечный бит памяти для хранения размера.

Martin York 31.10.2008 08:16

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

Steve Jessop 31.10.2008 18:01

Черт, я пропустил всю суть вопроса, но оставлю свой исходный ответ в качестве примечания. У нас есть delete[], потому что давным-давно у нас был delete[cnt], даже сегодня, если вы пишете delete[9] или delete[cnt], компилятор просто игнорирует вещи между [], но компилирует нормально. В то время C++ сначала обрабатывался интерфейсом, а затем передавался в обычный компилятор C. У них не было возможности хранить счет где-то под занавеской, возможно, они даже не могли подумать об этом в то время. И для обратной совместимости компиляторы, скорее всего, использовали значение, указанное между [], как счетчик массива, если такого значения нет, они получили счетчик из префикса, поэтому он работал в обоих направлениях. Потом между [] ничего не набирали, все заработало. Сегодня я не думаю, что delete[] необходим, но реализации этого требуют.

Мой первоначальный ответ (который упускает из виду):

delete удаляет один объект. delete[] удаляет массив объектов. Для работы delete[] реализация сохраняет количество элементов в массиве. Я просто дважды проверил это, отладив код ASM. В тестируемой мной реализации (VS2005) счетчик хранился как префикс к массиву объектов.

Если вы используете delete[] для одного объекта, переменная count будет мусором, поэтому код выйдет из строя. Если вы используете delete для массива объектов, из-за некоторой несогласованности происходит сбой кода. Я тестировал эти кейсы только сейчас!

«delete просто удаляет память, выделенную для массива». утверждение в другом ответе неверно. Если объект является классом, delete вызовет DTOR. Просто поместите точку останова в код DTOR и delete в объект, точка останова сработает.

Мне пришло в голову, что если компилятор и библиотеки предполагают, что все объекты, выделенные new, являются массивами объектов, было бы нормально вызывать delete для отдельных объектов или массивов объектов. Одиночные объекты были бы особым случаем массива объектов, имеющего счетчик 1. Может быть, я все равно чего-то упускаю.

Добавляя это, поскольку на данный момент нет другого ответа:

Массив delete[] нельзя использовать в классе указателя на базовый класс - хотя компилятор сохраняет количество объектов, когда вы вызываете new[], он не сохраняет типы или размеры объектов (как указал Дэвид, в C++ вы редко платите за функцию, которую вы не используете). Однако скалярный delete можно безопасно удалить через базовый класс, поэтому он используется как для обычной очистки объекта, так и для полиморфной очистки:

struct Base { virtual ~Base(); };
struct Derived : Base { };
int main(){
    Base* b = new Derived;
    delete b; // this is good

    Base* b = new Derived[2];
    delete[] b; // bad! undefined behavior
}

Однако в противоположном случае - не виртуальный деструктор - скалярный delete должен быть как можно более дешевым - он не должен проверять количество объектов или тип удаляемого объекта. Это делает удаление встроенного типа или простого старого типа данных очень дешевым, поскольку компилятору нужно только вызывать ::operator delete и ничего больше:

int main(){
    int * p = new int;
    delete p; // cheap operation, no dynamic dispatch, no conditional branching
}

Хотя это не исчерпывающее описание распределения памяти, я надеюсь, что это поможет прояснить широту возможностей управления памятью, доступных в C++.

Меня немного смутил ответ Аарона, и, честно говоря, я не совсем понимаю, зачем и где нужен delete[].

Я провел несколько экспериментов с его образцом кода (после исправления нескольких опечаток). Вот мои результаты. Опечатки: ~Base требовалось тело функции Base *b декларировался дважды

struct Base { virtual ~Base(){ }>; };
struct Derived : Base { };
int main(){
Base* b = new Derived;
delete b; // this is good

<strike>Base</strike> b = new Derived[2];
delete[] b; // bad! undefined behavior
}

Составление и исполнение

david@Godel:g++ -o atest atest.cpp 
david@Godel: ./atest 
david@Godel: # No error message

Модифицированная программа с удаленным delete[]

struct Base { virtual ~Base(){}; };
struct Derived : Base { };

int main(){
    Base* b = new Derived;
    delete b; // this is good

    b = new Derived[2];
    delete b; // bad! undefined behavior
}

Составление и исполнение

david@Godel:g++ -o atest atest.cpp 
david@Godel: ./atest 
atest(30746) malloc: *** error for object 0x1099008c8: pointer being freed was n
ot allocated
*** set a breakpoint in malloc_error_break to debug
Abort trap: 6

Конечно, я не знаю, действительно ли delete[] b работает в первом примере; Я знаю только, что он не выдает сообщения об ошибке компилятора.

Другие вопросы по теме