Як delete [] знає, що це масив?


136

Добре, я думаю, що всі ми згодні з тим, що те, що відбувається із наступним кодом, не визначено, залежно від того, що передано,

void deleteForMe(int* pointer)
{
     delete[] pointer;
}

Вказівник може бути різними різними речами, і тому виконання безумовного delete[]на ньому не визначено. Однак припустимо, що ми дійсно передаємо вказівник масиву,

int main()
{
     int* arr = new int[5];
     deleteForMe(arr);
     return 0;
}

Моє запитання в цьому випадку, коли вказівник є масивом, хто це знає? Я маю на увазі, з точки зору мови / компілятора, він не має поняття, є чи ні arrвказівник масиву проти вказівника на один int. Чорт, навіть не знає, чи arrбуло створено динамічно. Але якщо я роблю наступне,

int main()
{
     int* num = new int(1);
     deleteForMe(num);
     return 0;
}

Операційна система достатньо розумна, щоб видалити лише один int і не переходити на якийсь тип "вбивства", видаляючи решту пам'яті поза цією точкою (на відміну від цієї strlenта не закінченої \0рядка - вона продовжуватиме роботу, поки вона не буде звернень 0).

То чия робота - це пам’ятати ці речі? Чи зберігає ОС деякий тип запису у фоновому режимі? (Я маю на увазі, я розумію, що я почав цю посаду, сказавши, що те, що відбувається, не визначено, але факт полягає в тому, що сценарій "вбивства" не відбувається, тому в практичному світі хтось пам'ятає.)



6
це знає з квадратних дужок після видалення
JoelFan

"покажчик - це масив". Ні, вказівники ніколи не є масивами. Вони часто вказують на перший елемент масиву, але це інша річ.
Аарон Мак-Дейд

Відповіді:


99

Компілятор не знає, що це масив, він довіряє програмісту. Видалення вказівника на одиницю intз delete []приведе до невизначеної поведінки. Ваш другий main()приклад небезпечний, навіть якщо це не відразу виходить з ладу.

Компілятор повинен відслідковувати, скільки об’єктів потрібно якось видалити. Це може зробити, перерозподіливши достатньо для зберігання розміру масиву. Детальніше дивіться у розділі Поширені запитання про C ++ Super .


14
Власне, використання delete [] для видалення чогось створеного з новим є корисним. taossa.com/index.php/2007/01/03/…
Родріго

23
@Rodrigo Посилання у вашому коментарі розірвано, але, на щастя, автомат зворотного зв'язку має його копію за адресою replay.web.archive.org/20080703153358/http://taossa.com/…
Девід Гарднер

103

Одне запитання, на яке дано відповіді, на сьогодні, схоже, не відповідає: якщо бібліотеки часу виконання (не дійсно ОС) можуть відслідковувати кількість речей у масиві, то навіщо нам взагалі потрібен delete[]синтаксис? Чому не deleteможна використовувати одну форму для обробки всіх видалених?

Відповідь на це сягає коріння C ++ як мови, сумісної з C (якою вона більше не прагне бути.) Філософія Струструпа полягала в тому, що програмісту не слід платити за будь-які функції, якими вони не користуються. Якщо вони не використовують масиви, вони не повинні переносити вартість масивів об'єктів за кожен виділений фрагмент пам'яті.

Тобто, якщо ваш код просто так

Foo* foo = new Foo;

то місце для пам'яті, на яке виділено, fooне повинно містити зайвих накладних витрат, необхідних для підтримки масивів Foo.

Оскільки для розміщення інформації про додатковий розмір масиву встановлюються лише виділення масивів, то вам потрібно повідомити бібліотеки часу виконання, щоб шукати цю інформацію під час видалення об'єктів. Ось чому нам потрібно користуватися

delete[] bar;

замість просто

delete bar;

якщо бар - вказівник на масив.

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


20
"метушня щодо кількох зайвих байтів пам'яті в наші дні здається вигадливою". На щастя, для таких людей голі масиви також починають виглядати химерно, тому вони можуть просто використовувати вектор або примножувати :: масив, і забути про видалення [] назавжди :-)
Стів Джессоп

28

Так, ОС зберігає деякі речі на "тлі". Наприклад, якщо ти біжиш

