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 nim7; zamiast tego użyjboolz<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ą.