Чи безпечно видалити порожній покажчик?


96

Припустимо, у мене є такий код:

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

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

Це безпечно? Або має ptrбути передано char*до видалення?


Чому ви займаєтесь управлінням пам’яттю самостійно? Яку структуру даних ви створюєте? Потреба в явному управлінні пам'яттю досить рідко зустрічається в C ++; зазвичай слід використовувати класи, які обробляють його для вас із STL (або з Boost у крайньому випадку).
Брайан

6
Тільки для людей, які читають, я використовую void * змінні як параметри для моїх потоків у win c ++ (див. _Beginthreadex). Зазвичай вони акутно вказують на заняття.
ryansstack

3
У цьому випадку це загальна обгортка для new / delete, яка може містити статистику відстеження розподілу або оптимізований пул пам'яті. В інших випадках я бачив вказівники на об'єкти, неправильно збережені у вигляді змінних-членів void * і неправильно видалені в деструкторі, не повертаючи відповідний тип об'єкта. Тож мені було цікаво щодо безпеки / підводних каменів.
An̲̳̳drew

1
Для обгортки загального призначення для нового / видалення ви можете перевантажувати операторів нового / видалення. Залежно від того, яке середовище ви використовуєте, ви, мабуть, потрапляєте в управління пам’яттю для відстеження розподілу. Якщо ви опинитесь в ситуації, коли ви не знаєте, що ви видаляєте, сприймайте це як сильний натяк на те, що ваш дизайн є неоптимальним і потребує рефакторингу.
Ганс

38
Я думаю, що надто багато ставити питання, а не відповідати на нього. (Не тільки тут, але і в усіх СО)
Петруза

Відповіді:


24

Це залежить від "безпечного". Зазвичай це спрацює, оскільки інформація зберігається разом із вказівником про сам розподіл, тож угодник може повернути його в потрібне місце. У цьому сенсі це "безпечно", поки ваш розподільник використовує внутрішні межі меж. (Багато хто робить.)

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

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

Як зазначалося в інших відповідях, це невизначена поведінка в C ++. Загалом добре уникати невизначеної поведінки, хоча сама тема складна і наповнена суперечливими думками.


9
Як це прийнята відповідь? Немає сенсу "видаляти порожній покажчик" - безпека є спірним питанням.
Kerrek SB

6
"Немає вагомих причин робити те, що ти робиш так, як робиш". Це ваша думка, а не факт.
rxantos

8
@rxantos Наведіть зустрічний приклад, коли в C ++ є хорошою ідеєю того, що хоче зробити автор питання.
Крістофер

Я вважаю, що ця відповідь насправді є розумною, але я також вважаю, що будь-яка відповідь на це питання має хоча б згадати, що це невизначена поведінка.
Kyle Strand

@Christopher Спробуйте написати єдину систему збирання сміття, яка не є типовою, а просто працює. Той факт, що sizeof(T*) == sizeof(U*)для всіх T,Uсвідчить про те, що має бути можливо мати 1 не шаблонну void *реалізацію збирача сміття. Але тоді, коли gc насправді повинен видалити / звільнити покажчик, саме таке питання виникає. Для того, щоб це працювало, вам або потрібні обгортки деструкторних функцій лямбда (urgh), або вам знадобиться якась динамічна річ "тип як дані", яка дозволяє переходити між типом і тим, що зберігається.
BitTickler

147

Видалення за допомогою покажчика void не визначено стандартом C ++ - див. Розділ 5.3.5 / 3:

У першій альтернативі (об’єкт видалення), якщо статичний тип операнда відрізняється від його динамічного типу, статичний тип повинен бути базовим класом динамічного типу операнда, а статичний тип повинен мати віртуальний деструктор або поведінка не визначена . У другому варіанті (видалити масив), якщо динамічний тип об’єкта, що підлягає видаленню, відрізняється від його статичного типу, поведінка невизначена.

І його виноска:

Це означає, що об'єкт не можна видалити за допомогою вказівника типу void *, оскільки немає об'єктів типу void

.


6
Ви впевнені, що вдарили правильну цитату? Я думаю, виноска посилається на цей текст: "У першій альтернативі (видалити об'єкт), якщо статичний тип операнда відрізняється від його динамічного типу, статичним типом повинен бути базовий клас динамічного типу операнда і статичним type повинен мати віртуальний деструктор, або поведінка невизначена. У другому варіанті (видалити масив), якщо динамічний тип об'єкта, що підлягає видаленню, відрізняється від статичного типу, поведінка невизначена. " :)
Йоханнес Шауб - літб

2
Ви маєте рацію - я оновив відповідь. Я не думаю, що це заперечує основний момент?

Ні, звичайно ні. Він все ще каже, що це UB. Більше того, зараз він нормативно стверджує, що видалення void * - це UB :)
Йоганнес Шауб - litb

