Работа с динамической памятью
Распределение памяти
В этом уроке научимся работать с динамической памятью микроконтроллера. Прежде всего нужно ознакомиться с распределением памяти и понять, как оно работает и что мы вообще будем делать. Перед вами схема распределения памяти в МК от AVR, которые стоят на Arduino:
Память – это по сути большой массив, каждая ячейка которого имеет свой адрес, адрес растёт слева направо (по картинке выше). Первым идёт Flash, она же программная память, память, в ней хранится код программы. Во время работы программы этот код не меняется (есть способы сделать это, но это не относится к теме урока). Сегодня нас интересует динамическая память, SRAM, которая на схеме представлена совокупностью синей, зелёной, розовой и оранжевой областей, а также белой областью со стрелочками между розовой и оранжевой. Рассмотрим подробнее:
- Globals (синий и зелёный) – в этой области живут глобальные и статические переменные. Размер этой области известен на момент запуска программы и не меняется в процессе её выполнения, т.к. глобальные и статические переменные уже объявлены, их размер и количество известны.
- Stack, он же стек (оранжевый) – в этой области живут локальные переменные и аргументы функций. Размер этой области меняется во время выполнения программы, стек растёт от конца области памяти в сторону уменьшения адресов, навстречу куче (см. стрелку на схеме). Переменные, которые тут живут, называются автоматическими: программа сама выделяет память (при создании локальной переменной) и сама эту память освобождает (локальная переменная удаляется при выходе из функции). Важно: процессор не контролирует размер стека, то есть во время работы стек может врезаться в кучу и перезаписать находящиеся там данные.
- Heap (розовый), она же куча – из этой области мы можем самостоятельно выделить память под свои нужды. Размер этой области может меняться во время выполнения программы, куча “растёт” в сторону увеличения адресов, слева направо, что показано стрелкой на схеме. Эту память мы контролируем сами: сами выделяем и сами освобождаем. Важно: процессор не даст вам выделить область, если свободной памяти под неё недостаточно, т.е. наползание кучи на стек очень маловероятно.
Выделение памяти
Для выделения и освобождения динамической памяти “из кучи” у нас есть две функции: malloc()
и free()
соответственно. Также есть операторы new
и delete
, которые делают то же самое. При выделении памяти мы получаем адрес на первый байт выделенной области, поэтому рекомендую почитать урок про адреса и указатели.
malloc(количество)
– выделяет количество байт динамической памяти (из кучи) и возвращает адрес на первый байт выделенной области. Если свободной памяти недостаточно для выделения – возвращает “нулевой указатель” –NULL
.free(ptr)
– освобождает память, на которую указываетptr
. Освободить можно только память, выделенную при помощи функцийmalloc()
,realloc()
илиcalloc()
. В выделяемой области хранится размер этой области (+2 байта), и при освобождении функцияfree
сама знает, какой размер освобождать.new
иdelete
– технически то же самое, разница в применении (см. пример ниже)
Функции для работы с динамической памятью довольно тяжёлые, их использование добавляет к весу программы около 2 кБ! Именно поэтому библиотека для String-строк занимает столько места.
Примеры
Рассмотрим пример с выделением и освобождением памяти при помощи malloc/free и new/delete. Примеры абсолютно одинаковые с точки зрения происходящего, отличаются только функциями:
malloc/free
void setup() { // выделяем память под 10 переменных типа byte (10 байт) byte *by = malloc(10); // выделяем память под 20 переменных типа int (40 байт) int *in = malloc(20 * sizeof(int)); // выделяем память под 1 переменную типа uint32_t (4 байта) uint32_t *ui = malloc(4); // работаем с массивом by[0] = 50; by[1] = 60; by[2] = 90; uart.println(by[0]); uart.println(by[1]); uart.println(by[2]); // с обычной переменной *ui = 123456; free(ui); // освобождаем free(in); // освобождаем free(by); // освобождаем // тут *ui равна нулю! Мы её "удалили" } void loop() {}
new/delete
void setup() { // выделяем память под 10 переменных типа byte (10 байт) byte *by = new byte [10]; // выделяем память под 20 переменных типа int (40 байт) int *in = new int [20]; // выделяем память под 1 переменную типа uint32_t (4 байта) uint32_t *ui = new uint32_t; // работаем с массивом by[0] = 50; by[1] = 60; by[2] = 90; // с обычной переменной *ui = 123456; delete ui; // освобождаем delete [] in; // освобождаем (указываем, что это массив) delete [] by; // освобождаем (указываем, что это массив) // тут *ui и остальные равны нулю! Мы их "удалили" } void loop() {}
Таким образом мы выделили себе память, с этой памятью можем взаимодействовать как с обычной переменной, а потом её освободить.
Есть ещё две функции: calloc()
и realloc()
:
calloc(количество, размер)
– выделяет память под количество элементов с размером размер каждый (в байтах). Тот же malloc, но чуть удобнее использовать: в примере выше мы умножали, чтобы получить нужное количество байт для храненияint malloc(20 * sizeof(int))
, а можно было вызватьcalloc(20, sizeof(int));
– заменив знак умножения на запятую.realloc(ptr, размер)
– изменяет величину выделенной памяти, на которую указывает ptr, на новую величину, задаваемую параметром размер. Величина размер задается в байтах и может быть больше или меньше оригинала. Возвращается указатель на блок памяти, поскольку может возникнуть необходимость переместить блок при возрастании его размера. В таком случае содержимое старого блока копируется в новый блок и информация не теряется.
Пакетное управление памятью
Итак, выделять и освобождать память мы научились, теперь рассмотрим несколько инструментов для удобной работы с динамической памятью:
memset(ptr, значение, количество)
– заполняет область памяти, на которую указывает ptr, байтами значение, в количестве количество штук. Часто используется для задания начальных значений выделенной области памяти. Внимание! Заполняет только байтами, 0.. 255.memcpy(ptr1, ptr2, количество)
– переписывает байты из области ptr2 в ptr1 в количестве количество. Грубо говоря переписывает один массив в другой. Внимание! Работает с байтами!
// === memset === // выделили 50 байт byte *buf = (byte*)malloc(50); // забили их значением 10 memset(buf, 10, 50); // === memcpy === // сделали массив byte data1[] = {1, 2, 3, 4, 5}; // и ещё массив //byte data2[5]; // можно из "стека" выделить byte *data2 = malloc(5); // можно из "кучи" // перепишем data1 в data2 memcpy(data2, data1, 5); // data2 теперь 1 2 3 4 5
Зачем?
Я встречал работу с динамической памятью только в библиотеках дисплеев и адресных светодиодных лент, там в динамической памяти был создан буфер. Это удобно в тех случаях, когда нужно поместить какой-то объём данных в буфер, который обладает большой областью видимости, в отличие от локальной переменной. С этим буфером можно взаимодействовать из разных уголков программы, а затем освободить память. Или создать объект класса, чтобы поработать с ним в разных участках программы, а затем удалить.
А так, работа с динамической памятью вам скорее всего не пригодится, но знать о таком инструменте полезно.
Полезные страницы
- Набор GyverKIT – большой стартовый набор Arduino моей разработки, продаётся в России
- Каталог ссылок на дешёвые Ардуины, датчики, модули и прочие железки с AliExpress у проверенных продавцов
- Подборка библиотек для Arduino, самых интересных и полезных, официальных и не очень
- Полная документация по языку Ардуино, все встроенные функции и макросы, все доступные типы данных
- Сборник полезных алгоритмов для написания скетчей: структура кода, таймеры, фильтры, парсинг данных
- Видео уроки по программированию Arduino с канала “Заметки Ардуинщика” – одни из самых подробных в рунете
- Поддержать автора за работу над уроками
- Обратная связь – сообщить об ошибке в уроке или предложить дополнение по тексту (alex@alexgyver.ru)