Как работают free и malloc в C?

Я пытаюсь выяснить, что произойдет, если я попытаюсь освободить указатель "из середины" например, посмотрите на следующий код:

char *ptr = (char*)malloc(10*sizeof(char));

for (char i=0 ; i<10 ; ++i)
{
    ptr[i] = i+10;
}
++ptr;
++ptr;
++ptr;
++ptr;
free(ptr);

Я получаю сообщение об ошибке с сообщением об ошибке Unhandled exception. Я хочу понять, почему и как работает бесплатный, чтобы я знал не только, как его использовать, но и уметь понимать странные ошибки и исключения и лучше отлаживать свой код ץ

Спасибо большое

Ответ 1

Когда вы malloc блок, он фактически выделяет немного больше памяти, чем вы просили. Эта дополнительная память используется для хранения информации, такой как размер выделенного блока, и ссылка на следующий свободный/используемый блок в цепочке блоков, а иногда и на некоторые "данные защиты", которые помогают системе обнаруживать, что вы пишете прошлое конец вашего выделенного блока. Кроме того, большинство распределителей будут округлять общий размер и/или начало вашей части памяти до нескольких байтов (например, в 64-битной системе он может выровнять данные до кратного 64 бита (8 байтов), как доступ к данным из не выровненных адресов может быть более сложным и неэффективным для процессора/шины), поэтому вы также можете получить некоторые "отступы" (неиспользуемые байты).

Когда вы освобождаете указатель, он использует этот адрес, чтобы найти специальную информацию, добавленную в начало (обычно) вашего выделенного блока. Если вы передадите другой адрес, он получит доступ к памяти, содержащей мусор, и, следовательно, ее поведение undefined (но чаще всего приведет к сбою)

Позже, если вы освободите() блок, но не "забудете" свой указатель, вы можете случайно попытаться получить доступ к данным через этот указатель в будущем, а поведение - undefined. Возможна любая из следующих ситуаций:

  • память может быть помещена в список свободных блоков, поэтому, когда вы обращаетесь к ней, все равно будет содержать данные, которые вы оставили там, и ваш код работает нормально.
  • распределитель памяти, возможно, дал (часть) памяти другой части вашей программы, и это, вероятно, будет затем перезаписано (некоторые из) ваших старых данных, поэтому, когда вы его прочитаете, вы получите мусор, который может вызывают неожиданное поведение или сбои в вашем коде. Или вы напишете другие данные, в результате чего другая часть вашей программы будет вести себя странно в какой-то момент в будущем.
  • память могла быть возвращена в операционную систему ( "страница" памяти, которую вы больше не используете, может быть удалена из вашего адресного пространства, поэтому на этом адресе больше нет памяти - по существу неиспользуемый "дыра" в памяти приложения). Когда ваше приложение пытается получить доступ к данным, произойдет ошибка жесткой памяти и убейте ваш процесс.

Вот почему важно убедиться, что вы не используете указатель после освобождения памяти, на которую он указывает, - лучше всего это установить указатель на NULL после освобождения памяти, потому что вы можете легко протестировать NULL и попытка получить доступ к памяти с помощью указателя NULL вызовет плохое, но последовательное поведение, которое гораздо легче отлаживать.

Ответ 2

Вероятно, вы знаете, что вы должны точно передать указатель, который вы получили.

Поскольку free() не знает сначала, насколько велик ваш блок, ему нужна вспомогательная информация, чтобы идентифицировать исходный блок с его адреса, а затем вернуть его в свободный список. Он также попытается объединить небольшие освобожденные блоки с соседями, чтобы создать более ценный большой свободный блок.

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

Я опишу три способа сделать это.

  • Одно очевидное место - хранить его непосредственно перед возвращаемым указателем. Он мог бы выделить блок, который на несколько байтов больше, чем требуется, сохранить размер в первом слове, а затем вернуть вам указатель на второе слово.

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

  • Реализация может получить некоторую информацию с адреса, а часть - с карты. Ядерный распределитель 4.3BSD (называемый, я думаю, "распределитель McKusick-Karel" ) обеспечивает выделение ресурсов из двух блоков для объектов размером меньше, чем размер страницы, и сохраняет только размер каждой страницы, делая все распределения с заданной страницы один размер.

Было бы возможно, если бы некоторые типы второго и, вероятно, любого типа третьего типа распределителя фактически обнаруживали, что вы продвинули указатель, и DTRT, хотя я сомневаюсь, что какая-либо реализация будет записывать время выполнения, чтобы сделать это.

Ответ 3

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

Если ваш пример, когда вы выделяете 10 байт памяти, система фактически резервирует, скажем, 14. Первые 4 содержат количество запрошенных вами данных (10), а затем возвращаемое значение malloc является указатель на первый байт неиспользуемых данных в 14 выделенных.

Когда вы вызываете free на этом указателе, система будет искать 4 байта назад, чтобы знать, что изначально было выделено 14 байтов, чтобы он знал, сколько свободного. Эта система запрещает вам предоставлять объем данных бесплатно в качестве дополнительного параметра самому free.

Конечно, другая реализация malloc/free может выбрать другой способ достижения этого. Но они обычно не поддерживают free на другом указателе, чем то, что было возвращено malloc или эквивалентной функцией.

Ответ 4

Это поведение undefined - не делайте этого. Только указатели free(), полученные из malloc(), никогда не корректируют их до этого.

Задача free() должна быть очень быстрой, поэтому она не пытается найти выделение, к которому относится ваш отрегулированный адрес, но вместо этого пытается вернуть блок с точно скорректированным адресом в кучу. Это приводит к поведению undefined - обычно повреждение кучи или сбой программы.

Ответ 5

От http://opengroup.org/onlinepubs/007908775/xsh/free.html

Функция free() вызывает освобождение пространства, на которое указывает ptr; то есть, доступный для дальнейшего распределения. Если ptr является нулевым указателем, никаких действий не происходит. В противном случае, если аргумент не соответствует указателю, ранее возвращенному функциями calloc(), malloc(), realloc() или valloc(), или если пространство освобождается вызовом free() или realloc(), поведение undefined. Любое использование указателя, который ссылается на освобожденное пространство, вызывает поведение undefined.

Ответ 6

Вы освобождаете неправильный адрес. Изменяя значение ptr, вы меняете адрес. free не имеет способа узнать, что он должен попытаться освободить блок, начиная с 4 байтов назад. Сохраните исходный указатель без изменений и освободите его, а не управляемый. Как отмечали другие, результаты выполнения ваших действий - "undefined"... следовательно, необработанное исключение.

Ответ 7

Никогда не делайте этого.

Вы освобождаете неправильный адрес. Изменяя значение ptr, вы меняете адрес. free не имеет способа узнать, что он должен попытаться освободить блок, начиная с 4 байтов назад. Сохраните исходный указатель без изменений и освободите его, а не управляемый. Как указывали другие, результаты выполнения ваших действий "undefined"... следовательно, необработанное исключение

Ответ 8

Взято из книги: Понимание и использование указателей C

Когда выделена память, дополнительная информация сохраняется как часть структуры данных, поддерживаемой менеджером кучи. Эта информация включает, помимо прочего, размер блоков и обычно размещается непосредственно рядом с выделенным блоком.