Заповнювати вказану адресу пам’яті порожнього вказівника NULLякоюсь мірою для управління пам’яттю додатків?
artu-hnrq

25

Це не гарна ідея і не те, що ви робили б на C ++. Ви втрачаєте інформацію про тип без причини.

Ваш деструктор не буде викликаний для об'єктів у вашому масиві, які ви видаляєте, коли викликаєте його для не примітивних типів.

Натомість слід замінити нове / видалити.

Видалення void *, можливо, випадково звільнить пам'ять правильно, але це неправильно, оскільки результати невизначені.

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


5
Ви маєте рацію щодо того, що деструктор не викликали, але помиляєтесь, коли розмір невідомий. Якщо ви даєте видалити покажчик , який ви отримали від нового, він робить насправді знає розмір речі видаляється, повністю незалежно від типу. Як це робиться, це не визначено стандартом С ++, але я бачив реалізації, де розмір зберігається безпосередньо перед даними, на які вказує покажчик, який повертає "новий".
KeyserSoze 02

Видалено частину про розмір, хоча стандарт C ++ каже, що вона невизначена. Я знаю, що malloc / free буде працювати, хоча для порожніх * покажчиків.
Брайан Р. Бонді

Не думаєте, що у вас є веб-посилання на відповідний розділ стандарту? Я знаю, що декілька реалізацій new / delete, які я розглядав, безумовно працюють коректно, не знаючи типу, але я визнаю, що я не дивився на те, що вказує стандарт. IIRC C ++ спочатку вимагав підрахунку елементів масиву під час видалення масивів, але з найновішими версіями цього більше не робить.
KeyserSoze 02

Будь ласка, перегляньте відповідь @Neil Butterworth. На мою думку, його відповідь повинна бути прийнятою.
Брайан Р. Бонді

2
@keysersoze: Як правило, я не згоден з вашим твердженням. Те, що деяка реалізація зберігала розмір до виділеної пам’яті, не означає, що це правило.

13

Видалення порожнього вказівника небезпечно, оскільки деструктори не будуть викликати значення, на яке він насправді вказує. Це може призвести до витоків пам'яті / ресурсів у вашому додатку.


1
char не має конструктора / деструктора.
rxantos

9

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

Ви використовуєте deleteдля знищення об’єкта, який був динамічно розподілений. Зробивши це, ви формуєте вираз видалення з вказівником на цей об’єкт . Ви ніколи не "видаляєте вказівник". Ви насправді "видаляєте об'єкт, який ідентифікується за його адресою".

Тепер ми бачимо, чому питання не має сенсу: покажчик void не є "адресою об'єкта". Це просто адреса, без будь-якої семантики. Він може виходити з адреси реального об'єкта, але ця інформація втрачається, так як він був закодований в типі вихідного покажчика. Єдиний спосіб відновити покажчик об’єкта - повернути покажчик void назад до покажчика об’єкта (що вимагає від автора знання, що означає вказівник). voidсам по собі є неповним типом і, отже, ніколи не є типом об'єкта, а порожній вказівник ніколи не може використовуватися для ідентифікації об'єкта. (Об’єкти ідентифікуються спільно за типом та адресою.)


Слід визнати, що питання не має особливого сенсу без будь-якого оточуючого контексту. Деякі компілятори C ++ все ще із задоволенням збиратимуть такий безглуздий код (якщо вони відчувають корисність, можуть зняти попередження про це). Отже, запитання було поставлене для того, щоб оцінити відомі ризики запуску застарілого коду, що містить цю необдуману операцію: чи він не спрацює? витік частини або всієї пам'яті масиву символів? щось інше, що залежить від платформи?
An̲̳̳drew

Дякую за вдумливу відповідь. Голосуючи!
An̲̳̳drew

1
@Andrew: Я боюся, що стандарт досить чіткий щодо цього: "Значення операнда deleteможе бути нульовим значенням покажчика, вказівником на об'єкт, що не є масивом, створеним попереднім новим виразом , або вказівником на суб'єкт, що представляє базовий клас такого об'єкта. Якщо ні, поведінка не визначена. " Тож якщо компілятор приймає ваш код без діагностики, це не що інше, як помилка в компіляторі ...
Kerrek SB

1
@KerrekSB - Отже, це не що інше, як помилка у компіляторі - я не згоден. Стандарт говорить, що поведінка невизначена. Це означає, що компілятор / реалізація може робити все, що завгодно, і при цьому відповідати стандартам. Якщо відповідь компілятора говорить, що ви не можете видалити покажчик void *, це нормально. Якщо відповідь компілятора - стерти жорсткий диск, це теж нормально. OTOH, якщо відповідь компілятора полягає в тому, щоб не генерувати діагностику, а замість цього генерувати код, який звільняє пам'ять, пов'язану з цим покажчиком, це теж нормально. Це простий спосіб боротьби з цією формою UB.
Девід Хаммен,