int* num = new int[5];

ОС може виділити 4 зайвих байта, зберегти розмір виділення в перших 4 байтах виділеної пам'яті і повернути покажчик зміщення (тобто він виділяє простори пам’яті від 1000 до 1024, але покажчик повертає точки на 1004, з розташуванням 1000- 1003 зберігання розміру виділення). Потім, коли виклик видалення, він може переглянути 4 байти, перш ніж покажчик перейшов до нього, щоб знайти розмір виділення.

Я впевнений, що є й інші способи відстеження розміру виділення, але це один варіант.


26
+1 - дійсна точка загалом, за винятком того, що зазвичай за час виконання мови відповідає за зберігання цих метаданих, а не ОС.
гострий зуб

Що відбувається з розміром масиву або розміром об'єкта, для якого визначений масив? Чи показує він додаткові 4 байти, коли ви робите sizeof для цього об’єкта?
Шрі

3
Ні, sizeof показує просто розмір масиву. Якщо час виконання програми вирішить реалізувати його без описаного нами методу, це суто деталі реалізації та з точки зору користувача, які слід замаскувати. Пам'ять перед вказівником не "належить" користувачеві і не враховується.
bsdfish

2
Що ще важливіше, sizeof не поверне справжній розмір динамічно розподіленого масиву ні в якому разі. Він може повертати лише розміри, відомі під час компіляції.
bdonlan

Чи можливо використовувати ці метадані в циклі for для точного циклу на масив? наприклад for(int i = 0; i < *(arrayPointer - 1); i++){ }
Сем

13

Це дуже схоже на це питання, і в ньому є багато деталей, які ви шукаєте.

Але достатньо сказати, що завдання ОС відслідковувати щось із цього не має. Насправді бібліотеки часу виконання або основний менеджер пам'яті відстежуватиме розмір масиву. Зазвичай це відбувається шляхом виділення додаткової пам’яті на передню частину і збереження розміру масиву в цьому місці (більшість використовують головний вузол).

Це можна побачити в деяких реалізаціях, виконавши наступний код

int* pArray = new int[5];
int size = *(pArray-1);

це буде працювати? У Windows та Linux ми цього не працювали.
приятель

1
спробуйте size_t size = *(reinterpret_cast<size_t *>(pArray) - 1)замість цього

9

deleteабо delete[], ймовірно, обидва б звільнили виділену пам'ять (вказано в пам'яті), але велика різниця полягає в тому, що deleteв масиві не буде викликатися деструктор кожного елемента масиву.

У всякому разі, змішування new/new[]і delete/delete[], мабуть, UB.


1
Чітка, коротка та найкорисніша відповідь!
GntS

6

Він не знає, що це масив, тому вам доведеться постачати delete[]замість звичайного старого delete.


5

У мене було подібне питання до цього. У C ви виділяєте пам'ять за допомогою malloc () (або іншої подібної функції) та видаляєте її безкоштовно (). Є лише один malloc (), який просто виділяє певну кількість байтів. Є лише один вільний (), який просто приймає вказівник як його параметр.

То чому в C ви можете просто здати покажчик безкоштовно, а в C ++ ви повинні сказати йому, чи це масив чи одна змінна?

Відповідь, яку я дізнався, стосується деструкторів класів.

Якщо ви виділите екземпляр класу MyClass ...

classes = new MyClass[3];

І видалити його з видаленням, ви можете отримати деструктор лише для першого виклику MyClass. Якщо ви використовуєте delete [], ви можете бути впевнені, що деструктор буде викликаний для всіх екземплярів масиву.

ЦЕ - важлива відмінність. Якщо ви просто працюєте зі стандартними типами (наприклад, int), ви справді не побачите цю проблему. Плюс слід пам’ятати, що поведінка щодо використання delete on new [] та delete [] on new не визначена - це може не працювати однаково у кожному компіляторі / системі.


3

Це залежить від часу виконання, який відповідає за розподіл пам'яті, так само, як ви можете видалити масив, створений malloc у стандартному C, використовуючи безкоштовно. Я думаю, кожен компілятор реалізує це по-різному. Один поширений спосіб - виділити додаткову комірку для розміру масиву.

