Как delete [] знает, что это массив?

136

Хорошо, я думаю, что мы все согласны с тем, что то, что происходит со следующим кодом, не определено, в зависимости от того, что передано,

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

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

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

Мой вопрос, в этом случае, когда указатель является массивом, кто это знает? Я имею в виду, с точки зрения языка / компилятора, он понятия не имеет, arrявляется ли указатель массива по сравнению с указателем на одно целое. Черт возьми, он даже не знает, arrбыл ли он создан динамически. Тем не менее, если я сделаю следующее вместо этого,

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

ОС достаточно умен , чтобы удалить только один междунар и не идти на какой - то типа «Череда убийств», удалив остальную часть памяти за этой точкой (контраст , что с strlenи не- \0-завершённый строка - она будет продолжать идти до него Хиты 0).

Так чья работа помнить эти вещи? Сохраняет ли ОС какой-либо тип записи в фоновом режиме? (Я имею в виду, я понимаю, что начал этот пост, сказав, что то, что происходит, не определено, но на самом деле сценарий «убийства» не происходит, поэтому в практическом мире кто-то вспоминает.)

GRB
источник
6
это знает из квадратных скобок после удаления
JoelFan
«указатель является массивом». Нет, указатели никогда не являются массивами. Они часто указывают на первый элемент массива, но это совсем другое.
Аарон МакДейд

Ответы:

99

Компилятор не знает, что это массив, он доверяет программисту. Удаление указателя на одиночную intс delete []может привести к неопределенному поведению. Ваш второй main()пример небезопасен, даже если он не сразу падает.

Компилятор должен отслеживать, сколько объектов нужно каким-либо образом удалить. Это может быть сделано путем перераспределения, достаточного для хранения размера массива. Для получения более подробной информации см. C ++ Super FAQ .

Фред Ларсон
источник
14
На самом деле, использование delete [] для удаления чего-то, созданного с помощью new, пригодно для использования. 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
"суетливость в отношении нескольких лишних байтов памяти в наши дни кажется странной". К счастью, для таких людей голые массивы также начинают выглядеть странно, поэтому они могут просто использовать вектор или boost :: array и забыть об удалении [] навсегда :-)
Steve Jessop
28

Да, ОС хранит некоторые вещи в фоновом режиме. Например, если вы запустите

int* num = new int[5];

ОС может выделить 4 дополнительных байта, сохранить размер выделения в первых 4 байтах выделенной памяти и вернуть указатель смещения (т. е. она выделяет области памяти от 1000 до 1024, но возвращенный указатель указывает на 1004, с местоположениями 1000- 1003 хранения размера выделения). Затем, когда вызывается delete, он может просмотреть 4 байта, прежде чем указатель будет передан ему, чтобы найти размер выделения.

Я уверен, что есть другие способы отслеживания размера размещения, но это один из вариантов.

bsdfish
источник
26
+1 - допустимый пункт в целом, за исключением того, что обычно за сохранение этих метаданных отвечает язык среды выполнения, а не ОС.
sharptooth
Что происходит с размером массива или размером объекта, для которого определен массив? Показывает ли это дополнительные 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);
JaredPar
источник
это будет работать? В Windows и Linux мы не получили это работает.
приятель
1
попробуйте size_t size = *(reinterpret_cast<size_t *>(pArray) - 1)вместо этого
9

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

Во всяком случае, смешивание new/new[]и delete/delete[], вероятно, UB.

Benoît
источник
1
Четкий, краткий и самый полезный ответ!
GntS
6

Он не знает, что это массив, поэтому вы должны поставить delete[]вместо обычного старого delete.

eduffy
источник
5

У меня был похожий вопрос к этому. В C вы выделяете память с помощью malloc () (или другой аналогичной функции) и удаляете ее с помощью free (). Существует только один malloc (), который просто выделяет определенное количество байтов. Существует только одна функция free (), которая просто принимает указатель в качестве параметра.

Так почему же в C вы можете просто передать указатель на free, а в C ++ вы должны указать, является ли это массивом или единственной переменной?

