Безопасно ли удалять пустой указатель?

Предположим, у меня есть следующий код:

void* my_alloc (size_t size)
{
   return new char [size];
}

void my_free (void* ptr)
{
   delete [] ptr;
}

Это безопасно? Или ptr нужно преобразовать в char* перед удалением?


person An̲̳̳drew    schedule 02.06.2009    source источник
comment
Почему вы сами занимаетесь управлением памятью? Какую структуру данных вы создаете? Необходимость в явном управлении памятью встречается в C++ довольно редко; обычно вы должны использовать классы, которые обрабатывают это для вас из STL (или, в крайнем случае, из Boost).   -  person Brian    schedule 03.06.2009
comment
Просто для тех, кто читает, я использую переменные void* в качестве параметров для своих потоков в win C++ (см. _beginthreadex). Обычно они указывают на классы.   -  person ryansstack    schedule 03.06.2009
comment
В данном случае это оболочка общего назначения для создания/удаления, которая может содержать статистику отслеживания распределения или оптимизированный пул памяти. В других случаях я видел, как указатели объектов неправильно сохранялись как переменные-члены void* и некорректно удалялись в деструкторе без возврата к соответствующему типу объекта. Поэтому мне было интересно узнать о безопасности/подводных камнях.   -  person An̲̳̳drew    schedule 03.06.2009
comment
Для оболочки общего назначения для нового/удаления вы можете перегрузить операторы нового/удаления. В зависимости от того, какую среду вы используете, вы, вероятно, получите доступ к управлению памятью для отслеживания распределения. Если вы окажетесь в ситуации, когда не знаете, что удаляете, примите это как сильный намек на то, что ваш дизайн неоптимален и нуждается в рефакторинге.   -  person Hans    schedule 03.06.2009
comment
Поскольку вы уже обертываете выделение и уничтожение, вы можете легко выполнить кастинг без дополнительных затрат.   -  person Sanjaya R    schedule 03.06.2009
comment
Можно придумать множество причин, по которым вы хотели бы сделать это на С++, например, скажем, у вас есть база данных, которая нужна вашей программе, она была экспортирована в двоичный формат, и вы хотите иметь возможность загрузить эту предварительно обработанную базу данных в непрерывный блок памяти ... или если у вас есть собственная виртуальная файловая система (хотя я полагаю, что такие методы, вероятно, более полезны при разработке игр, где производительность имеет решающее значение   -  person Stowelly    schedule 04.06.2009
comment
Я думаю, что есть слишком много вопросов, вместо того, чтобы ответить на него. (Не только здесь, но и во всех ТАК)   -  person Petruza    schedule 07.12.2011
comment
А если серьезно, @Andrew просто использует malloc и free напрямую. Это неявно несет в себе тот факт, что конструктор/деструктор не будет вызываться (как говорят ответы ниже) для того, что лежит в основе этого указателя void*.   -  person bobobobo    schedule 07.05.2013
comment
Поскольку у char нет деструктора, это будет безопасно. malloc/free будет быстрее, но тогда вы потеряете std::bad_alloc.   -  person rxantos    schedule 01.02.2015
comment
Я использую void* в качестве контекста, который передается мне кодом C# и тип которого будет в стороннем коде C++, реализующем базовый класс подключаемого модуля, который я предоставляю. В моем случае все, что требовалось, это сделать delete (MyPluginBase*) aPointer и убедиться, что MyPlugin имеет виртуальный деструктор.   -  person Derf Skren    schedule 04.07.2016


Ответы (13)


Это зависит от "безопасности". Обычно это работает, потому что информация о самом выделении хранится вместе с указателем, поэтому делокатор может вернуть ее в нужное место. В этом смысле это «безопасно», пока ваш распределитель использует внутренние граничные теги. (Многие делают.)

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

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

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

person Christopher    schedule 02.06.2009
comment
Как это принятый ответ? Нет никакого смысла удалять пустой указатель — вопрос безопасности спорный. - person Kerrek SB; 08.09.2013
comment
Нет веских причин делать то, что вы делаете, так, как вы это делаете. Это ваше мнение, а не факт. - person rxantos; 01.02.2015
comment
@rxantos Приведите встречный пример, когда делать то, что хочет автор вопроса, - хорошая идея на C ++. - person Christopher; 08.03.2015
comment
Я думаю, что этот ответ на самом деле в основном разумен, но я также думаю, что любой ответ на этот вопрос должен по крайней мере упомянуть, что это поведение undefined. - person Kyle Strand; 30.07.2018
comment
@Christopher Попробуйте написать единую систему сборщика мусора, которая не зависит от типа, а просто работает. Тот факт, что sizeof(T*) == sizeof(U*) для всех T,U предполагает, что должна быть возможность иметь 1 не шаблонную, основанную на void * реализацию сборщика мусора. Но тогда, когда gc действительно должен удалить/освободить указатель, возникает именно этот вопрос. Чтобы заставить его работать, вам либо нужны оболочки деструктора лямбда-функции (ургх), либо вам понадобится какой-то динамический тип в качестве типа данных, который позволяет перемещаться между типом и чем-то сохраняемым. - person BitTickler; 24.06.2019
comment
@BitTickler Я сделал. Я знаю, что это сложно, но дело в том, что вопрос задает неопределенное поведение. См. chromium.googlesource.com /chromium/src/+/master/ Third_Party/ за хороший способ сборки мусора на C++. - person Christopher; 25.06.2019
comment
@Christopher Хотя я меньше фокусировался на самом алгоритме GC (использовал unordered_map<void*,Something>), я нашел - вполне работоспособный, хотя и не очень эффективный способ сделать это: template <class T> struct Foo { void deleter(void*p) { delete reinterpret_cast<T*>(p); } }; Затем я сохранил указатель на эту функцию удаления в Something, в переход от типизированного к нетипизированному. Помимо массивов T, это должно быть прочным и легко расширяемым. - person BitTickler; 25.06.2019
comment
сама тема сложна и наполнена противоречивыми мнениями. Не совсем так. Неопределенное поведение неприемлемо. Когда оно происходит, оно делает все недействительным. Это зависит от сейфа. Опять же, не совсем так. Это неопределенное поведение. Не существует рационального определения безопасного кода, допускающего неопределенное поведение. - person François Andrieux; 29.01.2021

Удаление с помощью указателя void не определено стандартом C++ - см. раздел 5.3.5/3:

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

И его сноска:

Это означает, что объект не может быть удален с помощью указателя типа void*, потому что нет объектов типа void*.

.

person Community    schedule 02.06.2009
comment
Вы уверены, что попали в нужную цитату? Я думаю, что сноска относится к этому тексту: в первом варианте (удалить объект), если статический тип операнда отличается от его динамического типа, статический тип должен быть базовым классом динамического типа операнда и статического типа должен иметь виртуальный деструктор, иначе поведение не определено. Во втором варианте (удаление массива), если динамический тип удаляемого объекта отличается от его статического типа, поведение не определено. :) - person Johannes Schaub - litb; 03.06.2009
comment
Вы правы - я обновил ответ. Я не думаю, что это отменяет основную мысль? - person ; 03.06.2009
comment
Нет, конечно нет. Он все еще говорит, что это UB. Тем более, что теперь нормативно прописано, что удаление void* — это UB :) - person Johannes Schaub - litb; 03.06.2009
comment
Заполнить указанную адресную память пустого указателя NULL имеет ли какое-либо значение для управления памятью приложения? - person artu-hnrq; 27.02.2020

