Menu

typedef w C: nazywanie typów, idiom typedef struct i kiedy go unikać

Jak typedef nadaje istniejącemu typowi w C nową nazwę: idiom typedef struct z etykietą i bez niej, typedef dla wskaźników na funkcje i tablic oraz jedyny przypadek, w którym typedef szkodzi: ukrywanie wskaźnika.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

typedef robi jedną małą rzecz: nadaje typowi, który już istnieje, inną nazwę. Nie powstaje żaden nowy typ, nie jest rezerwowana pamięć i nic w zachowaniu wartości się nie zmienia. To narzędzie do nazywania, a w C, gdzie wbudowane nazwy typów bywają długie (unsigned long long, struct Employee, void (*)(int)), dobra nazwa jest wiele warta.

Podstawowa postać

Składnia wygląda jak deklaracja zmiennej z doklejonym z przodu typedef. Tam, gdzie byłaby nazwa zmiennej, wstawiasz nazwę nowego typu:

unsigned long  count;           // deklaruje zmienna o nazwie count
typedef unsigned long  ulong;   // deklaruje TYP o nazwie ulong

To cała zasada i wyjaśnia ona każdy typedef, jaki kiedykolwiek przeczytasz, łącznie z tymi dziwnie wyglądającymi dalej na tej stronie.

Zwróć uwagę, czego para Celsius/Fahrenheit nie daje: bezpieczeństwa typów. Obie to po prostu double, więc przekazanie wartości w Fahrenheitach tam, gdzie oczekiwane są Celsjusze, spokojnie się skompiluje. Nazwy dokumentują intencję dla ludzi, ale nie tworzą żadnej kontroli.

Idiom typedef struct

To powód, dla którego większość programów w C w ogóle zawiera typedef. W C nazwa typu struktury zawiera słowo kluczowe:

struct Point { int x; int y; };
struct Point p;              /* slowo "struct" za kazdym razem */

Typedef to skraca:

Przeczytaj to według wcześniejszej zasady: usuń typedef, a dostaniesz struct Point {...} Point;, czyli deklarację zmiennej o nazwie Point. Dodaj typedef z powrotem, a Point stanie się zamiast tego nazwą typu.

Z etykietą czy bez

Etykietę można całkowicie pominąć i dostać anonimową strukturę, która ma tylko nazwę z typedef:

typedef struct {
    int x;
    int y;
} Point;          /* nie istnieje zadne "struct Point", tylko "Point" */

To schludniejsze i przy zwykłym rekordzie danych to popularny styl. Ale zawodzi w chwili, gdy struktura musi wspomnieć o sobie, bo nazwy z typedef nie można użyć, dopóki jej własna deklaracja się nie skończy:

/* NIE kompiluje sie: "Node" nie jest jeszcze typem wewnatrz wlasnych nawiasow. */
typedef struct {
    int value;
    Node *next;
} Node;

Zachowaj etykietę, a odwołanie do siebie zadziała, bo struct Node jest dostępne, gdy tylko kompilator zobaczy etykietę:

Praktyczna zasada: zachowuj etykietę. Kosztuje jedno słowo, pasuje do nazwy z typedef, więc nic nie myli, i zostawia otwartą drogę dla odwołań do siebie oraz deklaracji zapowiadających w nagłówkach. Na stronie o strukturach i wskaźnikach zobaczysz, w co rozrasta się taki węzeł.

Deklaracje zapowiadające i typy nieprzezroczyste

Wskaźnik na strukturę ma znany rozmiar nawet wtedy, gdy zawartość struktury jest nieznana, więc nagłówek może udostępnić typ bez ujawniania jego pól:

/* stack.h */
typedef struct Stack Stack;     /* zadeklarowany, nie zdefiniowany */

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

Pełne struct Stack { ... }; mieszka w stack.c i nikt z zewnątrz nie może dotknąć jego pól. Ten wzorzec typu nieprzezroczystego (opaque type) to sposób, w jaki biblioteki C wymuszają hermetyzację, i dokładnie tym jest FILE w bibliotece standardowej: dostajesz FILE * z fopen i nie dowiadujesz się niczego o tym, co jest w środku. Zobacz obsługę plików.

typedef dla enumów i unii

Ten sam idiom dotyczy dwóch pozostałych rodzajów typów złożonych:

Bez typedef byłyby to enum Color c; i union Number n;. Co faktycznie robią, opisują strony o enumach i uniach.

typedef dla wskaźnika na funkcję

