Menu

Указатель на структуру в C: оператор стрелка и структуры в куче

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

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Структура, переданная в функцию, копируется. Для точки из двух int это нормально, а для записи в 200 байт расточительно — и делает изменение невозможным, потому что функция видит только собственную копию. Указатель на структуру решает обе проблемы разом, а ещё это единственный способ строить растущие структуры данных: списки, деревья, графы.

Как взять указатель на структуру

Ничего особенного не требуется. & даёт адрес, а тип получается «указатель на структуру»:

Все три печатают 3. Последние две — одна и та же операция, записанная двумя способами.

Почему в (*ptr).x нужны скобки

Может возникнуть желание их опустить и написать *ptr.x. Это компилируется во что-то совсем другое, а чаще вообще отказывается компилироваться, потому что оператор доступа к полю . имеет более высокий приоритет, чем разыменование *:

*ptr.x        // разбирается как *(ptr.x)  -- неверно
(*ptr).x      // сначала разыменовать, потом взять поле -- верно
ptr->x        // то же самое, только короче

ptr.x просит поле у указателя, а у указателя полей нет. Жалоба компилятора («request for member 'x' in something not a structure or union») — это переодетая ошибка приоритетов.

Поскольку форма со скобками уродлива и в ней легко ошибиться, C предоставляет ->. На практике (*p).x в реальном коде вы почти не увидите: берите p->x и забудьте о длинной форме — разве что как об объяснении того, что означает стрелка.

Передача структуры по указателю

Параметр-указатель даёт функции настоящую структуру вызывающего, поэтому изменения сохраняются:

Две привычки, которые стоит здесь выработать:

  • Помечайте указатели только для чтения как const. const struct Point *p обещает, что функция не изменит то, на что указывает p. Если кто-то позже добавит внутрь p->x = 0, сборка сломается вместо того, чтобы баг уехал в релиз. Это ещё и документирует намерение в месте вызова.
  • Передавайте указатель для больших структур даже при чтении. Структура с буфером имени на 64 байта стоит 64 байта копирования на каждый вызов по значению; указатель стоит 8.

Небольшие структуры по-прежнему нормально передавать по значению: struct Point в 8 байт копируется так же дёшево, как указатель, а версия по значению не может быть NULL, что убирает целый класс отказов.

Указатели внутри массивов структур

Арифметика указателей на массивах структур работает ровно так же, как на массивах int: p + 1 продвигается на одну целую структуру, включая байты выравнивания.

Имя массива превращается в указатель на первый элемент, поэтому при вызове staff уже является struct Employee *. По той же причине функции нужно отдельно передавать n: sizeof внутри функции измерил бы указатель, а не массив.

Размещение структуры в куче

Структуры на стеке умирают в конце своей области видимости. Чтобы создать такую, что переживёт создавшую её функцию, — или чтобы создать столько, сколько потребует ввод, — выделяйте память через malloc:

Четыре детали в этой маленькой функции — все несущие:

  • sizeof *e, а не sizeof(struct Employee). Это читается как «размер того, на что указывает e», поэтому при смене типа выделение подстроится само. Разойтись этим двум вещам просто негде.
  • Проверка на NULL. malloc возвращает NULL, когда не может удовлетворить запрос. Запись e->id через нулевой указатель — это ошибка сегментации.
  • Инициализация каждого поля. malloc не обнуляет память; структура приходит забитой мусором. (calloc(1, sizeof *e) обнулит её за вас.)
  • Кто-то обязан вызвать free. Возврат выделенного указателя перекладывает эту обязанность на вызывающего. Скажите об этом в комментарии: неясное правило владения — это то, с чего начинаются утечки памяти.

Ради чего всё: узел связного списка

Вот то, чего структуры не могут без указателей. Структура не может содержать саму себя — она была бы бесконечно большой, — но может содержать указатель на структуру своего же типа, и этот единственный приём строит все связные структуры данных в C.

Обратите внимание, что struct Node *next; ссылается на struct Node в тот момент, когда struct Node ещё объявляется. Это законно именно потому, что у указателя известный размер независимо от того, на что он указывает: полное определение компилятору пока не нужно. Это же единственное место, где приходится использовать тег struct Node, даже если вы написали и typedef: имя из typedef внутри собственного определения ещё не существует.

Цикл free_list сохраняет head->next до вызова free(head). Чтение head->next после освобождения — это использование освобождённой памяти: она больше не ваша, и прочитанное значение будет тем, что положил туда аллокатор.

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

  • p->x на неинициализированном или NULL указателе. Самая частая причина падения в коде, насыщенном структурами. Инициализируйте указатели значением NULL и проверяйте перед разыменованием.
  • Возврат указателя на локальную структуру. struct Point *bad(void) { struct Point p = {1,2}; return &p; } возвращает адрес памяти, которая перестаёт существовать в момент возврата. Возвращайте структуру по значению или выделяйте её в куче.
  • Двойное освобождение или забытый free. Каждому malloc соответствует ровно один free. После освобождения присвойте указателю NULL, чтобы позднее случайное использование громко упало, а не тихо испортило память.
  • Копирование структуры, содержащей указатель. Присваивание структуры копирует значение указателя, а не то, на что он указывает. Теперь обе структуры ссылаются на один буфер, и та, что освободит его первой, оставит другую с висячим указателем.

Часто задаваемые вопросы

Что делает оператор -> в C?

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

Почему *p.x не работает для указателя на структуру?

Потому что . связывает сильнее, чем *. *p.x разбирается как *(p.x): взять поле x у p (а это указатель, а не структура) и разыменовать его. Нужно писать (*p).x или просто p->x.

Передавать структуру по значению или по указателю в C?

По указателю — когда функция должна изменить структуру вызывающего или когда структура велика и копировать её расточительно. По значению — для небольших структур только для чтения, где копия дёшева, а код проще. Помечайте параметры-указатели только для чтения как const struct T *, чтобы обещание проверял компилятор.

Как выделить структуру через malloc в C?

struct Node *n = malloc(sizeof *n); — форма sizeof *n спрашивает размер того, на что указывает n, поэтому она останется верной, если тип позже изменится. Проверьте результат на NULL, инициализируйте каждое поле и вызовите free(n), когда закончите.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