Однак час виконання не є достатньо розумним, щоб виявити, чи це масив чи покажчик, вам потрібно повідомити про це, і якщо ви помиляєтесь, ви або не видаляєте правильно (наприклад, ptr замість масиву), або ви в кінцевому підсумку приймаєте незв'язане значення для розміру і завдаєте значної шкоди.


3

Один із підходів для компіляторів - виділити трохи більше пам’яті та зберегти кількість елементів у головному елементі.

Приклад, як це можна зробити: Ось

int* i = new int[4];

компілятор виділить sizeof (int) * 5 байт.

int *temp = malloc(sizeof(int)*5)

Збережеться 4в перших sizeof(int)байтах

*temp = 4;

і встановити i

i = temp + 1;

Отже, iвказує на масив з 4 елементів, а не 5.

І

delete[] i;

буде оброблено наступним чином

int *temp = i - 1;
int numbers_of_element = *temp; // = 4
... call destructor for numbers_of_element elements if needed
... that are stored in temp + 1, temp + 2, ... temp + 4
free (temp)

1

Семантично обидві версії оператора видалення в C ++ можуть "з'їсти" будь-який вказівник; однак, якщо вказано на вказівник на один об’єкт delete[], то UB призведе до цього, тобто будь-що може статися, включаючи збої системи або взагалі нічого.

C ++ вимагає, щоб програміст обрав належну версію оператора видалення залежно від предмета розстановки: масив або один об'єкт.

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


1

Погодьтеся, що компілятор не знає, чи це масив чи ні. Це залежить від програміста.

Іноді компілятор відстежує, скільки об’єктів потрібно видалити, перерозподіливши достатньо для зберігання розміру масиву, але це не завжди необхідно.

Для отримання повної специфікації, коли виділено додаткове сховище, зверніться до C ++ ABI (як реалізуються компілятори): Itanium C ++ ABI: Оператор масиву, нові кукі


Я тільки хочу кожен компілятор спостерігається деякий документований ABI для C ++. +1 за посиланням, яке я відвідував раніше. Дякую.
Дон Уейкфілд

0

Ви не можете використовувати delete для масиву, і ви не можете використовувати delete [] для не масиву.


8
Я думаю , що ви маєте в виду слід НЕ так , як ваш середній компілятор не збирається виявити зловживання.
Дон Уейкфілд

0

"невизначена поведінка" просто означає, що мовна специфіка не дає гарантій щодо того, що станеться. Це не означає, що трапиться щось погане.