Лише додам: я не погоджую використання delete void_pointer. Це невизначена поведінка. Програмісти ніколи не повинні посилатися на невизначене поведінку, навіть якщо відповідь, як видається, робить те, що хотів зробити програміст.
Девід Хаммен

6

Якщо ви дійсно повинні це зробити, то чому б не вирізати середнього чоловіка ( операторів 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якщо такий зареєстрований).


Це правильно, оскільки char не має конструктора / деструктора.
rxantos

5

Тому що char не має особливої ​​логіки деструктора. ЦЕ не вийде.

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

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

Директора не покличуть.


5

Якщо ви хочете використовувати 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);
}

Я не писав код у прикладі - натрапив на цей шаблон, який використовувався в кількох місцях, з цікавістю змішуючи управління пам'яттю C / C ++, і дивувався, які конкретні небезпеки.
Андрю

Написання C / C ++ - це рецепт невдачі. Той, хто це писав, повинен був написати те чи інше.
Девід Торнлі, 02

2
@David Це C ++, а не C / C ++. C не має шаблонів, а також не використовує new та delete.
rxantos

5

Дуже багато людей вже коментують, що ні, видалити порожній покажчик не безпечно. Я згоден з цим, але я також хотів додати, що якщо ви працюєте з порожніми вказівниками для того, щоб розподілити суміжні масиви або щось подібне, ви можете це newзробити, щоб ви могли deleteбезпечно користуватися (разом з , трохи додаткової роботи). Це робиться шляхом виділення порожнього вказівника в область пам’яті (яка називається «ареною»), а потім подання вказівника на арену в нову. Дивіться цей розділ у поширених запитаннях на C ++ . Це загальний підхід до впровадження пулів пам'яті в C ++.


0

Навряд чи є причина для цього.

Перш за все, якщо ви не знаєте тип даних, і все, що ви знаєте, це те, що ви void*, то вам справді слід просто розглядати ці дані як нетипову крапку двійкових даних ( unsigned char*) і використовувати malloc/ freeдля роботи з ними . Іноді це потрібно для таких речей, як дані форми сигналу тощо, де вам потрібно передати void*вказівники на C apis. Добре.

Якщо ви робите знати тип даних (тобто має CTOR / dtor), але з якоїсь - то причини ви закінчили з void*покажчиком (за якою - небудь причини у вас є) , то ви дійсно повинні кинути його назад до типу ви знаєте його бути , і закликати deleteце.


0

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

Це абсолютно має сенс видалити невідоме (пустота *). Просто переконайтесь, що покажчик дотримується цих вказівок, інакше це може перестати мати сенс:

1) Невідомий вказівник не повинен вказувати на тип, який має тривіальний деконструктор, і тому, коли його кастирують як невідомий покажчик, його НІКОЛИ НЕ БУДУТЬ ВІДДАЛЕНО. Видаліть невідомий покажчик лише ПІСЛЯ повернення його назад до типу ОРИГІНАЛ.

2) Чи посилається на екземпляр як на невідомий вказівник у пам’яті, пов’язаній зі стеком чи купою? Якщо невідомий покажчик посилається на екземпляр у стеку, його НІКОЛИ НЕ ВИДАЛЯЙТЕ!

3) Ви на 100% впевнені, що невідомий покажчик є дійсним регіоном пам'яті? Ні, тоді це НІКОЛИ НЕ БУДЕ ВДАЛЕНО!

Загалом, дуже мало безпосередньої роботи, яка може бути виконана за допомогою невідомого (void *) типу покажчика. Однак опосередковано void * є великим активом для розробників C ++, коли потрібно розраховувати на неоднозначність даних.


0

Якщо ви просто хочете буфер, використовуйте malloc / free. Якщо вам потрібно використовувати новий / видалити, розгляньте тривіальний клас обгортки:

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;

0

Для приватного випадку char.

char - це внутрішній тип, який не має спеціального деструктора. Тож аргументи про витоки є суперечливими.

sizeof (char) зазвичай один, тому аргументу вирівнювання також немає. У випадку рідкісних платформ, де розмір (char) не один, вони виділяють пам'ять, вирівняну для їх char. Тож аргумент вирівнювання також є спірним.

malloc / free у цьому випадку буде швидшим. Але ви втрачаєте std :: bad_alloc і повинні перевірити результат malloc. Виклик глобальних операторів new та delete може бути кращим, оскільки це обходить середнього.


1
" sizeof (char), як правило, один " sizeof (char) ЗАВЖДИ один
цікавийгун

Ще недавно (2019 рік) люди думають, що newнасправді визначено кинути. Це не правда. Це компілятор і перемикач компілятора. Дивіться, наприклад, /GX[-] enable C++ EH (same as /EHsc)комутатори MSVC2019 . Також на вбудованих системах багато хто вирішує не платити податки на продуктивність за винятки на C ++. Тож речення, що починається з "Але ви втрачаєте std :: bad_alloc ...", є сумнівним.
BitTickler
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.