Tu typedef przestaje być wygodą i staje się niemal koniecznością. Surowa składnia wskaźnika na funkcję jest słynna z nieczytelności:

void (*handler)(int, const char *);                /* zmienna */
void register_cb(void (*cb)(int, const char *));   /* parametr */

Nazwij typ raz, a oba zapisy staną się czytelne:

Zastosuj znowu zasadę usuwania typedef: samo int (*IntBinaryOp)(int, int); zadeklarowałoby zmienną o nazwie IntBinaryOp, wskazującą na funkcję, która przyjmuje dwie liczby int i zwraca int. Z typedef staje się to nazwą typu. Więcej na stronie o wskaźnikach na funkcje.

typedef a #define

Przy prostych aliasach wyglądają na zamienne, ale takie nie są:

#define INT_PTR int *
typedef int *int_ptr;

INT_PTR  a, b;    /* rozwija sie do: int *a, b;  czyli b to zwykly int! */
int_ptr  c, d;    /* zarowno c, jak i d to int *              */

#define to ślepe podstawienie tekstu wykonywane, zanim kompilator zobaczy kod, a typedef to prawdziwa deklaracja, którą kompilator rozumie i która dotyczy każdej nazwy na liście. Używaj typedef dla typów, a #define zostaw dla stałych i makr.

Kiedy typedef szkodzi

Typedef może też ukryć informacje, których czytelnik potrzebuje. Najostrzejszy przykład to ukrywanie wskaźnika:

typedef struct Node *NodeRef;    /* gwiazdka znika */

NodeRef n = get_node();

Patrząc na NodeRef n, czytelnik nie wie, czy n może być NULL, czy trzeba je zwolnić, czy n i jego kopia współdzielą ten sam obiekt ani czy do pól sięga się przez ., czy przez ->. Na wszystkie cztery pytania natychmiast odpowiada struct Node *n.

Biblioteka standardowa się z tym zgadza: robi typedef dla struktury (FILE) i zostawia gwiazdkę w miejscu użycia (FILE *fp). Wynikają z tego dwie rozsądne wytyczne:

  • Rób typedef dla struktur, unii, enumów i wskaźników na funkcje: usunięty szum nie niesie żadnej informacji.
  • Nie rób typedef dla wskaźnika tylko po to, żeby go skrócić, i nie rób typedef dla typu podstawowego, żeby go zamaskować (typedef int Boolean; zachęca, by ktoś zapisał w nim 7; zamiast tego użyj bool z <stdbool.h>, opisanego na stronie o typach logicznych).

Własne nazwy o stałej szerokości z <stdint.h> w bibliotece standardowej (uint32_t, int64_t, size_t) to wszystko typedefy i są one najlepszym argumentem za tą funkcją języka: mówią dokładnie, czym są, są przenośne i niczego nie ukrywają.

Najczęściej zadawane pytania

Co robi typedef w C?

Nadaje istniejącemu typowi drugą nazwę. typedef unsigned long ulong; oznacza, że od tej chwili ulong i unsigned long to ten sam typ. Nie tworzy nowego typu ani nowej pamięci, a jedynie krótszy lub bardziej opisowy sposób zapisania typu, który już masz.

Czym jest idiom typedef struct w C?

typedef struct Point { int x; int y; } Point; deklaruje strukturę i nazywa typ Point w jednej instrukcji, więc możesz pisać Point p; zamiast struct Point p;. Zachowanie etykiety (struct Point) ma znaczenie, gdy struktura musi odwoływać się do samej siebie, jak węzeł listy wiązanej.

Czym różni się typedef od #define?

typedef obsługuje kompilator i tworzy prawdziwy alias typu, a #define to podstawienie tekstu wykonywane przez preprocesor przed kompilacją. Ta różnica gryzie przy wskaźnikach: #define PTR int* sprawia, że PTR a, b; rozwija się do int* a, b;, więc wskaźnikiem jest tylko a. typedef int *PTR; daje dwa wskaźniki.

Czy w C robić typedef dla typu wskaźnikowego?

Zwykle nie. typedef struct Node *NodeRef; ukrywa fakt, że typ jest wskaźnikiem, więc czytelnik nie wie, czy zmienna może być NULL, czy trzeba ją zwolnić ani czy potrzebuje ->. Biblioteka standardowa właśnie dlatego zostawia gwiazdkę widoczną w FILE *. Rób typedef dla struktury, a nie dla wskaźnika na nią.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