Добрый день, уважаемый читатель! Сегодня я хочу познакомить вас с одной замечательной библиотекой, “встроенной” в ESP-IDF – библиотекой для работы с однонаправленными или двунаправленными связными списками.
Строго говоря, эта библиотека не является частью ESP-IDF, как и Espressif не является авторами данной библиотеки. Создана она аж в 1993 году в University of California и просто используется в ESP32 в готовом виде. Поэтому справки по её использованию в официальной справочной системе вы не найдете. Сам я наткнулся на эту библиотечку совершенно случайно, когда изучал исходники ESP-IDF, а до этого пользовался самодельной реализацией. Но эта готовая реализация настолько удобна, что необходимости в велосипедах более нет.
Связные списки удобно применять для хранения в памяти любых структурированных данных, когда длина этих данных заранее не известна. Можно, конечно использовать динамические массивы, но в некоторых случаях использование списков более оправданно.
Описание библиотеки
Библиотека представляет собой набор макросов препроцессора для быстрого создания списков четырех типов и содержит все необходимые функции для работы с ними:
- Односвязные списки SLIST
- Односвязные хвостовые очереди STAILQ
- Списки (двусвязные) LIST
- Хвостовые очереди TAILQ
Вы можете выбрать любой тип очереди, исходя из своих целей и задач, а также необходимых для их реализации функций.
Для подключения библиотеки в свой проект добавьте строчку:
#include "sys/queue.h"
А теперь давайте познакомимся со всем этих хозяйством более подробно…
Базовые функции
Все четыре структуры поддерживают следующие функции:
- _INSERT_HEAD – вставка новой записи в начало списка
- _INSERT_AFTER – вставка новой записи после любого элемента в списке
- _NEXT – прямой обход списка
- _SWAP – замена содержимого двух списков
Здесь не случайно все _МАКРОСЫ записаны с символом подчеркивания, потому что к каждой функции при использовании необходимо будет добавить тип списка, например для односвязного списка функция добавления в начало списка будет записана как SLIST_INSERT_HEAD(…)
Односвязные структуры
Односвязные типы структур данных являются простейшими и поддерживают указанные выше функции плюс ещё несколько.
SLIST
Односвязный список начинается с одного прямого указателя. Элементы связаны одним указателем для минимального объема памяти и накладных расходов на манипулирование указателем за счет удаления O(n) для произвольных элементов. Новые элементы могут быть добавлены в список после существующего элемента или в начало списка. Элементы, удаляемые из головы списка, должны использовать явный макрос для этой цели для оптимальной эффективности. Односвязный список может быть пройден только в прямом направлении.
Односвязные списки добавляют к базовым следующие функции:
- SLIST_REMOVE_HEAD – удаление первой записи в списке
- SLIST_REMOVE_AFTER – удаление записи после любого элемента в списке
- SLIST_REMOVE 🐌 – O(n) удаление любой записи в списке
- SLIST_CONCAT 🐌 – O(n) они могут быть объединены.
Односвязные списки идеально подходят для приложений с большими наборами данных и небольшим количеством удалений или без них, а также для реализации очереди LIFO.
STAILQ
Односвязная хвостовая очередь характеризуется парой указателей, один на начало списка, а другой на конец списка. Элементы по прежнему связаны одним указателем для минимального объема памяти и накладных расходов на манипулирование указателем за счет удаления O(n) для произвольных элементов. Новые элементы могут быть добавлены в список после существующего элемента, в начало списка или в конец списка. Элементы, удаляемые из головы хвостовой очереди, должны использовать явный макрос для этой цели для оптимальной эффективности. Односвязная хвостовая очередь может проходить только в прямом направлении.
Односвязные хвостовые очереди добавляют следующую функциональность:
- STAILQ_INSERT_TAIL – записи могут быть добавлены в конец списка
- STAILQ_REMOVE_HEAD – удаление первой записи в списке
- STAILQ_REMOVE_AFTER – удаление записи после любого элемента в списке
- STAILQ_REMOVE 🐌 – O(n) удаление любой записи в списке
- STAILQ_CONCAT – они могут быть объединены.
Однако ничто в этом мире не дается просто так, и за новые возможности приходится платить:
- Все вставки элементов должны указывать заголовок списка.
- Для каждой записи заголовка списка требуется два указателя, а не один.
- Размер кода примерно на 15% больше, а операции выполняются примерно на 20% медленнее, чем на односвязных списках.
Односвязные хвостовые очереди идеально подходят для приложений с большими наборами данных и небольшим количеством удалений или без них, а также для реализации очереди FIFO.
Двусвязные структуры
Все двусвязные типы структур данных (списки и хвостовые очереди) дополнительно позволяют:
- _INSERT_BEFORE – вставка новой записи перед любым элементом в списке
- _REMOVE – быстрое удаление любой записи из списка
Однако:
- Для каждого элемента требуется два указателя, а не один
- Размер кода и время выполнения операций (кроме удаления) примерно в два раза больше, чем у односвязных структур данных
LIST
Двусвязный список начинается с одного прямого указателя (или массива прямых указателей для заголовка хеш-таблицы). Элементы связаны между собой двумя указателями, так что произвольный элемент может быть удален без необходимости обхода списка. Новые элементы могут быть добавлены в список до или после существующего элемента или в начале списка. Но добавление новых значений в список занимает больше времени, чем в односвязный. Список может быть пройден в любом направлении.
Списки добавляют следующие функции к вышеперечисленным (базовым и общим для двусвязных типов):
- LIST_PREV – их можно пройти в обратном направлении
- LIST_CONCAT 🐌 – O(n) они могут быть объединены
Однако:
- Для обхода в обратном направлении необходимо указать запись для начала обхода и список, в котором она содержится
TAILQ
Хвостовую очередь возглавляет пара указателей, один на начало списка, а другой на конец списка. Элементы связаны между собой двумя указателями, так что произвольный элемент может быть удален без необходимости обхода списка. Новые элементы могут быть добавлены в список до или после существующего элемента, в начало списка или в конец списка. Хвостовую очередь можно пройти в любом направлении.
Хвостовые очереди добавляют следующие функции:
- TAILQ_INSERT_TAIL – записи могут быть добавлены в конец списка
- TAILQ_PREV – их можно пройти назад, от хвоста к голове
- TAILQ_CONCAT – они могут быть объединены
Однако:
- Все вставки и удаления списка должны указывать заголовок списка
- Для каждой записи заголовка требуется два указателя, а не один
- Размер кода примерно на 15% больше, а операции выполняются примерно на 20% медленнее, чем двусвязные списки
Перечень реализованных функций для каждого из типов вы можете увидеть в сводной таблице ниже:
Примечание:
- + означает, что макрос доступен
- – означает, что макрос недоступен
- s означает, что макрос доступен, но работает 🐌 медленно (выполняется за время O(n)).
Макросы
Теперь давайте рассмотрим эти макросы – функции более подробно.
В определениях макросов TYPE – это имя определяемой пользователем структуры, которая должна содержать поле типа SLIST_ENTRY, STAILQ_ENTRY, LIST_ENTRY или TAILQ_ENTRY с именем NAME.
Аргумент HEADNAME – это имя определяемой пользователем структуры, которая должна быть объявлена с использованием макросов SLIST_HEAD, STAILQ_HEAD, LIST_HEAD или TAILQ_HEAD
Односвязные списки
Односвязный список возглавляет структура, определяемая макросом SLIST_HEAD, которая объявляется следующим образом:
SLIST_HEAD(HEADNAME, TYPE) head;
где HEADNAME — имя определяемой структуры, а TYPE — тип элементов, которые должны быть связаны со списком. Указатель на начало списка позже может быть объявлен как:
struct HEADNAME *headp;
Имена head и headp выбираются программистом самостоятельно.
- Макрос SLIST_ENTRY объявляет структуру, которая соединяет элементы в списке.
- Макрос SLIST_HEAD_INITIALIZER оценивается как инициализатор для заголовка списка (например NULL).
- Макрос SLIST_INIT инициализирует список, на который ссылается head.
- Макрос SLIST_EMPTY принимает значение true, если в списке нет элементов.
- Макрос SLIST_FIRST возвращает первый элемент в списке или NULL, если список пуст.
- Макрос SLIST_NEXT возвращает следующий элемент в списке.
- Макрос SLIST_INSERT_HEAD вставляет новый элемент elm в начало списка.
- Макрос SLIST_INSERT_AFTER вставляет новый элемент elm после элемента listelm.
- Макрос SLIST_REMOVE удаляет элемент elm из списка. Универсальный, но и самый не оптимальный макрос.
- Макрос SLIST_REMOVE_AFTER удаляет элемент после elm из списка. В отличие от SLIST_REMOVE, этот макрос не проходит по всему списку.
- Макрос SLIST_REMOVE_HEAD удаляет элемент elm из начала списка. Для оптимальной эффективности элементы, удаляемые из начала списка, должны явно использовать этот макрос вместо универсального макроса SLIST_REMOVE.
- Макрос SLIST_SWAP меняет местами содержимое head1 и head2.
- Макрос SLIST_FOREACH просматривает список, на который ссылается head, в прямом направлении, присваивая каждому элементу по очереди var.
- Макрос SLIST_FOREACH_FROM ведет себя идентично SLIST_FOREACH, когда var имеет значение NULL. В противном случае он обрабатывает var как ранее найденный элемент SLIST и начинает цикл с var вместо первого элемента в SLIST, на который ссылается head. То есть его можно использовать для продолжения просмотра списка.
- Макрос SLIST_FOREACH_SAFE обходит список, на который ссылается заголовок head, в прямом направлении, присваивая каждый элемент по очереди var. Однако, в отличие от SLIST_FOREACH, здесь разрешено как удалять var, так и безопасно освобождать ее внутри цикла, не мешая обходу.
- Макрос SLIST_FOREACH_FROM_SAFE ведет себя идентично SLIST_FOREACH_SAFE , когда var имеет значение NULL, в противном случае он обрабатывает var как ранее найденный элемент SLIST и начинает цикл с var вместо первого элемента в SLIST, на который ссылается head.
Односвязная хвостовая очередь
Односвязная хвостовая очередь возглавляется структурой, определяемой макросом STAILQ_HEAD. Эта структура содержит два указателя, один на первый элемент в хвостовой очереди, а другой на последний элемент в хвостовой очереди. Элементы связаны одним указателем для минимальных накладных расходов на манипулирование указателем. Новые элементы могут быть добавлены в хвостовую очередь после существующего элемента, в начало хвостовой очереди или в конец хвостовой очереди. Структура STAILQ_HEAD объявляется следующим образом:
STAILQ_HEAD(HEADNAME, TYPE) head;
где HEADNAME — имя определяемой структуры, а TYPE — тип элементов, которые должны быть связаны с хвостовой очередью. Указатель на голову хвостовой очереди позже может быть объявлен как:
struct HEADNAME *headp;
Имена head и headp выбираются программистом самостоятельно.
Макрос STAILQ_HEAD_INITIALIZER оценивается как инициализатор хвостовой очереди head (например NULL).
Макрос STAILQ_ENTRY объявляет структуру, которая соединяет элементы в хвостовой очереди.
Макрос STAILQ_INIT инициализирует хвостовую очередь, на которую ссылается head.
Макрос STAILQ_EMPTY принимает значение true, если в хвостовой очереди нет элементов.
Макрос STAILQ_FIRST возвращает первый элемент в хвостовой очереди или NULL, если хвостовая очередь пуста.
Макрос STAILQ_LAST возвращает последний элемент в хвостовой очереди. Если хвостовая очередь пуста, возвращаемое значение равно NULL .
Макрос STAILQ_NEXT возвращает следующий элемент в хвостовой очереди или NULL, этот элемент является последним.
Макрос STAILQ_INSERT_HEAD вставляет новый элемент elm в начало хвостовой очереди.
Макрос STAILQ_INSERT_TAIL вставляет новый элемент elm в конец хвостовой очереди.
Макрос STAILQ_INSERT_AFTER вставляет новый элемент elm после элемента listelm.
Макрос STAILQ_REMOVE удаляет элемент elm из хвостовой очереди.
Макрос STAILQ_REMOVE_HEAD удаляет элемент в начале хвостовой очереди. Для оптимальной эффективности элементы, удаляемые из головы хвостовой очереди, должны явно использовать этот макрос, а не общий макрос STAILQ_REMOVE.
Макрос STAILQ_FOREACH проходит хвостовую очередь, на которую ссылается head, в прямом направлении, присваивая каждому элементу по очереди var.
Макрос STAILQ_FOREACH_SAFE проходит хвостовую очередь, на которую ссылается head, в прямом направлении, присваивая каждый элемент по очереди var. Однако, в отличие от STAILQ_FOREACH() здесь разрешено как удалить var, так и безопасно освободить его из цикла, не мешая обходу.
Макрос STAILQ_CONCAT объединяет хвостовую очередь, возглавляемую head2, с концом очереди, возглавляемой head1, удаляя все записи из первой.
Макрос STAILQ_SWAP меняет местами содержимое head1 и head2.
// Заголовок односвязной хвостовой очереди STAILQ_HEAD(stailhead, entry) head = STAILQ_HEAD_INITIALIZER(head); struct stailhead *headp; // Объявляем структуру для хранения данных struct entry { uint32_t data1; uint32_t data2; uint32_t data3; char string[32]; STAILQ_ENTRY(entry) next; } *n1, *n2, *n3, *np, *np_temp; // Инициализация очереди STAILQ_INIT(&head); // Добавляем элемент в самое начало очереди n1 = malloc(sizeof(struct entry)); STAILQ_INSERT_HEAD(&head, n1, next); // Добавляем элемент в самый конец очереди n1 = malloc(sizeof(struct entry)); STAILQ_INSERT_TAIL(&head, n1, next); // Добавляем элемент после указанного n2 = malloc(sizeof(struct entry)); STAILQ_INSERT_AFTER(&head, n1, n2, next); // Удаление элемента из списка STAILQ_REMOVE(&head, n2, entry, next); free(n2); // Удаление элемента из начала списка n3 = STAILQ_FIRST(&head); STAILQ_REMOVE_HEAD(&head, next); free(n3); // Сканирование вперед STAILQ_FOREACH(np, &head, next) { // Что-то делаем // np-> ... }; // Безопасное сканирование с удалением элементов STAILQ_FOREACH_SAFE(np, &head, next, np_temp) { STAILQ_REMOVE(&head, np, entry, next); free(np); }; // Ещё один способ удаления элементов - с головы while (!STAILQ_EMPTY(&head)) { n1 = STAILQ_FIRST(&head); STAILQ_REMOVE_HEAD(&head, next); free(n1); } // Быстрое удаление n1 = STAILQ_FIRST(&head); while (n1 != NULL) { n2 = STAILQ_NEXT(n1, next); free(n1); n1 = n2; }; STAILQ_INIT(&head);
Двусвязный список
Список возглавляет структура, определяемая макросом LIST_HEAD. Эта структура содержит единственный указатель на первый элемент в списке. Элементы связаны между собой двойными связями, так что произвольный элемент может быть удален без необходимости обхода всего списка. Новые элементы могут быть добавлены в список после существующего элемента, перед существующим элементом или в начало списка. Структура LIST_HEAD объявляется следующим образом:
LIST_HEAD(HEADNAME, TYPE) head;
где HEADNAME — имя определяемой структуры, а TYPE — тип элементов, которые должны быть связаны со списком. Указатель на начало списка позже может быть объявлен как:
struct HEADNAME *headp;
Имена head и headp выбираются программистом самостоятельно.
- Макрос LIST_ENTRY объявляет структуру, которая соединяет элементы в списке.
- Макрос LIST_HEAD_INITIALIZER оценивается как инициализатор для заголовка списка (например NULL).
- Макрос LIST_INIT инициализирует список, на который ссылается head.
- Макрос LIST_EMPTY принимает значение true, если в списке нет элементов.
- Макрос LIST_FIRST возвращает первый элемент в списке или NULL, если список пуст.
- Макрос LIST_PREV возвращает предыдущий элемент в списке.
- Макрос LIST_NEXT возвращает следующий элемент в списке.
- Макрос LIST_INSERT_HEAD вставляет новый элемент elm в начало списка.
- Макрос LIST_INSERT_AFTER вставляет новый элемент elm после элемента listelm.
- Макрос LIST_INSERT_BEFORE вставляет новый элемент elm перед элементом listelm.
- Макрос LIST_REMOVE удаляет элемент elm из списка.
- Макрос LIST_SWAP меняет местами содержимое head1 и head2.
- Макрос LIST_FOREACH просматривает список, на который ссылается head, в прямом направлении, присваивая каждому элементу по очереди var.
- Макрос LIST_FOREACH_FROM ведет себя идентично LIST_FOREACH(), когда var имеет значение NULL. В противном случае он обрабатывает var как ранее найденный элемент LIST и начинает цикл с var вместо первого элемента в LIST, на который ссылается head. То есть его можно использовать для продолжения просмотра списка.
- Макрос LIST_FOREACH_SAFE обходит список, на который ссылается заголовок head, в прямом направлении, присваивая каждый элемент по очереди var. Однако, в отличие от LIST_FOREACH(), здесь разрешено как удалять var, так и безопасно освобождать ее внутри цикла, не мешая обходу.
- Макрос LIST_FOREACH_FROM_SAFE ведет себя идентично LIST_FOREACH_SAFE(), когда var имеет значение NULL, в противном случае он обрабатывает var как ранее найденный элемент LIST и начинает цикл с var вместо первого элемента в LIST, на который ссылается head.
// Заголовок списка LIST_HEAD(listhead, entry) head = LIST_HEAD_INITIALIZER(head); struct listhead *headp; // Объявляем структуру для хранения данных struct entry { uint32_t data1; uint32_t data2; uint32_t data3; char string[32]; LIST_ENTRY(entry) next; } *n1, *n2, *n3, *np, *np_temp; // Инициализация списка LIST_INIT(&head); // Добавляем элемент в самое начало списка n1 = malloc(sizeof(struct entry)); LIST_INSERT_HEAD(&head, n1, next); // Добавляем элемент после указанного n2 = malloc(sizeof(struct entry)); LIST_INSERT_AFTER(n1, n2, next); // Добавляем элемент перед указанным n3 = malloc(sizeof(struct entry)); LIST_INSERT_BEFORE(n2, n3, next); // Удаление элемента из списка LIST_REMOVE(n2, next); free(n2); // Сканирование вперед LIST_FOREACH(np, &head, next) { // Что-то делаем // np-> ... }; // Безопасное сканирование с удалением элементов LIST_FOREACH_SAFE(np, &head, next, np_temp) { LIST_REMOVE(np, next); free(np); }; // Ещё один способ удаления элементов - с головы while (!LIST_EMPTY(&head)) { n1 = LIST_FIRST(&head); LIST_REMOVE(n1, next); free(n1); } // Быстрое удаление n1 = LIST_FIRST(&head); while (n1 != NULL) { n2 = LIST_NEXT(n1, next); free(n1); n1 = n2; }; LIST_INIT(&head);
Хвостовая очередь
Хвостовую очередь возглавляет структура, определяемая макросом TAILQ_HEAD . Эта структура содержит пару указателей, один на первый элемент в хвостовой очереди, а другой на последний элемент в хвостовой очереди. Элементы так же дважды связаны между собой, так что произвольный элемент может быть удален без обхода всей хвостовой очереди. Новые элементы могут быть добавлены в хвостовую очередь после существующего элемента, перед существующим элементом, в начале хвостовой очереди или в конце хвостовой очереди. Структура TAILQ_HEAD объявляется следующим образом:
TAILQ_HEAD(HEADNAME, TYPE) head;
где HEADNAME — имя определяемой структуры, а TYPE — тип элементов, которые должны быть связаны с очередью. Указатель на начало очереди позже может быть объявлен как:
struct HEADNAME *headp;
Имена head и headp выбираются программистом самостоятельно.
- Макрос TAILQ_HEAD_INITIALIZER оценивается как инициализатор хвостовой очереди head.
- Макрос TAILQ_ENTRY объявляет структуру, которая соединяет элементы в хвостовой очереди.
- Макрос TAILQ_INIT инициализирует хвостовую очередь, на которую ссылается head.
- Макрос TAILQ_EMPTY принимает значение true, если в хвостовой очереди нет элементов.
- Макрос TAILQ_LAST возвращает последний элемент в хвостовой очереди. Если хвостовая очередь пуста, возвращаемое значение равно NULL.
- Макрос TAILQ_FIRST возвращает первый элемент в хвостовой очереди или NULL, если хвостовая очередь пуста.
- Макрос TAILQ_NEXT возвращает следующий элемент в хвостовой очереди или NULL, если этот элемент является последним.
- Макрос TAILQ_PREV возвращает предыдущий элемент в хвостовой очереди или NULL, если этот элемент является первым.
- Макрос TAILQ_INSERT_HEAD вставляет новый элемент elm в начало хвостовой очереди.
- Макрос TAILQ_INSERT_TAIL вставляет новый элемент elm в конец хвостовой очереди.
- Макрос TAILQ_INSERT_AFTER вставляет новый элемент elm после элемента listelm.
- Макрос TAILQ_INSERT_BEFORE вставляет новый элемент elm перед элементом listelm.
- Макрос TAILQ_REMOVE удаляет элемент elm из хвостовой очереди.
- Макрос TAILQ_CONCAT объединяет хвостовую очередь, возглавляемую head2, с концом очереди, возглавляемой head1, удаляя все записи из первой.
- Макрос TAILQ_FOREACH обходит хвостовую очередь, на которую ссылается head, в прямом направлении, присваивая каждому элементу по очереди var. var устанавливается в NULL, если цикл завершается нормально или если элементов не было.
- Макрос TAILQ_FOREACH_REVERSE проходит хвостовую очередь, на которую ссылается head, в обратном направлении, присваивая каждый элемент по очереди var.
- Макросы TAILQ_FOREACH_SAFE и TAILQ_FOREACH_REVERSE_SAFE обходят список, на который ссылается заголовок head, в прямом или обратном направлении соответственно, присваивая каждый элемент по очереди var. Однако, в отличие от своих небезопасных аналогов, TAILQ_FOREACH и TAILQ_FOREACH_REVERSE позволяют как удалить переменную var, так и безопасно освободить ее внутри цикла, не мешая обходу.
// Заголовок хвостовой очереди TAILQ_HEAD(tailhead, entry) head = TAILQ_HEAD_INITIALIZER(head); struct tailhead *headp; // Объявляем структуру для хранения данных struct entry { uint32_t data1; uint32_t data2; uint32_t data3; char string[32]; TAILQ_ENTRY(entry) next; } *n1, *n2, *n3, *np, *np_temp; // Инициализация очереди TAILQ_INIT(&head); // Добавляем элемент в самое начало очереди n1 = malloc(sizeof(struct entry)); TAILQ_INSERT_HEAD(&head, n1, next); // Добавляем элемент в самый конец очереди n1 = malloc(sizeof(struct entry)); TAILQ_INSERT_TAIL(&head, n1, next); // Добавляем элемент после указанного n2 = malloc(sizeof(struct entry)); TAILQ_INSERT_AFTER(&head, n1, n2, next); // Добавляем элемент перед указанным n3 = malloc(sizeof(struct entry)); TAILQ_INSERT_BEFORE(n2, n3, next); // Удаление элемента из списка TAILQ_REMOVE(&head, n3, next); free(n3); // Сканирование вперед TAILQ_FOREACH(np, &head, next) { // Что-то делаем // np-> ... }; // Безопасное сканирование с удалением элементов TAILQ_FOREACH_SAFE(np, &head, next, np_temp) { TAILQ_REMOVE(&head, np, next); free(np); }; // Ещё один способ удаления элементов - с головы while (!TAILQ_EMPTY(&head)) { n1 = TAILQ_FIRST(&head); TAILQ_REMOVE(&head, n1, next); free(n1); } // Быстрое удаление n1 = TAILQ_FIRST(&head); while (n1 != NULL) { n2 = TAILQ_NEXT(n1, next); free(n1); n1 = n2; }; TAILQ_INIT(&head);
Ссылки
- Исходники для данной статьи: https://github.com/kotyara12/dzen/blob/master/queues/src/main.c
- Пример использования очереди в ESP-IDF – очередь отправки MQTT: https://github.com/espressif/esp-mqtt/blob/master/lib/mqtt_outbox.c
- Пример использования связных списков для хранения параметров: https://github.com/kotyara12/reParams/blob/master/src/reParams.cpp
- Пример использования связных списков для хранения настроек зон охраны в ОПС: https://github.com/kotyara12/reAlarm/blob/master/src/reAlarm.cpp
📌 Полный архив статей вы найдете здесь
Пожалуйста, оцените статью:
-= Каталог статей (по разделам) =- -= Архив статей (подряд) =-