Menu

typedef в C: имена для типов, идиома typedef struct и когда без неё лучше

Как typedef даёт существующему типу новое имя: идиома typedef struct с тегом и без, typedef для указателей на функции и массивов и единственный случай, когда typedef вредит, — когда он прячет указатель.

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

typedef делает одну небольшую вещь: даёт уже существующему типу ещё одно имя. Новый тип не создаётся, память не отводится, и в поведении значения ничего не меняется. Это инструмент именования, а в C, где встроенные имена типов длинны (unsigned long long, struct Employee, void (*)(int)), хорошее имя стоит немало.

Базовая форма

Синтаксис читается как объявление переменной с приделанным спереди typedef. Там, где стояло бы имя переменной, вы пишете новое имя типа:

unsigned long  count;    // объявляет переменную с именем count
typedef unsigned long  ulong;   // объявляет ТИП с именем ulong

Это всё правило целиком, и оно объясняет любой typedef, который вам встретится, включая странные на вид ниже на этой странице.

Обратите внимание, чего пара Celsius/Fahrenheit вам не даёт: типобезопасности. Оба типа — просто double, поэтому передача значения в Фаренгейтах туда, где ожидаются Цельсии, спокойно скомпилируется. Имена документируют намерение для людей, но не создают проверки.

Идиома typedef struct

Именно из-за неё typedef вообще встречается в большинстве программ на C. В C имя типа структуры включает ключевое слово:

struct Point { int x; int y; };
struct Point p;              /* слово "struct" каждый раз */

typedef это сворачивает:

Прочитайте это по правилу из начала: уберите typedef — и получится struct Point {...} Point;, объявление переменной с именем Point. Верните typedef — и Point становится именем типа.

С тегом или без

Тег можно вообще опустить, получив анонимную структуру, у которой есть только имя из typedef:

typedef struct {
    int x;
    int y;
} Point;          /* никакого "struct Point" не существует - только "Point" */

Это опрятнее, и для простой записи с данными такой стиль распространён. Но он ломается в тот момент, когда структуре нужно упомянуть саму себя, потому что имя из typedef недоступно, пока его собственное объявление не завершится:

/* НЕ компилируется: "Node" внутри собственных скобок ещё не тип. */
typedef struct {
    int value;
    Node *next;
} Node;

Оставьте тег — и самоссылка заработает, потому что struct Node доступен сразу, как только тег увиден:

Практическое правило: сохраняйте тег. Он стоит одного слова, совпадает с именем из typedef, так что путаницы нет, и оставляет открытой дверь для самоссылок и предварительных объявлений в заголовках. Во что вырастает этот связанный узел, см. в статье структуры и указатели.

Предварительные объявления и непрозрачные типы

Поскольку у указателя на структуру известный размер даже тогда, когда содержимое структуры неизвестно, заголовок может выдать тип, не раскрывая его полей:

/* stack.h */
typedef struct Stack Stack;     /* объявлено, но не определено */

Stack *stack_create(void);
void   stack_push(Stack *s, int value);
int    stack_pop(Stack *s);
void   stack_destroy(Stack *s);

Полное struct Stack { ... }; живёт в stack.c, и никто снаружи не может тронуть его поля. Такой шаблон непрозрачного типа — способ, которым библиотеки на C обеспечивают инкапсуляцию, и ровно им является FILE в стандартной библиотеке: вы получаете FILE * от fopen, и вам ничего не сообщают о том, что внутри. См. работа с файлами.

typedef для перечислений и объединений

Та же идиома применима к двум другим составным видам:

Без typedef это были бы enum Color c; и union Number n;. Чем они на самом деле занимаются, см. в статьях перечисления и объединения.

typedef для указателя на функцию

Вот здесь typedef перестаёт быть удобством и становится почти необходимостью. Сырой синтаксис указателя на функцию знаменит своей нечитаемостью:

void (*handler)(int, const char *);        /* переменная */
void register_cb(void (*cb)(int, const char *));   /* параметр */

Назовите тип один раз — и оба становятся читаемыми:

Примените правило «убрать typedef» ещё раз: сама по себе строка int (*IntBinaryOp)(int, int); объявила бы переменную с именем IntBinaryOp, указывающую на функцию, которая принимает два int и возвращает int. С typedef это становится именем типа. Подробнее — в статье указатели на функции.

typedef против #define

Для простых псевдонимов они выглядят взаимозаменяемыми, но таковыми не являются:

#define INT_PTR int *
typedef int *int_ptr;

INT_PTR  a, b;    /* раскрывается в: int *a, b;  -- b обычный int! */
int_ptr  c, d;    /* и c, и d являются int *     */

#define — слепая текстовая подстановка, выполняемая до того, как компилятор увидит код; typedef — настоящее объявление, которое компилятор понимает и которое относится к каждому имени в списке. Используйте typedef для типов, а #define оставьте для констант и макросов.

Когда typedef вредит

Typedef может и скрыть информацию, которая нужна читателю. Самый острый пример — сокрытие указателя:

typedef struct Node *NodeRef;    /* звёздочка исчезает */

NodeRef n = get_node();

Глядя на NodeRef n, читатель не может понять, может ли n быть NULL, нужно ли его освобождать, разделяют ли n и его копия один объект и как обращаться к полям — через . или ->. На все четыре вопроса мгновенно отвечает запись struct Node *n.

Стандартная библиотека с этим согласна: она делает typedef для структуры (FILE) и оставляет звёздочку в месте использования (FILE *fp). Отсюда вытекают два разумных ориентира:

  • Стоит делать typedef для структур, объединений, перечислений и указателей на функции: убранный шум не несёт информации.
  • Не стоит делать typedef для указателя просто чтобы сократить запись, и не стоит переименовывать базовый тип, чтобы его замаскировать (typedef int Boolean; провоцирует кого-нибудь положить туда 7 — используйте bool из <stdbool.h>, о чём в статье логические значения).

Собственные имена фиксированной ширины из <stdint.h> (uint32_t, int64_t, size_t) — все typedef, и они лучший аргумент в пользу этого средства: они говорят ровно то, чем являются, они переносимы, и ничего не скрыто.

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

Что делает typedef в C?

Он даёт существующему типу второе имя. typedef unsigned long ulong; означает, что с этого момента ulong и unsigned long — один и тот же тип. Ни нового типа, ни новой памяти при этом не появляется, только более короткий или более выразительный способ записать то, что у вас уже есть.

Что такое идиома typedef struct в C?

typedef struct Point { int x; int y; } Point; одной инструкцией объявляет структуру и называет тип Point, так что можно писать Point p; вместо struct Point p;. Сохранять тег (struct Point) важно, когда структуре нужно сослаться на саму себя, как узлу связного списка.

Чем typedef отличается от #define?

typedef обрабатывается компилятором и создаёт настоящий псевдоним типа; #define — это текстовая подстановка, выполняемая препроцессором до компиляции. Разница кусается на указателях: #define PTR int* превращает PTR a, b; в int* a, b;, где указатель только a. А typedef int *PTR; делает указателями оба.

Стоит ли делать typedef для типа указателя в C?

Обычно нет. typedef struct Node *NodeRef; скрывает то, что тип является указателем, и читатель не может понять, может ли переменная быть NULL, нужно ли её освобождать и обращаться ли к полям через ->. В стандартной библиотеке FILE * сохраняет звёздочку видимой именно поэтому. Делайте typedef для структуры, а не для указателя на неё.

Coddy programming languages illustration

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

НАЧАТЬ