Макрос перебора списков в стиле for-each

Последнее обновление: 01.07.2025

Перебор коллекций данных в стиле 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 для универсального списка

Предыдущий пример перебора списка привязан к конкретной реализации. Но в прошлой статье был показан пример универсального обобщенного списка, который не зависит от конкретного типа данных. Определим макрос перебора в стиле 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
Помощь сайту
Юмани:
410011174743222
Номер карты:
4048415020898850