Это не очень хорошая идея и не то, что вы бы сделали на С++. Вы теряете информацию о типе без причины.

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

Вместо этого следует переопределить новое/удаление.

Удаление void*, вероятно, случайно правильно освободит вашу память, но это неправильно, потому что результаты не определены.

Если по какой-то неизвестной мне причине вам нужно сохранить свой указатель в пустоте *, а затем освободить его, вы должны использовать malloc и free.

person Brian R. Bondy    schedule 02.06.2009
comment
Вы правы в том, что деструктор не вызывается, но ошибаетесь в том, что размер неизвестен. Если вы даете удалить указатель, который вы получили от нового, он действительно фактически знает размер удаляемой вещи, полностью независимо от типа. Как это делается, не указано в стандарте C++, но я видел реализации, в которых размер сохраняется непосредственно перед данными, на которые указывает указатель, возвращаемый 'new'. - person KeyserSoze; 03.06.2009
comment
Удалена часть о размере, хотя стандарт C++ говорит, что он не определен. Я знаю, что malloc/free будет работать для указателей void*. - person Brian R. Bondy; 03.06.2009
comment
У вас нет веб-ссылки на соответствующий раздел стандарта? Я знаю, что несколько реализаций new/delete, на которые я смотрел, определенно работают правильно без знания типа, но я признаю, что не смотрел на то, что указано в стандарте. IIRC C ++ изначально требовал подсчета элементов массива при удалении массивов, но больше не требуется в новейших версиях. - person KeyserSoze; 03.06.2009
comment
Пожалуйста, смотрите ответ @Neil Butterworth. Его ответ должен быть принятым, на мой взгляд. - person Brian R. Bondy; 03.06.2009
comment
@keysersoze: вы уверены, что new возвращает данные, которым предшествует их размер? Для меня было бы странно избегать использования виртуалов из-за дополнительного места, занимаемого для vptr, а затем просто платить ту же цену (размер, вероятно, 4 байта, как указатель) за вещь, которая выглядит не очень полезной. - person ; 04.06.2009
comment
@keysersoze: В целом я не согласен с вашим утверждением. Тот факт, что некоторые реализации сохранили размер перед выделенной памятью, не означает, что это правило. - person ; 05.06.2009

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

