Перебор коллекций данных в стиле for-each является популярным способом перебора наборов данных в различных языках программирования. В языке Си по умолчанию отсутствует подобная конструкция,
но с помощью макросов мы можем соорудить некоторый аналог.
В зависимости от типа перебираемых данных, наших задач, каких-то других условий реализации for-each могут различаться. Перевуд лишь несколько примеров. Начнем с простейшего примера:
#include <stdio.h>
#include <stdlib.h>
#define foreach( iter__, head__ ) \
for ( iter__ = (head__); iter__ != 0; iter__ = iter__->next )
typedef struct node{
struct node* next;
int value;
} node;
int main( void )
{
// узлы списка
node n1 = {.value = 2};
node n2 = {.value = 3};
node n3 = {.value = 4};
node n4 = {.value = 5};
// устанавливаем отношения между узлами, формируя однонаправленный список
n1.next = &n2;
n2.next = &n3;
n3.next = &n4;
n4.next = 0;
node* v; // переменная, в которую извлекаем каждый узел списка
// применение for-each - каждый элемент
foreach(v, &n1) printf("%d\n", v->value);
}
Эта программа создает и инициализирует простой односвязный список, а затем использует специальный макрос foreach для обхода списка и вывода значения каждого узла.
Вначале идет определение макроса foreach:
#define foreach( iter__, head__ ) \ for ( iter__ = (head__); iter__ != 0; iter__ = iter__->next )
Макрос принимает две переменных:
iter__: Переменная, которая будет указывать на текущий узел в каждой итерации.
head__: Указатель на первый узел списка.
После заголовка макроса идет определение цикла for, с помощью которого и происходит собственно перебор данных:
v = (&n1): В начале цикла v (наш итератор) инициализируется адресом первого узла списка (n1).
v != 0: Условие продолжения цикла. Цикл продолжается до тех пор, пока v не станет нулевым указателем (NULL), что означает конец списка.
v = v->next: В каждой итерации v переходит к следующему узлу в списке, используя поле next текущего узла.
В качестве примера для переьора данных определена структура node:
typedef struct node{
struct node* next;
int value;
} node;
Здесь поле struct node* next хранит указатель на следующий узел в списке, а поле int value представляет целочисленное значение - собственно те данные, которые
хранятся в данном узле.
В функции main cоздаем узлы списка и устанавливаем связи между ними:
node n1 = {.value = 2};
node n2 = {.value = 3};
node n3 = {.value = 4};
node n4 = {.value = 5};
n1.next = &n2;
n2.next = &n3;
n3.next = &n4;
n4.next = 0; // или NULL
В конце идет собственно итерация по списку с помощью foreach:
node* v; // переменная, в которую извлекаем каждый узел списка
foreach(v, &n1) printf("%d\n", v->value);
Здесь объявляется указатель v типа node*. Затем используется макрос foreach. Он начинает обход с узла n1. В каждой итерации
v указывает на текущий узел. Процесс продолжается до тех пор, пока v не станет NULL (после n4), что завершает цикл.
Вывод программы
2 3 4 5
Предыдущий пример перебора списка привязан к конкретной реализации. Но в прошлой статье был показан пример универсального обобщенного списка, который не зависит от конкретного типа данных. Определим макрос перебора в стиле for-each для подобного списка:
#include <stdio.h>
#include <stdlib.h>
#define DEFINE_LIST(T) \
typedef struct { \
T* data; \
size_t count; \
size_t size; \
} list_##T; \
\
void list_##T##_new(list_##T* v) { \
v->data = NULL; \
v->count= v->size = 0; \
} \
\
void list_##T##_push(list_##T* v, T elem) { \
if (v->count == v->size) { \
v->size = v->size ? v->size << 1 : 1; \
v->data = (T*)realloc(v->data, v->size * sizeof(T)); \
} \
v->data[v->count++] = elem; \
} \
\
T list_##T##_pop(list_##T* v) { \
return v->data[--v->count]; \
} \
\
void list_##T##_free(list_##T* v) { \
free(v->data); \
list_##T##_new(v); \
}
DEFINE_LIST(int) // определяем список
// Определение макроса перебора в стиле for-each
#define for_each(elem_name, list_ptr) \
for (size_t i = 0; i < (list_ptr)->count;i++) \
if((elem_name = (list_ptr)->data[i]), 1)
int main( void )
{
list_int numbers; // определяем переменную списка
list_int_new(&numbers); // инициализируем его поля
// добавляем несколько элементов
list_int_push(&numbers, 11);
list_int_push(&numbers, 12);
list_int_push(&numbers, 13);
puts("List items:");
int item; // Объявляем переменную для элемента
for_each(item, &numbers) {
printf("%d\n", item);
}
list_int_free(&numbers); // очищаем память под список
}
Я не буду подробно разбирать все макросы, которые определяют функционал обобщенного списка (подробно можно посмотреть в прошлой статье - https://metanit.com/c/tutorial/12.3.php.
Остановлюсь только на макросе for_each:
#define for_each(elem_name, list_ptr)
Здесь определяем макрос, который принимает два параметра - перебираемый список в виде параметра list_ptr и параметр elem_name - название переменной, в которую помещается каждый элемент списка. Важно: elem_var должна быть объявлена перед использованием макроса.
for (size_t i = 0; i < (list_ptr)->count; i++) \
стандартный цикл for, который итерируется по индексам списка от 0 до count - 1
if((elem_name = (list_ptr)->data[i]), 1)
Это хитрая часть. Вначале происходит присваивание текущего элемента списка переменной elem_var: (elem_name = (list_ptr)->data[i])
Далее идет часть ", 1": оператор запятой вычисляет свои операнды слева направо и возвращает значение правого операнда. В данном случае, после присваивания elem_var, выражение всегда возвращает 1 (true).
Это означает, что тело if (и, следовательно, тело "for-each" цикла) всегда будет выполняться.
Это позволяет выполнить операцию присваивания и при этом гарантировать выполнение следующего блока кода, который будет телом цикла.
Консольный вывод программы:
List items: 11 12 13