То чия робота - запам’ятати ці речі? Чи зберігає ОС деякий тип запису у фоновому режимі? (Я маю на увазі, я розумію, що я почав цю посаду, сказавши, що те, що відбувається, не визначено, але факт полягає в тому, що сценарій "вбивства" не відбувається, тому в практичному світі хтось пам'ятає.)

Тут зазвичай є два шари. Основний менеджер пам'яті та реалізація C ++.

Загалом менеджер пам'яті запам'ятає (серед іншого) розмір виділеного блоку пам'яті. Це може бути більше, ніж блок, про який вимагає реалізація C ++. Зазвичай менеджер пам'яті зберігає його метадані перед виділеним блоком пам'яті.

Реалізація C ++, як правило, запам'ятовує розмір масиву лише в тому випадку, якщо це потрібно зробити для власних цілей, як правило, тому, що тип має нетривіальний деструктор.

Отже, для типів із тривіальним деструктором реалізація "delete" та "delete []" зазвичай однакова. Реалізація C ++ просто передає вказівник на основний менеджер пам'яті. Щось на зразок

free(p)

З іншого боку, для типів із нетривіальним деструктором "delete" та "delete []", ймовірно, будуть різними. "delete" буде щось подібне (де T - тип, на який вказує вказівник)

p->~T();
free(p);

Хоча "delete []" буде чимось подібним.

size_t * pcount = ((size_t *)p)-1;
size_t count = *count;
for (size_t i=0;i<count;i++) {
  p[i].~T();
}
char * pmemblock = ((char *)p) - max(sizeof(size_t),alignof(T));
free(pmemblock);

-1

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

// overloaded new expression 
void* operator new[]( size_t size )
{
    // allocate 4 bytes more see comment below 
    int* ptr = (int*)malloc( size + 4 );

    // set value stored at address to 0 
    // and shift pointer by 4 bytes to avoid situation that
    // might arise where two memory blocks 
    // are adjacent and non-zero
    *ptr = 0;
    ++ptr; 

    return ptr;
}
//////////////////////////////////////////

// overloaded delete expression 
void static operator delete[]( void* ptr )
{
    // decrement value of pointer to get the
    // "Real Pointer Value"
    int* realPtr = (int*)ptr;
    --realPtr;

    free( realPtr );
}
//////////////////////////////////////////

// Template used to call destructor if needed 
// and call appropriate delete 
template<class T>
void Deallocate( T* ptr )
{
    int* instanceCount = (int*)ptr;
    --instanceCount;

    if(*instanceCount > 0) // if larger than 0 array is being deleted
    {
        // call destructor for each object
        for(int i = 0; i < *instanceCount; i++)
        {
            ptr[i].~T();
        }
        // call delete passing instance count witch points
        // to begin of array memory 
        ::operator delete[]( instanceCount );
    }
    else
    {
        // single instance deleted call destructor
        // and delete passing ptr
        ptr->~T();
        ::operator delete[]( ptr );
    }
}

// Replace calls to new and delete
#define MyNew ::new
#define MyDelete(ptr) Deallocate(ptr)

// structure with constructor/ destructor
struct StructureOne
{
    StructureOne():
    someInt(0)
    {}
    ~StructureOne() 
    {
        someInt = 0;
    }

    int someInt;
};
//////////////////////////////

// structure without constructor/ destructor
struct StructureTwo
{
    int someInt;
};
//////////////////////////////


void main(void)
{
    const unsigned int numElements = 30;

    StructureOne* structOne = nullptr;
    StructureTwo* structTwo = nullptr;
    int* basicType = nullptr;
    size_t ArraySize = 0;

/**********************************************************************/
    // basic type array 

    // place break point here and in new expression
    // check size and compare it with size passed 
    // in to new expression size will be the same
    ArraySize = sizeof( int ) * numElements;

    // this will be treated as size rather than object array as there is no 
    // constructor and destructor. value assigned to basicType pointer
    // will be the same as value of "++ptr" in new expression
    basicType = MyNew int[numElements];

    // Place break point in template function to see the behavior
    // destructors will not be called and it will be treated as 
    // single instance of size equal to "sizeof( int ) * numElements"
    MyDelete( basicType );

/**********************************************************************/
    // structure without constructor and destructor array 

    // behavior will be the same as with basic type 

    // place break point here and in new expression
    // check size and compare it with size passed 
    // in to new expression size will be the same
    ArraySize = sizeof( StructureTwo ) * numElements;

    // this will be treated as size rather than object array as there is no 
    // constructor and destructor value assigned to structTwo pointer
    // will be the same as value of "++ptr" in new expression
    structTwo = MyNew StructureTwo[numElements]; 

    // Place break point in template function to see the behavior
    // destructors will not be called and it will be treated as 
    // single instance of size equal to "sizeof( StructureTwo ) * numElements"
    MyDelete( structTwo );

/**********************************************************************/
    // structure with constructor and destructor array 

    // place break point check size and compare it with size passed in
    // new expression size in expression will be larger by 4 bytes
    ArraySize = sizeof( StructureOne ) * numElements;

    // value assigned to "structOne pointer" will be different 
    // of "++ptr" in new expression  "shifted by another 4 bytes"
    structOne = MyNew StructureOne[numElements];

    // Place break point in template function to see the behavior
    // destructors will be called for each array object 
    MyDelete( structOne );
}
///////////////////////////////////////////

-2

просто визначте деструктор всередині класу та виконайте свій код із обох синтаксисів

delete pointer

delete [] pointer

відповідно до виводу u можна знайти рішення


використовуйте delete [] під час створення нового типу масиву. наприклад, int * a = new int; int * b = new int [5]; видалити a; видалити [] b;
Лінеєш К Мохан

-3

Відповідь:

int * pArray = new int [5];

int size = * (pArray-1);

Опублікований вище не є правильним і дає недійсне значення. "-1" підраховує елементи У 64-бітній ОС Windows правильний розмір буфера знаходиться в адресі Ptr - 4 байти

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.