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
זו הסיבה שרוב תוכניות ה-C מכילות typedef בכלל. ב-C שם הטיפוס של struct כולל את מילת המפתח:
struct Point { int x; int y; };
struct Point p; /* המילה "struct" בכל פעם מחדש */
typedef מקצר את זה:
קראו את זה עם הכלל מקודם: הסירו את ה-typedef ונשארתם עם struct Point {...} Point;, הצהרה על משתנה בשם Point. החזירו את typedef, ו-Point הופך לשם של טיפוס במקום.
עם תגית או בלעדיה
אפשר להשמיט את התגית לגמרי, ולקבל struct אנונימי עם שם typedef בלבד:
typedef struct {
int x;
int y;
} Point; /* אין "struct Point", יש רק "Point" */
זה מסודר יותר, ולרשומת נתונים פשוטה זה הסגנון הנפוץ. אבל זה נכשל ברגע שה-struct צריך להזכיר את עצמו, כי אי אפשר להשתמש בשם ה-typedef עד שההצהרה שלו מסתיימת:
/* לא מתקמפל: "Node" עוד אינו טיפוס בתוך הסוגריים של עצמו. */
typedef struct {
int value;
Node *next;
} Node;
השאירו את התגית וההתייחסות העצמית עובדת, כי אפשר להשתמש ב-struct Node ברגע שהתגית נראתה:
הכלל המעשי: השאירו את התגית. היא עולה מילה אחת, היא תואמת את שם ה-typedef כך ששום דבר לא מבלבל, והיא משאירה פתח להתייחסות עצמית ולהצהרות מוקדמות בקבצי header. בעמוד structs ומצביעים תראו למה הצומת המקושר הזה מתפתח.
הצהרות מוקדמות וטיפוסים אטומים
מכיוון שלמצביע ל-struct יש גודל ידוע גם כשהתוכן של ה-struct לא ידוע, קובץ header יכול למסור טיפוס בלי לחשוף את השדות שלו:
/* 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, ואף אחד מבחוץ לא יכול לגעת בשדות שלו. דפוס הטיפוס האטום (opaque type) הזה הוא הדרך שבה ספריות C אוכפות כימוס, וזה בדיוק מה ש-FILE הוא בספרייה הסטנדרטית: מקבלים FILE * מ-fopen ולא אומרים לכם כלום על מה שבתוכו. ראו טיפול בקבצים.
typedef ל-enums ול-unions
אותו ניב חל על שני הסוגים המורכבים האחרים:
בלי ה-typedef אלה היו enum Color c; ו-union Number n;. ראו את עמוד ה-enums ואת unions כדי להבין מה הם עושים בפועל.
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 מזיק
typedefs יכולים גם להסתיר מידע שהקורא צריך. הדוגמה החדה ביותר היא הסתרה של מצביע:
typedef struct Node *NodeRef; /* הכוכבית נעלמת */
NodeRef n = get_node();
כשמסתכלים על NodeRef n, קורא לא יכול לדעת אם n יכול להיות NULL, אם צריך לשחרר אותו, אם n ועותק שלו חולקים את אותו אובייקט, או אם ניגשים לשדות עם . או עם ->. struct Node *n עונה על כל ארבע השאלות מיד.
הספרייה הסטנדרטית מסכימה: היא עושה typedef ל-struct (FILE) ומשאירה את הכוכבית בנקודת השימוש (FILE *fp). מזה נובעות שתי הנחיות סבירות:
- כן לעשות typedef ל-structs, ל-unions, ל-enums ולמצביעים לפונקציות: הרעש שמוסר לא נושא שום מידע.
- לא לעשות typedef למצביע רק כדי לקצר אותו, ולא לעשות typedef לטיפוס בסיסי כדי להסוות אותו (
typedef int Boolean;מזמין מישהו לשמור בו7. השתמשו במקום זה ב-boolמתוך<stdbool.h>, שמוסבר בערכים בוליאניים).
השמות ברוחב קבוע של הספרייה הסטנדרטית עצמה ב-<stdint.h> (uint32_t, int64_t, size_t) הם כולם typedefs, והם הטיעון הטוב ביותר בעד היכולת הזו: הם אומרים בדיוק מה הם, הם ניידים, ושום דבר לא מוסתר.
שאלות נפוצות
מה typedef עושה ב-C?
הוא נותן לטיפוס קיים שם שני. typedef unsigned long ulong; אומר ש-ulong ו-unsigned long הם מאותו רגע אותו טיפוס. הוא לא יוצר טיפוס חדש ולא אחסון חדש: רק דרך קצרה או תיאורית יותר לאיית טיפוס שכבר יש לכם.
מה זה הניב typedef struct ב-C?
typedef struct Point { int x; int y; } Point; מצהיר על ה-struct ונותן לטיפוס את השם Point בפקודה אחת, כך שאפשר לכתוב Point p; במקום struct Point p;. שמירת התגית (struct Point) חשובה כשה-struct צריך להתייחס לעצמו, כמו צומת של רשימה מקושרת.
מה ההבדל בין typedef לבין #define?
typedef מטופל על ידי הקומפיילר ויוצר כינוי אמיתי לטיפוס, ו-#define הוא החלפת טקסט שה-preprocessor מבצע לפני הקומפילציה. ההבדל הזה נושך עם מצביעים: #define PTR int* גורם ל-PTR a, b; להתרחב ל-int* a, b;, ורק a הוא מצביע. typedef int *PTR; הופך את שניהם למצביעים.
האם כדאי לעשות typedef לטיפוס מצביע ב-C?
בדרך כלל לא. typedef struct Node *NodeRef; מסתיר את העובדה שהטיפוס הוא מצביע, כך שקוראים לא יכולים לדעת אם משתנה יכול להיות NULL, אם צריך לשחרר אותו, או אם צריך ->. ה-FILE * של הספרייה הסטנדרטית משאיר את הכוכבית גלויה בדיוק מהסיבה הזו. עשו typedef ל-struct, לא למצביע אליו.