Как работает память в c
Функция memset из заголовочного файла string.h позволяет инициализировать блок динамической памяти начальными значениями. Она имеет следующий прототип:
destination : указатель на блок выделенной памяти
value : значение, которым инициализуются значения в блоке выделенной памяти
N : размер блока выделенной памяти
Например, выделим блок памяти для 5 чисел типа int и инициализируем все байты нулями:
Стоит отметить, что инициализируются не числа int, которые здесь составляют массив ptr, а каждый байт блока памяти.
memcpy
Функция memcpy копирует определенное количество байт из одного блока памяти в другой. Она имеет следующий прототип:
destination : указатель на блок памяти, в который копируем
source : указатель на блок памяти, из которого копируем
N : количество копируемых байтов
Например, скопируем 5 байт из одного блока памяти в другой:
Стоит отметить, что указателя блока, в который копируются байты, может также указывать на блок динамически выделяемой памяти:
Также стоит отметить, что блоки памяти для копирования данных не обязательно представляют блок объектов типа char , они могут быть предназначены для любых других типов, например, int :
Другой пример — копирование структур:
Поскольку функция memcpy принимают указатели, то в примере выше в данную функцию передаются адреса структур.
Сравнение объектов. memcmp
Функция memcmp сравнивает определенное количество байт двух объектов. Она имеет следующий прототип:
Первый и второй параметры представляют указатели на блоки памяти, которые сравниваются. Последний параметр представляет количество байт блоков p1 и p2, которые сравниваются.
Функция возвращает 0, если N байтов обоих объектов равны.
Если первый несовпадающий байт первого блока памяти больше соответствующего байта из второго блока, то возвращается число больше 0.
Если первый несовпадающий байт первого блока памяти меньше соответствующего байта из второго блока, то возвращается число меньше 0.
Например, сравним два объекта структуры:
В данном случае сравниваем все байты обоих структур. Поскольку их значения равны, то функция memcmp возвратит 0.
Причем поскольку функция сравнивает последовательно соответствующие байты обоих объектов, а в структурах данные располагаются по порядку определения элементов, то может быть так, что окажутся равны объекты разных типов структур:
Здесь два объекта структур person и employee имееют одни и те же элементы и их значения. Соответственно функция memcmp возвратит 0, несмотря на то, что это разные структуры.
Еще один пример — сравненим массивы:
Здесь массивы не равны, соответственно функция возвратит число, которое не равно 0.
Name already in use
msu_cpp_lectures / 02.memory.md
- Go to file T
- Go to line L
- Copy path
- Copy permalink
- Open with Desktop
- View raw
- Copy raw contents Copy raw contents
Copy raw contents
Copy raw contents
Кеш, оперативная память, стек и куча, выделение и освобождение памяти
Линейное представление памяти
Адрес | Значение (1 байт) |
---|---|
0x0000 | . |
. | . |
0x1000 | 1 |
0x1001 | 2 |
0x1002 | 3 |
0x1003 | 4 |
. | . |
0xffffffffff | . |
C-cast использовать в С++ нельзя! Как надо приводить типы в С++ и надо ли вообще будет в другой лекции
Знаковые | Беззнаковые |
---|---|
char | unsigned char |
short | unsigned short |
int | unsigned или unsigned int |
long | unsigned long |
Стандарт не регламентирует размер типов
Размер, бит | Тип |
---|---|
8 | int8_t, int_fast8_t, int_least8_t |
16 | int16_t, int_fast16_t, int_least16_t |
32 | int32_t, int_fast32_t, int_least32_t |
64 | int64_t, int_fast64_t, int_least64_t |
Беззнаковая (unsigned) версия — добавление префикса u
Память разбита на сегменты:
- кода (CS)
- данных (DS)
- стека (SS)
Регистр сегмента (CS, DS, SS) указывают на дескриптор.
Для инструкций и стека на смещение в сегменте указывает регистр:
- кода (EIP)
- стека (ESP)
Линейный адрес — это сумма базового адреса сегмента и смещения.
Segment limit (20 bit) — размер сегмента, 55-й бит G определяет гранулярность размера:
- байты, если 0
- страницы, если 1 (размер страницы обычно 4Кб)
- 000 — сегмент данных, только чтение
- 001 — сегмент данных, чтение и запись
- 010 — сегмент стека, только чтение
- 011 — сегмент стека, чтение и запись
- 100 — сегмент кода, только выполнение
- 101- сегмент кода, чтение и выполнение
- Память делится на страницы
- Страница может находится в оперативной памяти или на внешнем носителе
- Трансляция из физического адреса в виртуальный и обратно выполняется через специальные таблицы: PGD (Page Global Directory), PMD (Page Middle Directory) и PTE (Page Table Entry). В PTE хранятся физические адреса страниц
- Для ускорения трансляции адресов процессор хранит в кеше таблицу TLB (Translation lookaside buffer)
- Если обращение к памяти не может быть оттранслировано через TLB, процессор обращается к таблицам страниц и пытается загрузить PTE оттуда в TLB. Если загрузка не удалась, процессор вызывает прерывание Page Fault
- Обработчик прерывания Page Fault находится в подсистеме виртуальной памяти ядра ОС и может загрузить требуемую страницу с внешнего носителя в оперативную память
Кроме задержки (latency) есть понятие пропускной способности (throughput, bandwidth). В случае чтения из RAM — 10-50 Gb/sec
Выводы из таблицы
- Стараться укладывать данные в кеш
- Минимизировать скачки по памяти
- В условиях основной веткой делать ветку которая выполняется с большей вероятностью
Классы управления памятью и областью видимости в C++
Характеризуются тремя понятиями:
- Время жизни
Продолжительность хранения данных в памяти
- Область видимости
Части кода из которых можно получить доступ к данным
- Связывание (linkage)
Если к данным можно обратиться из другой единицы трансляции — связывание внешнее (external), иначе связывание внутреннее (internal)
Время жизни | Область видимости | Связывание |
---|---|---|
Автоматическое (блок) | Блок | Отсутствует |
Статический без связывания
Время жизни | Область видимости | Связывание |
---|---|---|
Статическое | Блок | Отсутствует |
Инициализируется при первом обращении
Статический с внутренним связыванием
Время жизни | Область видимости | Связывание |
---|---|---|
Статическое | Файл | Внутреннее |
Инициализируется до входа в main
Статический с внешним связыванием
Время жизни | Область видимости | Связывание |
---|---|---|
Статическое | Файл | Внешнее |
Выделение памяти на стеке очень быстрая, но стек не резиновый
Память в куче выделяют new и malloc, есть сторонние менеджеры памяти.
- new то же, что и malloc, только дополнительно вызывает конструкторы
- Внутри malloc есть буфер, если в буфере есть место, ваш вызов может выполниться быстро
- Если памяти в буфере нет, будет запрошена память у ОС (sbrk, VirtualAlloc) — это дорого
- ОС выделяет память страницами от 4Кб, а может быть и все 2Мб
- Стандартные аллокаторы универсальные, то есть должны быть потокобезопасны, быть одинаково эффективны для блоков разной длины, и 10 байт и 100Мб. Плата за универсальность — быстродействие
Глобальная память (data segment)
Если не удастся разместить блок глобальной памяти, то программа даже не запустится
Значение в квадратных скобках должно быть известно на этапе компиляции, увы
Фактически — это вычисление смещения:
Массив — непрерывный блок байт в памяти, sizeof(data) вернет размер массива в байтах (не элементах!). Размер массива в элементах можно вычислить: sizeof(data) / sizeof(data[0])
Измеряем скорость работы (benchmark)
- Измерений должно быть много
- Одному прогону верить нельзя
- Компилятор оптимизирует, надо ему мешать
- Перед тестами надо греться
Пример «вредной» оптимизации
Не даем компилятору оптимизировать
Написать свой аллокатор со стратегией линейного выделения памяти со следующим интерфейсом:
При вызове makeAllocator аллоцируется указанный размер, после чего при вызове alloc возвращает указатель на блок запрошенного размера или nullptr, если места недостаточно. После вызова reset аллокатор позволяет использовать свою память снова.
C Language
Управление памятью
Для управления динамически распределенной памятью стандартная библиотека C предоставляет функции malloc() , calloc() , realloc() и free() . В C99 и более поздних версиях также есть aligned_alloc() . Некоторые системы также предоставляют alloca() .
Синтаксис
- void * aligned_alloc (size_t alignment, size_t size); / * Только с C11 * /
- void * calloc (size_t nelements, size_t size);
- void free (void * ptr);
- void * malloc (size_t size);
- void * realloc (void * ptr, size_t size);
- void * alloca (size_t size); / * от alloca.h, не стандартный, не портативный, опасный. * /
параметры
название | описание |
---|---|
размер ( malloc , realloc и aligned_alloc ) | общий размер памяти в байтах. Для aligned_alloc размер должен быть целым кратным выравниванию. |
размер ( calloc ) | размер каждого элемента |
nelements | количество элементов |
PTR | указатель на выделенную память, ранее возвращенную malloc , calloc , realloc или aligned_alloc |
выравнивание | выравнивание выделенной памяти |
замечания
Обратите внимание: aligned_alloc() определен только для C11 или более поздних aligned_alloc() .
Системы, такие как основанные на POSIX, предоставляют другие способы выделения выровненной памяти (например, posix_memalign() ), а также другие параметры управления памятью (например, mmap() ).
Освобождение памяти
Можно освободить динамически выделенную память, вызвав free () .
Память, на которую указывает p , восстанавливается (либо посредством реализации libc, либо базовой ОС) после вызова free() , поэтому доступ к этому свободному блоку памяти через p приведет к неопределенному поведению . Указатели, которые ссылаются на освобожденные элементы памяти, обычно называются оборванными указателями и представляют угрозу безопасности. Кроме того, стандарт C утверждает, что даже доступ к значению висячего указателя имеет неопределенное поведение. Обратите внимание, что сам указатель p может быть переназначен, как показано выше.
Обратите внимание, что вы можете звонить только free() по указателям, которые были возвращены непосредственно из функций malloc() , calloc() , realloc() и aligned_alloc() или где документация сообщает вам, что память была выделена таким образом (функции такие как strdup () являются заметными примерами). Освобождая указатель, который есть,
- полученные с помощью оператора & на переменной, или
- в середине выделенного блока,
запрещен. Такая ошибка обычно не будет диагностирована вашим компилятором, но приведет к выполнению программы в неопределенном состоянии.
Существуют две распространенные стратегии предотвращения таких случаев неопределенного поведения.
Первое и предпочтительное — просто: p перестает существовать, когда он больше не нужен, например:
Вызывая free() непосредственно перед концом содержащего блока (то есть > ), p сама перестает существовать. Компилятор даст ошибку компиляции при любой попытке использовать p после этого.
Второй подход заключается в том, чтобы также сделать недействительным сам указатель после освобождения памяти, на которую он указывает:
Аргументы для этого подхода:
На многих платформах попытка разыменования нулевого указателя вызовет мгновенный сбой: ошибка сегментации. Здесь мы получаем как минимум трассировку стека, указывающую на переменную, которая была использована после освобождения.
Не устанавливая указатель на NULL мы имеем свисающий указатель. Вероятно, программа все равно будет разбиваться, но позже, потому что память, на которую указывает указатель, будет бесшумно повреждена. Такие ошибки трудно отследить, поскольку они могут привести к стеку вызовов, полностью не связанному с исходной проблемой.
Этот подход, следовательно, следует за неудачной концепцией .
Безопасно освобождать нулевой указатель. В стандарте C указано, что free(NULL) не действует:
Свободная функция заставляет пространство, на которое указывает ptr, освобождается, то есть становится доступным для дальнейшего выделения. Если ptr является нулевым указателем, никаких действий не происходит. В противном случае, если аргумент не соответствует указателю, ранее возвращенному функцией calloc , malloc или realloc , или если пространство было освобождено вызовом free или realloc , поведение не определено.
- Иногда первый подход не может быть использован (например, память выделяется одной функцией и значительно позже освобождается от совершенно другой функции)
Выделение памяти
Стандартное распределение
Функции распределения динамической памяти C определены в заголовке <stdlib.h> . Если вы хотите динамически выделять пространство памяти для объекта, можно использовать следующий код:
Это вычисляет количество байтов, которое занимает десять int s в памяти, затем запрашивает, что много байтов из malloc и присваивает результат (то есть начальный адрес блока памяти, который только что был создан с помощью malloc ), указателю с именем p .
Рекомендуется использовать sizeof для вычисления объема запрашиваемой памяти, так как результат sizeof определяется реализацией (за исключением типов символов , которые являются char , signed char и unsigned char , для которых sizeof определен всегда, чтобы дать 1 ).
Поскольку malloc не может обслуживать запрос, он может вернуть нулевой указатель. Для этого важно проверить это, чтобы предотвратить последующие попытки разыменовать нулевой указатель.
Память, динамически распределенная с помощью malloc() может быть изменена с использованием realloc() или, если она больше не понадобится, освобождается с помощью free() .
Альтернативно, объявляя int array[10]; будет выделять один и тот же объем памяти. Однако, если он объявлен внутри функции без ключевого слова static , он будет использоваться только внутри функции, в которой он объявлен, и функций, которые он вызывает (поскольку массив будет выделен в стеке, и пространство будет выпущено для повторного использования, когда функция возвращает). В качестве альтернативы, если она определена со static внутри функции или если она определена вне любой функции, то ее время жизни является временем жизни программы. Указатели также могут быть возвращены из функции, однако функция в C не может вернуть массив.
Нулевая память
Память, возвращаемая malloc не может быть инициализирована до разумного значения, и следует позаботиться об обнулении памяти с помощью memset или немедленно скопировать в нее подходящее значение. В качестве альтернативы calloc возвращает блок требуемого размера, где все биты инициализируются до 0 . Это не должно быть таким же, как представление нулевой точки с плавающей точкой или константы нулевого указателя.
Замечание о calloc : Большинство (обычно используемых) реализаций оптимизируют calloc() для производительности, поэтому он будет быстрее, чем вызов malloc() , затем memset() , хотя сетевой эффект идентичен.
Выровненная память
C11 представила новую функцию aligned_alloc() которая выделяет пространство с заданным выравниванием. Он может использоваться, если выделенная память необходима для выравнивания на определенных границах, которые не могут быть удовлетворены malloc() или calloc() . Функции malloc() и calloc() выделяют память, которая соответствующим образом выровнена для любого типа объекта (т.е. выравнивание равно alignof(max_align_t) ). Но с aligned_alloc() можно запросить большее выравнивание.
Стандарт C11 накладывает два ограничения: 1) запрошенный размер (второй аргумент) должен быть целым кратным выравниванию (первый аргумент) и 2) значение выравнивания должно быть действительным выравниванием, поддерживаемым реализацией. Несоблюдение любого из них приводит к неопределенному поведению .
Перераспределение памяти
Возможно, вам придется расширять или сокращать пространство для хранения указателя после того, как вы выделили ему память. Функция void *realloc(void *ptr, size_t size) освобождает старый объект, на который указывает ptr и возвращает указатель на объект, размер которого задан по size . ptr — это указатель на блок памяти, ранее выделенный с помощью malloc , calloc или realloc (или нулевой указатель), который должен быть перераспределен. Сохраняется максимально возможное содержимое исходной памяти. Если новый размер больше, любая дополнительная память за пределами старого размера не инициализируется. Если новый размер короче, содержимое усаженной части теряется. Если ptr равно NULL, выделяется новый блок, и указатель на него возвращается функцией.
Перераспределенный объект может иметь или не иметь тот же адрес, что и *p . Поэтому важно зафиксировать возвращаемое значение из realloc которое содержит новый адрес, если вызов будет успешным.
Убедитесь, что возвращаемое значение realloc присваивается temporary а не оригиналу p . realloc вернет null в случае любого сбоя, который перезапишет указатель. Это потеряет ваши данные и создаст утечку памяти.
Многомерные массивы переменной величины
Поскольку C99, C имеет массивы переменной длины, VLA, эта модель массивов с границами, которые известны только во время инициализации. Хотя вы должны быть осторожны, чтобы не выделять слишком большие VLA (они могут разбивать ваш стек), использование указателей в VLA и использование их в выражениях sizeof в порядке.
Здесь matrix является указателем на элементы типа double[m] , а выражение sizeof с double[n][m] гарантирует, что оно содержит пространство для n таких элементов.
Все это пространство распределено смежно и, таким образом, может быть освобождено одним звонком на free .
Наличие VLA на языке также влияет на возможные объявления массивов и указателей в заголовках функций. Теперь внутри [] параметров массива допускается общее целочисленное выражение. Для обеих функций выражения в [] используют параметры, которые были объявлены ранее в списке параметров. Для sumAll это длины, которые пользовательский код ожидает для матрицы. Что касается всех параметров функции массива в C, то самое внутреннее измерение переписывается на тип указателя, поэтому это эквивалентно объявлению
То есть n не является частью функционального интерфейса, но информация может быть полезна для документации, и она также может использоваться ограничителями, проверяющими компиляторы, чтобы предупреждать об отсутствии доступа к границам.
По существу, для main выражение argc+1 является минимальной длиной, которую стандарт C предписывает для аргумента argv .
Обратите внимание, что официальная поддержка VLA является необязательной в C11, но мы не знаем компилятора, который реализует C11, и у которого их нет. Если нужно, вы можете протестировать макрос __STDC_NO_VLA__ .
realloc (ptr, 0) не эквивалентен свободному (ptr)
realloc концептуально эквивалентен malloc + memcpy + free на другом указателе.
Если размер запрашиваемого пространства равен нулю, поведение realloc определяется реализацией. Это похоже на все функции выделения памяти, которые получают параметр size 0 . Такие функции могут фактически возвращать ненулевой указатель, но это никогда не должно быть разыменовано.
Таким образом, realloc(ptr,0) не эквивалентен free(ptr) . Это может
- быть «ленивой» реализацией и просто вернуть ptr
- free(ptr) , выделите фиктивный элемент и верните его
- free(ptr) и возврат 0
- просто верните 0 для отказа и ничего не делайте.
Поэтому, в частности, последние два случая неразличимы по коду приложения.
Это означает, что realloc(ptr,0) может не освобождать / освобождать память и, следовательно, никогда не следует использовать в качестве замены free .
Пользовательское управление памятью
malloc() часто вызывает базовые функции операционной системы для получения страниц памяти. Но нет ничего особенного в функции, и она может быть реализована в прямом C, объявив большой статический массив и выделяя из него (есть небольшая трудность в обеспечении правильного выравнивания, на практике выравнивание до 8 байтов почти всегда адекватно).
Для реализации простой схемы блок управления хранится в области памяти непосредственно перед указателем, который должен быть возвращен из вызова. Это означает, что free() может быть реализовано путем вычитания из возвращаемого указателя и считывания управляющей информации, которая обычно является размером блока плюс некоторая информация, которая позволяет вернуть ее в свободный список — связанный список нераспределенных блоков.
Когда пользователь запрашивает выделение, выполняется поиск свободного списка до тех пор, пока не будет найден блок с одинаковым или большим размером запрашиваемой суммы, а затем при необходимости он будет разделен. Это может привести к фрагментации памяти, если пользователь постоянно делает много распределений и освобождает непредсказуемый размер, и с непредсказуемыми интервалами (не все реальные программы ведут себя так, простая схема часто подходит для небольших программ).
Многим программам требуется большое количество распределений небольших объектов одинакового размера. Это очень легко реализовать. Просто используйте блок со следующим указателем. Поэтому, если требуется блок из 32 байтов:
Эта схема чрезвычайно быстрая и эффективная, и ее можно сделать родовым с определенной потерей ясности.
alloca: выделить память в стеке
Предостережение: alloca упоминается здесь только для полноты. Он полностью не переносится (не подпадает под действие каких-либо общих стандартов) и имеет ряд потенциально опасных функций, которые делают его небезопасным для не подозревающих. Современный C-код должен заменить его массивами переменной длины (VLA).
Выделяйте память в стеке стека вызывающего, пространство, на которое ссылается возвращенный указатель, автоматически освобождается 'd, когда функция вызывающего абонента заканчивается.
Хотя эта функция удобна для автоматического управления памятью, имейте в виду, что запрос на большое выделение может привести к переполнению стека и что вы не можете использовать free с памятью, выделенной с помощью alloca (что может вызвать дополнительную проблему при переполнении стека).
По этой причине не рекомендуется использовать alloca внутри цикла или рекурсивную функцию.
И поскольку память free при возврате функции, вы не можете вернуть указатель как результат функции ( поведение будет неопределенным ).
Резюме
- вызов идентичен malloc
- автоматически free'd при возврате функции
- несовместимые со free функциями realloc ( неопределенное поведение )
- указатель не может быть возвращен как результат функции ( неопределенное поведение )
- размер ограниченного пространства стека, который (на большинстве машин) намного меньше места кучи, доступного для использования malloc()
- избегайте использования alloca() и VLA (массивы с переменной длиной) в одной функции
- alloca() не так переносима, как malloc() et al.
Рекомендация
- Не используйте alloca() в новом коде
Это работает там, где alloca() делает и работает в местах, где alloca() не работает (например, внутри циклов). Он предполагает либо реализацию C99, либо реализацию C11, которая не определяет __STDC_NO_VLA__ .