person JaredPar    schedule 02.06.2009
comment
char не имеет конструктора/деструктора. - person rxantos; 01.02.2015

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

Вы используете delete для уничтожения объекта, который был размещен динамически. При этом вы формируете выражение удаления с указателем на этот объект. Вы никогда не «удаляете указатель». Что вы действительно делаете, так это «удаляете объект, который идентифицируется по его адресу».

Теперь мы понимаем, почему вопрос не имеет смысла: указатель void не является «адресом объекта». Это просто адрес, без какой-либо семантики. Он может исходить из адреса фактического объекта, но эта информация будет потеряна, поскольку она была закодирована в типе исходного указателя. Единственный способ восстановить указатель объекта — привести указатель void обратно к указателю объекта (что требует от автора знания значения указателя). void сам по себе является неполным типом и, следовательно, никогда не является типом объекта, а указатель void никогда не может использоваться для идентификации объекта. (Объекты идентифицируются совместно по их типу и их адресу.)

person Kerrek SB    schedule 08.09.2013
comment
По общему признанию, вопрос не имеет большого смысла без какого-либо окружающего контекста. Некоторые компиляторы C++ по-прежнему будут с радостью компилировать такой бессмысленный код (если они чувствуют себя полезными, они могут выдать предупреждение об этом). Итак, вопрос был задан для того, чтобы оценить известные риски запуска устаревшего кода, содержащего эту опрометчивую операцию: произойдет ли сбой? утечка некоторой или всей памяти массива символов? что-то еще, что зависит от платформы? - person An̲̳̳drew; 01.10.2013
comment
Спасибо за вдумчивый ответ. Голосую! - person An̲̳̳drew; 01.10.2013
comment
@Andrew: я боюсь, что стандарт довольно ясен в этом: значение операнда delete может быть нулевым значением указателя, указателем на объект, не являющийся массивом, созданный предыдущим new-expression или указатель на подобъект, представляющий базовый класс такого объекта. Если нет, поведение не определено. Итак, если компилятор принимает ваш код без диагностики, это не что иное, как ошибка в компиляторе... - person Kerrek SB; 01.10.2013
comment
@KerrekSB - Re это не что иное, как ошибка в компиляторе - я не согласен. Стандарт говорит, что поведение не определено. Это означает, что компилятор/реализация может делать что угодно и при этом соответствовать стандарту. Если ответ компилятора говорит, что вы не можете удалить указатель void*, это нормально. Если ответом компилятора является стирание жесткого диска, это тоже нормально. OTOH, если ответ компилятора состоит в том, чтобы не генерировать никакой диагностики, а вместо этого генерировать код, который освобождает память, связанную с этим указателем, это тоже нормально. Это простой способ справиться с этой формой UB. - person David Hammen; 27.02.2014
comment
Просто добавлю: я не одобряю использование delete void_pointer. Это неопределенное поведение. Программисты никогда не должны вызывать неопределенное поведение, даже если ответ делает то, что хотел программист. - person David Hammen; 27.02.2014
comment
@DavidHammen: Хорошо, хорошо. Думайте об ошибке как о неспособности быть хорошим и ответственным инструментом... - person Kerrek SB; 27.02.2014
comment
@DavidHammen: Если ответом компилятора является стирание жесткого диска, это тоже нормально. Нет это не так. Поведение компилируемого кода не определено, но неограниченное действие возникает, когда код выполняется (и, кроме того, только если поток выполнения достигает этой конкретной части кода), а не когда он компилируется. - person Ben Voigt; 23.09.2016
comment
Итак, заполнение адресной памяти указателя void с помощью NULL имеет какое-либо значение для управления памятью приложения? - person artu-hnrq; 27.02.2020

