Работа с динамической памятью

Распределение памяти


В этом уроке научимся работать с динамической памятью микроконтроллера. Прежде всего нужно ознакомиться с распределением памяти и понять, как оно работает и что мы вообще будем делать. Перед вами схема распределения памяти в МК от 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)
5/5 - (6 голосов)
Назад Указатели и ссылки
Вперёд Оптимизация кода
Подписаться
Уведомить о
guest
6 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии