Указатели на структуры и оператор стрелки

Урок соединяет две большие темы — структуры и указатели: как работает указатель на структуру, зачем нужен оператор стрелки и как создавать структуры в динамической памяти.
Оператор стрелки p->field — это просто удобная запись для (*p).field: «разыменуй указатель и возьми поле». Через указатели на структуры строятся списки, деревья и все сложные структуры данных C.

Когда у нас есть указатель на структуру, доступ к полю выглядит иначе. Можно написать (*p).x — разыменовать и взять поле, — но это громоздко. Для этого придумали оператор стрелки:

struct Point { int x, y; };

struct Point pt = {3, 4};
struct Point *p = &pt;        // указатель на структуру

printf("%d\n", (*p).x);        // 3 — длинная запись
printf("%d\n", p->x);          // 3 — то же через стрелку

p->x = 100;                    // меняем поле через указатель
printf("%d\n", pt.x);          // 100 — изменился оригинал

Запись p->x полностью эквивалентна (*p).x, но читается легче. Это самый частый способ обращения к полям, потому что в реальном коде структуры почти всегда передаются и хранятся через указатели.

Как работает под капотом

Покажем, что стрелка — не магия, а сокращение. И зачем структуры размещают в динамической памяти:

struct Point *p = &pt;

p ----> pt: [ x: 3 ][ y: 4 ]
              ^
   p хранит адрес структуры

p->x  означает:  (*p).x
                 |
                 1. *p  — идём по адресу к структуре
                 2. .x  — берём поле x
                 итог: 3

Динамическая структура:
   struct Point *q = malloc(sizeof(struct Point));
   q ----> [ ? ? ] (на куче)
   q->x = 5;  q->y = 9;   // заполняем поля
   free(q);               // освобождаем

Размещение структуры в куче через malloc — основа динамических структур данных. Например, у узла связного списка есть поле-указатель на следующий узел, и каждый узел создаётся отдельным malloc. Так строятся списки, деревья, графы.

// Узел связного списка
struct Node {
    int data;
    struct Node *next;   // указатель на такой же узел
};

struct Node *n = malloc(sizeof(struct Node));
n->data = 42;
n->next = NULL;          // пока конец списка
printf("%d\n", n->data);
free(n);

Частые ошибки

  • Точка вместо стрелки. p.x для указателя — ошибка; нужно p->x или (*p).x.
  • Разыменование без выделения. struct Node *n; n->data = 5; без malloc — запись по мусорному адресу.
  • Забыли free для каждого узла. В списках и деревьях нужно освобождать каждый узел, иначе утечка.
  • Скобки в (*p).x. Без скобок *p.x трактуется неверно из-за приоритета — точка сильнее звёздочки.

Best practices

  • Используйте стрелку -> для указателей — это стандарт и читается яснее, чем (*p).field.
  • Сразу после malloc структуры проверяйте результат на NULL и инициализируйте поля, особенно поля-указатели — в NULL.
  • Для каждой динамически созданной структуры предусмотрите парный free; в связных структурах освобождайте узлы по порядку.

Связный список — идеальная иллюстрация указателей на структуры. Смоделируем его на Python через словари-узлы со ссылкой next.

# Узел списка как структура: data + ссылка на следующий
def make_node(data):
    return {"data": data, "next": None}   # next = NULL

# собираем список 1 -> 2 -> 3
head = make_node(1)
head["next"] = make_node(2)
head["next"]["next"] = make_node(3)

# проход по списку через "указатель" cur
cur = head
while cur is not None:        # пока не NULL
    print(cur["data"], end=" -> ")
    cur = cur["next"]         # cur = cur->next
print("NULL")

Та же логика на Python ▶ — поле next играет роль указателя struct Node *next, а проход cur = cur["next"] — это cur = cur->next в C. Так устроены все связные структуры.

Полный жизненный цикл связного списка

Указатели на структуры раскрываются в полную силу именно на динамических структурах, и связный список — канонический пример. Его сила в том, что вставка и удаление элемента в середину не требуют сдвига данных, как в массиве: достаточно перенаправить пару указателей next. Платой за это служит потеря быстрого доступа по индексу — чтобы добраться до n-го элемента, приходится пройти всю цепочку с начала. Но главная практическая трудность — корректное освобождение: чтобы не потерять остаток списка, перед free текущего узла нужно сохранить указатель на следующий, иначе после освобождения вы уже не сможете до него добраться. Эта аккуратность с порядком операций — типичный навык работы с динамическими структурами в C, и он переносится дальше на деревья, очереди и хеш-таблицы.

Итоги

Оператор стрелки p->field — это сокращение для (*p).field: разыменовать указатель и взять поле. Структуры почти всегда используют через указатели, в том числе размещая их в куче через malloc. Указатель-поле внутри структуры (struct Node *next) — основа связных списков, деревьев и графов. Главные правила — стрелка для указателей, проверка malloc на NULL и парный free для каждого узла.

Проверьте себя
1. Чему эквивалентна запись p->x для указателя на структуру?
Ap.x
B(*p).x
C&p.x
D*p.x
2. Что лежит в основе связного списка в C?
AДвумерный массив
BСтруктура, содержащая поле-указатель на такую же структуру (например, struct Node *next)
CГлобальная переменная
DТолько статические массивы