Если вам действительно необходимо это сделать, почему бы не убрать посредника (операторы new и delete) и напрямую вызвать глобальные operator new и operator delete? (Конечно, если вы пытаетесь инструментировать операторы new и delete, вам действительно следует переопределить operator new и operator delete.)

void* my_alloc (size_t size)
{
   return ::operator new(size);
}

void my_free (void* ptr)
{
   ::operator delete(ptr);
}

Обратите внимание, что в отличие от malloc(), operator new выдает std::bad_alloc при ошибке (или вызывает new_handler, если он зарегистрирован).

person bk1e    schedule 04.06.2009
comment
Это правильно, так как у char нет конструктора/деструктора. - person rxantos; 01.02.2015

Потому что у char нет специальной логики деструктора. ЭТО не сработает.

class foo
{
   ~foo() { printf("huzza"); }
}

main()
{
   foo * myFoo = new foo();
   delete ((void*)foo);
}

Доктора не вызовут.

person Community    schedule 02.06.2009

Если вы хотите использовать void*, почему бы вам не использовать только malloc/free? new/delete — это больше, чем просто управление памятью. По сути, new/delete вызывает конструктор/деструктор, и происходит больше вещей. Если вы просто используете встроенные типы (например, char*) и удаляете их через void*, это сработает, но все же это не рекомендуется. Суть в том, что используйте malloc/free, если вы хотите использовать void*. В противном случае вы можете использовать функции шаблона для вашего удобства.

template<typename T>
T* my_alloc (size_t size)
{
   return new T [size];
}

template<typename T>
void my_free (T* ptr)
{
   delete [] ptr;
}

int main(void)
{
    char* pChar = my_alloc<char>(10);
    my_free(pChar);
}
person young    schedule 02.06.2009
comment
Я не писал код в примере - наткнулся на этот шаблон, используемый в паре мест, любопытным образом смешивая управление памятью C/C++, и мне было интересно, каковы конкретные опасности. - person An̲̳̳drew; 03.06.2009
comment
Написание C/C++ — это путь к провалу. Тот, кто это написал, должен был написать и то, и другое. - person David Thornley; 03.06.2009
comment
@David Это C++, а не C/C++. C не имеет шаблонов, не использует new и delete. - person rxantos; 01.02.2015

Многие люди уже прокомментировали, говоря, что нет, небезопасно удалять пустой указатель. Я согласен с этим, но я также хотел добавить, что если вы работаете с пустыми указателями для выделения смежных массивов или чего-то подобного, вы можете сделать это с помощью new, чтобы вы могли безопасно использовать delete (с , гм, немного дополнительной работы). Это делается путем размещения указателя void в области памяти (называемой «ареной»), а затем предоставления указателя на арену в new. См. этот раздел в часто задаваемых вопросах по C++. Это распространенный подход к реализации пулов памяти в C++.

