Динамическое распределение памяти в языке C относится к выполнению ручного управления памятью для динамического распределения памяти в языке программирования C с помощью группы функций в стандартной библиотеке C , а именно malloc , realloc , calloc , align_alloc и free . [1] [2] [3]
Язык программирования C++ включает эти функции; однако операторы new и delete предоставляют схожую функциональность и рекомендуются авторами этого языка. [4] Тем не менее, есть несколько ситуаций, в которых использование / неприменимо, например, код сборки мусора или чувствительный к производительности код, и может потребоваться комбинация и размещения вместо оператора более высокого уровня.new
delete
malloc
new
new
Доступно множество различных реализаций фактического механизма выделения памяти, используемого malloc . Их производительность различается как по времени выполнения, так и по требуемой памяти.
Язык программирования C управляет памятью статически , автоматически или динамически . Переменные статической длительности выделяются в основной памяти, обычно вместе с исполняемым кодом программы, и сохраняются в течение всего срока службы программы; переменные автоматической длительности выделяются в стеке и появляются и исчезают по мере вызова и возврата функций. Для переменных статической и автоматической длительности размер выделения должен быть постоянным во время компиляции (за исключением случая автоматических массивов переменной длины [5] ). Если требуемый размер неизвестен до времени выполнения (например, если данные произвольного размера считываются от пользователя или из файла на диске), то использование объектов данных фиксированного размера неадекватно.
Время жизни выделенной памяти также может вызывать беспокойство. Ни статическая, ни автоматическая память не подходят для всех ситуаций. Автоматически выделенные данные не могут сохраняться при множественных вызовах функций, в то время как статические данные сохраняются в течение всего срока службы программы, независимо от того, нужны они или нет. Во многих ситуациях программисту требуется большая гибкость в управлении временем жизни выделенной памяти.
Эти ограничения обходят с помощью динамического выделения памяти , при котором память управляется более явно (но более гибко), обычно путем ее выделения из свободного хранилища (неофициально называемого «кучей»), [ требуется ссылка ] области памяти, структурированной для этой цели. В языке C библиотечная функция malloc
используется для выделения блока памяти в куче. Программа обращается к этому блоку памяти через указатель , который malloc
возвращает. Когда память больше не нужна, указатель передается в , free
который освобождает память, чтобы ее можно было использовать для других целей.
Первоначальное описание C указывало, что calloc
и cfree
были в стандартной библиотеке, но не malloc
. Код для простой реализации модели менеджера хранения для Unix был предоставлен с alloc
и free
как функции пользовательского интерфейса и с использованием sbrk
системного вызова для запроса памяти из операционной системы. [6] В документации Unix 6-го издания alloc
и указаны free
как низкоуровневые функции выделения памяти. [7] Процедуры malloc
и free
в их современной форме полностью описаны в руководстве Unix 7-го издания. [8] [9]
Некоторые платформы предоставляют библиотечные или встроенные вызовы функций , которые позволяют динамическое выделение памяти во время выполнения из стека C, а не из кучи (например, alloca()
[10] ). Эта память автоматически освобождается, когда вызывающая функция завершается.
Функции динамического выделения памяти в языке C определены в stdlib.h
заголовке ( cstdlib
header в C++). [1]
malloc()
принимает один аргумент (объем выделяемой памяти в байтах), а calloc()
принимает два аргумента — количество элементов и размер каждого элемента.malloc()
только выделяет память, при этом calloc()
выделяет и устанавливает байты в выделенной области в ноль. [11]Создать массив из десяти целых чисел с автоматической областью действия в языке C просто:
массив целых чисел [ 10 ];
Однако размер массива фиксируется во время компиляции. Если кто-то хочет динамически выделить подобный массив без использования массива переменной длины , поддержка которого не гарантируется во всех реализациях C11 , можно использовать следующий код:
int * array = malloc ( 10 * sizeof ( int ));
Он вычисляет количество байтов, которые десять целых чисел занимают в памяти, затем запрашивает это количество байтов malloc
и присваивает результат указателю с именем array
(благодаря синтаксису C указатели и массивы в некоторых ситуациях могут использоваться взаимозаменяемо).
Поскольку malloc
обработка запроса может оказаться невозможной, он может вернуть нулевой указатель , и хорошей практикой программирования является проверка на это:
int * array = malloc ( 10 * sizeof ( int )); если ( array == NULL ) { fprintf ( stderr , "malloc failed \n " ); вернуть -1 ; }
Когда программе больше не нужен динамический массив , она в конечном итоге должна вызвать функцию free
, чтобы вернуть занимаемую ею память в свободное хранилище:
бесплатно ( массив );
Память, выделенная с помощью , malloc
не инициализирована и может содержать cruft : остатки ранее использованных и отброшенных данных. После выделения с помощью malloc
элементы массива являются неинициализированными переменными . Команда calloc
вернет выделение, которое уже было очищено:
int * array = calloc ( 10 , sizeof ( int ));
С помощью realloc мы можем изменить размер памяти, на которую указывает указатель. Например, если у нас есть указатель, действующий как массив размера , и мы хотим изменить его на массив размера , мы можем использовать realloc.
int * arr = malloc ( 2 * sizeof ( int )); arr [ 0 ] = 1 ; arr [ 1 ] = 2 ; arr = realloc ( arr , 3 * sizeof ( int )); arr [ 2 ] = 3 ;
Обратите внимание, что необходимо предположить, что realloc изменил базовый адрес блока (т. е. если ему не удалось расширить размер исходного блока, и поэтому он выделил новый больший блок в другом месте и скопировал в него старое содержимое). Поэтому любые указатели на адреса внутри исходного блока также больше недействительны.
malloc
возвращает указатель void ( void *
), который указывает, что это указатель на область неизвестного типа данных. Использование приведения требуется в C++ из-за строгой системы типов, тогда как в C это не так. Можно «привести» (см. преобразование типов ) этот указатель к определенному типу:
int * ptr , * ptr2 ; ptr = malloc ( 10 * sizeof ( * ptr )); /* без приведения */ ptr2 = ( int * ) malloc ( 10 * sizeof ( * ptr )); /* с приведением */
Выполнение такого гипсования имеет свои преимущества и недостатки.
malloc
которые изначально возвращали char *
. [12]malloc()
(хотя современные компиляторы и статические анализаторы могут предупреждать о таком поведении, не требуя приведения типов [13] ).stdlib.h
, в котором находится прототип функции для . [12] [14] При отсутствии прототипа для стандарт C90 требует, чтобы компилятор C предполагал, что возвращает . Если приведения нет, C90 требует диагностику, когда это целое число назначается указателю; однако с приведением эта диагностика не будет выдаваться, скрывая ошибку. На некоторых архитектурах и моделях данных (например, LP64 на 64-битных системах, где и указатели являются 64-битными, а является 32-битными) эта ошибка может фактически привести к неопределенному поведению, поскольку неявно объявленная функция возвращает 32-битное значение, тогда как фактически определенная функция возвращает 64-битное значение. В зависимости от соглашений о вызовах и структуры памяти это может привести к разрушению стека . Эта проблема с меньшей вероятностью останется незамеченной в современных компиляторах, поскольку C99 не допускает неявных объявлений, поэтому компилятор должен выдать диагностику, даже если он предполагает return.malloc
malloc
malloc
int
long
int
malloc
int
malloc
он вызывается и приводится.Неправильное использование динамического распределения памяти часто может быть источником ошибок. К ним относятся ошибки безопасности или сбои программ, чаще всего из-за ошибок сегментации .
Наиболее распространенные ошибки следующие: [15]
free
приводит к накоплению неиспользуемой памяти, которая больше не используется программой. Это тратит ресурсы памяти впустую и может привести к сбоям выделения, когда эти ресурсы исчерпаны.malloc
, использование для хранения данных, освобождение с помощью free
. Несоблюдение этого шаблона, например, использование памяти после вызова free
( висячий указатель ) или перед вызовом malloc
( дикий указатель ), вызов free
дважды («двойное освобождение») и т. д., обычно приводит к ошибке сегментации и приводит к сбою программы. Эти ошибки могут быть временными и сложными для отладки — например, освобожденная память обычно не сразу возвращается ОС, и, таким образом, висячие указатели могут сохраняться некоторое время и казаться работающими.Кроме того, как интерфейс, предшествующий стандартизации ANSI C, malloc
и его связанные функции имеют поведение, которое было намеренно оставлено на усмотрение реализации для определения его самой. Одним из них является выделение нулевой длины, что является большей проблемой, realloc
поскольку чаще всего размер изменяется до нуля. [16] Хотя и POSIX , и Single Unix Specification требуют надлежащей обработки выделений нулевого размера либо путем возврата NULL
, либо чего-то другого, что может быть безопасно освобождено, [17] не все платформы обязаны соблюдать эти правила. Среди множества ошибок двойного освобождения, к которым это привело, особенно заметной была ошибка WhatsApp RCE 2019 года. [18] Способ обернуть эти функции, чтобы сделать их более безопасными, — просто проверить выделения памяти размером 0 и превратить их в выделения размером 1. (Возврат NULL
имеет свои собственные проблемы: в противном случае он указывает на ошибку нехватки памяти. В случае realloc
это означало бы, что исходная память не была перемещена и освобождена, что опять же не относится к размеру 0, что приводит к двойному освобождению.) [19]
Реализация управления памятью во многом зависит от операционной системы и архитектуры. Некоторые операционные системы предоставляют распределитель для malloc, в то время как другие предоставляют функции для управления определенными областями данных. Один и тот же динамический распределитель памяти часто используется для реализации как malloc
и оператора new
в C++ . [20]
Реализация устаревших распределителей обычно выполнялась с использованием сегмента кучи . Распределитель обычно расширял и сжимал кучу для выполнения запросов на выделение.
Метод кучи имеет несколько неотъемлемых недостатков:
Дуг Ли разработал общедоступный dlmalloc («Malloc Дуга Ли») как универсальный распределитель, начиная с 1987 года. Библиотека GNU C (glibc) является производной от ptmalloc Вольфрама Глогера («pthreads malloc»), ответвления dlmalloc с улучшениями, связанными с потоками. [21] [22] [23] По состоянию на ноябрь 2023 года последней версией dlmalloc является версия 2.8.6 от августа 2012 года. [24]
dlmalloc — это граничный тег-аллокатор. Память в куче выделяется в виде «кусков», 8-байтовой выровненной структуры данных , которая содержит заголовок и полезную память. Выделенная память содержит 8- или 16-байтовые накладные расходы для размера куска и флагов использования (аналогично dope-вектору ). Невыделенные куски также хранят указатели на другие свободные куски в области полезного пространства, делая минимальный размер куска 16 байт в 32-битных системах и 24/32 (в зависимости от выравнивания) байта в 64-битных системах. [22] [24] : 2.8.6, Минимальный выделенный размер
Нераспределенная память группируется в « корзины » схожих размеров, реализуемые с помощью двухсвязного списка фрагментов (с указателями, хранящимися в нераспределенном пространстве внутри фрагмента). Корзины сортируются по размеру на три класса: [22] [24] : Наложенные структуры данных
Разработчик игр Эдриан Стоун утверждает, что dlmalloc
, как распределитель граничных тегов, недружелюбен к консольным системам, которые имеют виртуальную память, но не имеют подкачки по требованию . Это связано с тем, что его обратные вызовы сокращения и роста пула ( sysmalloc
/ systrim
) не могут использоваться для выделения и фиксации отдельных страниц виртуальной памяти. При отсутствии подкачки по требованию фрагментация становится более серьезной проблемой. [27]
Начиная с FreeBSD 7.0 и NetBSD 5.0, старая malloc
реализация ( phkmalloc
от Poul-Henning Kamp ) была заменена на jemalloc, написанную Jason Evans. Основной причиной этого была недостаточная масштабируемость phkmalloc
с точки зрения многопоточности. Чтобы избежать конфликта блокировок, jemalloc
использует отдельные «арены» для каждого CPU . Эксперименты по измерению количества выделений в секунду в многопоточном приложении показали, что это делает его масштабируемым линейно с количеством потоков, в то время как для phkmalloc и dlmalloc производительность была обратно пропорциональна количеству потоков. [28]
Реализация функции OpenBSDmalloc
использует mmap . Для запросов, размер которых превышает одну страницу, все выделение извлекается с помощью mmap
; меньшие размеры назначаются из пулов памяти, поддерживаемых в malloc
пределах ряда «страниц корзины», также выделенных с помощью mmap
. [29] [ требуется лучший источник ] При вызове free
память освобождается и отменяет отображение из адресного пространства процесса с помощью munmap
. Эта система разработана для повышения безопасности за счет использования преимуществ рандомизации макета адресного пространства и функций gap page, реализованных как часть mmap
системного вызова OpenBSD , а также для обнаружения ошибок использования после освобождения — поскольку большое выделение памяти полностью отменяется после освобождения, дальнейшее использование вызывает ошибку сегментации и завершение программы.
Проект GrapheneOS изначально начинался с переноса распределителя памяти OpenBSD в библиотеку Bionic C Android. [30]
Hoard — это распределитель, целью которого является масштабируемая производительность выделения памяти. Как и распределитель OpenBSD, Hoard использует mmap
исключительно, но управляет памятью в кусках по 64 килобайта, называемых суперблоками. Куча Hoard логически разделена на одну глобальную кучу и несколько куч на процессор. Кроме того, существует локальный кэш потока, который может содержать ограниченное количество суперблоков. Выделяя только из суперблоков в локальной куче на поток или на процессор и перемещая в основном пустые суперблоки в глобальную кучу, чтобы их могли повторно использовать другие процессоры, Hoard сохраняет низкую фрагментацию, достигая при этом почти линейной масштабируемости с числом потоков. [31]
Компактный распределитель памяти общего назначения с открытым исходным кодом от Microsoft Research , ориентированный на производительность. [32] Библиотека содержит около 11 000 строк кода .
Каждый поток имеет локальное хранилище потока для небольших выделений. Для больших выделений можно использовать mmap или sbrk . TCMalloc, malloc, разработанный Google, [33] имеет сборку мусора для локального хранения мертвых потоков. Считается, что TCMalloc более чем в два раза быстрее ptmalloc из glibc для многопоточных программ. [34] [35]
Ядрам операционных систем необходимо выделять память так же, как это делают прикладные программы. Однако реализация malloc
в ядре часто существенно отличается от реализаций, используемых библиотеками C. Например, буферы памяти могут нуждаться в соответствии со специальными ограничениями, налагаемыми DMA , или функция выделения памяти может вызываться из контекста прерывания. [36] Это требует malloc
реализации, тесно интегрированной с подсистемой виртуальной памяти ядра операционной системы.
Поскольку malloc
и его родственники могут оказывать сильное влияние на производительность программы, не редкость переопределять функции для конкретного приложения с помощью пользовательских реализаций, оптимизированных для шаблонов распределения приложения. Стандарт C не предоставляет способа сделать это, но операционные системы нашли различные способы сделать это, используя динамическое связывание. Один из способов — просто связать другую библиотеку, чтобы переопределить символы. Другой, используемый в Unix System V.3 , — создать указатели функций malloc
и free
, которые приложение может сбросить до пользовательских функций. [37]
Наиболее распространенной формой в системах типа POSIX является установка переменной среды LD_PRELOAD с путем к распределителю, чтобы динамический компоновщик использовал эту версию malloc/calloc/free вместо реализации libc.
Максимально возможный блок памяти, malloc
который можно выделить, зависит от хост-системы, в частности от размера физической памяти и реализации операционной системы.
Теоретически, наибольшее число должно быть максимальным значением, которое может храниться в типе size_t
, который является беззнаковым целым числом, зависящим от реализации и представляющим размер области памяти. В стандарте C99 и более поздних версиях оно доступно как SIZE_MAX
константа из . Хотя это и не гарантируется ISO C , обычно оно равно .<stdint.h>
2^(CHAR_BIT * sizeof(size_t)) - 1
В системах glibc максимально возможный блок памяти, malloc
который можно выделить, составляет всего лишь половину этого размера, а именно . [38]2^(CHAR_BIT * sizeof(ptrdiff_t) - 1) - 1
Реализации библиотеки C, поставляемые с различными операционными системами и компиляторами, могут поставляться с альтернативами и расширениями стандартного malloc
интерфейса. Среди них следует отметить:
alloca
, который выделяет запрошенное количество байтов в стеке вызовов . Соответствующей функции освобождения не существует, поскольку обычно память освобождается сразу после возврата вызывающей функции. alloca
присутствовал в системах Unix еще в 32/V (1978), но его использование может быть проблематичным в некоторых (например, встроенных) контекстах. [39] Хотя он поддерживается многими компиляторами, он не является частью стандарта ANSI-C и, следовательно, не всегда может быть переносимым. Он также может вызывать незначительные проблемы с производительностью: он приводит к кадрам стека переменного размера, поэтому необходимо управлять как указателями стека, так и указателями фрейма (при кадрах стека фиксированного размера один из них является избыточным). [40] Более крупные выделения также могут увеличить риск неопределенного поведения из-за переполнения стека . [41] C99 предлагал массивы переменной длины в качестве альтернативного механизма выделения стека — однако эта функция была отнесена к необязательной в более позднем стандарте C11 .posix_memalign
, которая выделяет память с выравниванием, указанным вызывающей стороной. Ее выделения освобождаются с помощью free
, [42] , поэтому реализация обычно должна быть частью библиотеки malloc.calloc
и cfree
, а Раздел 8.7 (стр. 173) описывает реализацию для alloc
и free
.man
Страница для malloc
и т. д. указана на странице 275.