Я узнал, что ответ связан с деструкторами классов.

Если вы выделите экземпляр класса MyClass ...

classes = new MyClass[3];

И удалить его с помощью delete, вы можете получить деструктор только для первого экземпляра MyClass. Если вы используете delete [], вы можете быть уверены, что деструктор будет вызываться для всех экземпляров в массиве.

Это важное отличие. Если вы просто работаете со стандартными типами (например, int), вы действительно не увидите эту проблему. Кроме того, вы должны помнить, что поведение при использовании delete для new [] и delete [] для new не определено - оно может работать не одинаково на всех компиляторах / системах.

ProdigySim
источник
3

Это зависит от времени выполнения, которое отвечает за распределение памяти, так же, как вы можете удалить массив, созданный с помощью malloc в стандартном C, используя free. Я думаю, что каждый компилятор реализует это по-своему. Один из распространенных способов - выделить дополнительную ячейку для размера массива.

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

Uri
источник
3

Один из подходов для компиляторов - выделить немного больше памяти и сохранить количество элементов в элементе head.

Пример того, как это можно сделать: здесь

int* i = new int[4];

Компилятор выделит размер (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)
Avt
источник
1

Семантически обе версии оператора удаления в C ++ могут «съесть» любой указатель; однако, если указан указатель на один объект delete[], то будет получен UB, означающий, что может произойти что угодно, в том числе системный сбой или вообще ничего.

C ++ требует, чтобы программист выбрал правильную версию оператора удаления в зависимости от предмета освобождения: массив или отдельный объект.

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

mloskot
источник
1

Согласитесь, что компилятор не знает, массив это или нет. Это зависит от программиста.

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

Чтобы получить полную спецификацию при выделении дополнительной памяти, обратитесь к C ++ ABI (как реализованы компиляторы): Itanium C ++ ABI: новые файлы cookie для оператора массива

Shibo
источник
Я только хотел бы, чтобы каждый компилятор соблюдал некоторые документированные ABI для C ++. +1 за ссылку, которую я посетил ранее. Спасибо.
Дон Уэйкфилд
0

Вы не можете использовать delete для массива, и вы не можете использовать delete [] для не-массива.

Дон Уэйкфилд
источник
8
Я думаю, что вы имеете в виду не должны , так как ваш средний компилятор не собирается обнаруживать злоупотребления.
Дон Уэйкфилд
0

«неопределенное поведение» просто означает, что спецификация языка не дает никаких гарантий относительно того, что произойдет. Это не обязательно означает, что случится что-то плохое.

Так чья работа помнить эти вещи? Сохраняет ли ОС какой-либо тип записи в фоновом режиме? (Я имею в виду, я понимаю, что начал этот пост, сказав, что то, что происходит, не определено, но на самом деле сценарий «убийства» не происходит, поэтому в практическом мире кто-то вспоминает.)

Здесь обычно два слоя. Базовый менеджер памяти и реализация C ++.

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

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

Таким образом, для типов с тривиальным деструктором реализация «delete» и «delete []» обычно одинакова. Реализация C ++ просто передает указатель на базовый менеджер памяти. Что-то вроде

free(p)

С другой стороны, для типов с нетривиальным деструктором «delete» и «delete []» могут быть разными. «delete» будет выглядеть примерно так (где T - тип, на который указывает указатель)

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

Хотя «удалить []» будет что-то вроде.

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);
plugwash
источник
-1

переберите массив объектов и вызовите деструктор для каждого из них. Я создал этот простой код, который перегружает выражения new [] и delete [] и предоставляет шаблонную функцию для освобождения памяти и вызова деструктора для каждого объекта при необходимости:

// 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

в соответствии с выходом вы можете найти решения

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

Ответ:

int * pArray = new int [5];

int size = * (pArray-1);

Вышеуказанное неверно и выдает неверное значение. «-1» считает элементы В 64-битной ОС Windows правильный размер буфера находится в Ptr - 4 байта адреса

Евгений Райхель
источник