person Paul Morie    schedule 02.06.2009

Вряд ли для этого есть причины.

Прежде всего, если вы не знаете тип данных, и все, что вы знаете, это то, что это void*, то вам действительно следует рассматривать эти данные как бестиповый большой двоичный объект двоичных данных (unsigned char*) и используйте malloc/free для работы с ним. Иногда это требуется для таких вещей, как данные формы сигнала и т.п., когда вам нужно передать void* указателей на C apis. Это нормально.

Если вы действительно знаете тип данных (т.е. у них есть ctor/dtor), но по какой-то причине вы получили указатель void* (по любой причине), то вы действительно следует вернуть его к известному вам типу и вызвать для него delete.

person bobobobo    schedule 07.05.2013

Я использовал void* (также известные как неизвестные типы) в своей структуре для отражения кода и других подвигов двусмысленности, и до сих пор у меня не было проблем (утечка памяти, нарушения прав доступа и т. д.) от любых компиляторов. Только предупреждения из-за нестандартной операции.

Вполне имеет смысл удалить неизвестное (void*). Просто убедитесь, что указатель следует этим рекомендациям, иначе он может потерять смысл:

1) Неизвестный указатель не должен указывать на тип с тривиальным деконструктором, поэтому при приведении к неизвестному указателю он НИКОГДА НЕ ДОЛЖЕН БЫТЬ УДАЛЕНЫМ. Удаляйте неизвестный указатель только ПОСЛЕ приведения его обратно к ОРИГИНАЛЬНОМУ типу.

2) На экземпляр ссылаются как на неизвестный указатель в памяти, привязанной к стеку или куче? Если неизвестный указатель ссылается на экземпляр в стеке, то он НИКОГДА НЕ ДОЛЖЕН УДАЛЯТЬСЯ!

3) Вы на 100% уверены, что неизвестный указатель является допустимой областью памяти? Нет, тогда это НИКОГДА НЕ ДОЛЖНО БЫТЬ УДАЛЕНО!

В целом, существует очень мало прямой работы, которую можно выполнить, используя неизвестный (void*) тип указателя. Однако, косвенно, void* является большим преимуществом для разработчиков C++, на которое можно положиться, когда требуется неоднозначность данных.

person zackery.fix    schedule 04.01.2014

Если вам просто нужен буфер, используйте malloc/free. Если вы должны использовать new/delete, рассмотрите тривиальный класс-оболочку:

template<int size_ > struct size_buffer { 
  char data_[ size_]; 
  operator void*() { return (void*)&data_; }
};

typedef sized_buffer<100> OpaqueBuffer; // logical description of your sized buffer

OpaqueBuffer* ptr = new OpaqueBuffer();

delete ptr;
person Sanjaya R    schedule 03.06.2009

Для частного случая char.

char — это встроенный тип, не имеющий специального деструктора. Таким образом, аргументы утечки являются спорными.

sizeof(char) обычно равен единице, поэтому аргумента выравнивания также нет. В случае редкой платформы, где sizeof(char) не равен единице, они выделяют память, достаточно выровненную для своего char. Таким образом, аргумент выравнивания также является спорным.

malloc/free будет быстрее в этом случае. Но вы теряете std::bad_alloc и должны проверить результат malloc. Вызов глобальных операторов new и delete может быть лучше, поскольку он обходит посредника.

person rxantos    schedule 01.02.2015
comment
sizeof(char) обычно равен единице sizeof(char) ВСЕГДА равен единице - person curiousguy; 14.06.2018
comment
Только недавно (2019 г.) люди думали, что new на самом деле определяется как бросок. Это неправда. Это зависит от компилятора и переключателя компилятора. См., например, переключатели MSVC2019 /GX[-] enable C++ EH (same as /EHsc). Кроме того, во встроенных системах многие предпочитают не платить налог на производительность за исключения C++. Таким образом, предложение, начинающееся с «Но вы теряете std::bad_alloc...», вызывает сомнения. - person BitTickler; 24.06.2019