קוד מקומפל יושב בזיכרון בדיוק כמו נתונים, ולכן לפונקציה יש כתובת. מצביע לפונקציה הוא משתנה ששומר כתובת כזו, וברגע שאפשר לשמור פונקציה, אפשר להעביר אותה כארגומנט, להחזיק טבלה של פונקציות או לבחור ביניהן בזמן ריצה.
זו היכולת שעומדת מאחורי מיון בסדר מותאם, מטפלי אירועים, ממשקי plugins ומכונות מצבים. זה גם החלק בתחביר של C שהכי סביר שיגרום לקורא לעצור ולצמצם עיניים, אז נתחיל משם.
תחביר ההצהרה
קחו פונקציה רגילה:
int add(int a, int b);
כדי להצהיר על מצביע שיכול להחזיק את הכתובת שלה, החליפו את השם ב-(*name):
int (*op)(int, int);
קראו את זה מבפנים החוצה: op הוא מצביע, לפונקציה, שמקבלת (int, int), ומחזירה int.
הסוגריים הם לא קישוט. בלעדיהם, ה-* נקשר לטיפוס ההחזרה:
int (*f)(int); // מצביע לפונקציה שמקבלת int ומחזירה int
int *g(int); // פונקציה שמקבלת int ומחזירה int*, משהו אחר לגמרי
והנה זה עובד מקצה לקצה:
שתי צורות הקריאה עובדות כי שם של פונקציה מומר אוטומטית למצביע. כתבו op(10, 3); הכתיב (*op)(10, 3) הוא שריד מהעבר.
הערה על ה-printf האחרון: %p מצפה למצביע לאובייקט, ומצביעים לפונקציות הם רשמית משפחה נפרדת, כך שהדפסה של אחד כזה אינה ניידת לגמרי. ההמרה משתיקה את הקומפיילרים בפלטפורמות שבהן זה בכלל עובד; רק לעתים רחוקות תצטרכו להדפיס אחד.
typedef הופך את זה לקריא
תחביר ההצהרה מסתבך מהר, ו-typedef הוא התרופה הסטנדרטית:
השוו את int apply(BinaryOp op, int x, int y) לצורה הגולמית, int apply(int (*op)(int, int), int x, int y). אותה משמעות; אחת מהן אפשר לקרוא במבט אחד. ראו את העמוד על typedef לכללים הכלליים.
Callbacks: הקוראת מספקת את ההתנהגות
callback הוא פונקציה שמעבירים לפונקציה אחרת כדי שתוכל לקרוא לכם בחזרה ברגע הנכון. כך קטע קוד אחד מטפל במבנה של משימה, והקוראת מספקת את ההחלטה.
בלי מצביעים לפונקציות הייתם כותבים count_even, count_positive ו-count_big: שלושה עותקים של אותה לולאה. איתם, הלולאה נכתבת פעם אחת והבדיקה היא הפרמטר.
התמורה האמיתית: qsort
qsort מהספרייה הסטנדרטית היא הדוגמה למצביע לפונקציה שבאמת תשתמשו בה. היא ממיינת מערך של כל דבר, והיא מצליחה בזה בלי לדעת כלום על האיברים חוץ מהגודל שלהם ומפונקציית השוואה שאתם מספקים.
void qsort(void *base, size_t nmemb, size_t size,
int (*compar)(const void *, const void *));
פונקציית ההשוואה מקבלת שני const void *, כתובות של שני איברים, ומחזירה מספר שלילי אם הראשון ממוין לפני השני, אפס אם הם שווים, וחיובי אם הוא ממוין אחריו.
שני הרגלים לקחת מפונקציית ההשוואה הזו. לעולם אל תכתבו return x - y;: זה נראה מתוחכם, אבל גולש עבור ערכים גדולים או שליליים, מפיק סימן שגוי ומערך ממוין לא נכון. בצעו המרה לפני ה-dereference, לא אחריו: *(const int *)a, כי ל-*a על void * אין מה לקרוא.
אותו מנגנון ממיין structs לפי כל שדה שתרצו:
הביטוי (q->score > p->score) - (q->score < p->score) מחזיר בדיוק -1, 0 או 1, בלי חיסור ובלי גלישה. ול-strcmp כבר יש את מוסכמת ההחזרה הנכונה, כך שפונקציית השוואה למחרוזות היא שורה אחת.
טבלאות של מצביעים לפונקציות
מערך של מצביעים לפונקציות הופך שרשרת של if/else לחיפוש בטבלה. כך מפרשים (interpreters) מנתבים opcodes וכך מערכות תפריטים מריצות פקודות.
הוספת פעולה היא עכשיו הוספת שורה, לא עריכה של switch. שלבו את זה עם structs ותקבלו את הצורה הסטנדרטית של plugin ב-C: struct של מצביעים לפונקציות הוא מה ש"ממשק" אומר בשפה הזו.
דברים שנושכים
החתימות חייבות להתאים בדיוק. הטיפוס של מצביע לפונקציה כולל את טיפוסי הפרמטרים ואת טיפוס ההחזרה. השמה של פונקציה שלא מתאימה היא הפרת אילוץ, וקריאה דרך מצביע מטיפוס שגוי היא התנהגות לא מוגדרת גם כשזה "עובד". אל תמירו את החתימה של פונקציית השוואה כדי להשתיק אזהרה: תקנו את פונקציית ההשוואה.
מצביע null לפונקציה הוא עדיין מצביע null. אתחלו ל-NULL ובדקו לפני קריאה, בדיוק כמו עם מצביעים לנתונים:
if (handler != NULL) {
handler(event);
}
ראו מצביעי null כדי להבין למה הבדיקה הזו שווה את המקום שלה.
מצביעי void מאבדים את בטיחות הטיפוסים, ואתם מערכת הטיפוסים. בתוך פונקציית השוואה של qsort, שום דבר לא מונע מכם להמיר לטיפוס הלא נכון. אם תמיינו מערך של double עם compare_ints, זה יתקמפל בלי בעיה ויפיק שטויות.
תחביר ההצהרה מקונן. פונקציה שמחזירה מצביע לפונקציה היא int (*get_op(char c))(int, int);. כשאתם מוצאים את עצמכם כותבים את זה, פנו ל-typedef:
typedef int (*BinaryOp)(int, int);
BinaryOp get_op(char c); // אותו דבר, בצורה קריאה
שאלות נפוצות
איך מצהירים על מצביע לפונקציה ב-C?
כתבו את החתימה של הפונקציה עם (*name) במקום שבו היה שם הפונקציה: int (*op)(int, int); מצהיר על op כמצביע לפונקציה שמקבלת שני int ומחזירה int. הסוגריים סביב *op הכרחיים: בלעדיהם מצהירים על פונקציה שמחזירה מצביע.
איך קוראים לפונקציה דרך מצביע?
גם op(3, 4) וגם (*op)(3, 4): שתי הצורות חוקיות ועושות אותו דבר, כי שם של פונקציה מומר אוטומטית למצביע. קוד C מודרני משתמש בצורה הפשוטה op(3, 4).
מה זה callback ב-C?
פונקציה שמעבירים לפונקציה אחרת כדי שתוכל לקרוא לכם בחזרה. qsort היא הדוגמה הקלאסית: מעבירים לה פונקציית השוואה, ו-qsort קוראת לה בכל פעם שהיא צריכה לדעת איזה משני איברים בא קודם. זה מה שמאפשר לשגרת מיון אחת לעבוד על כל טיפוס.
האם צריך & כדי לקחת את הכתובת של פונקציה?
לא. שם של פונקציה כבר מומר למצביע לאותה פונקציה, כך ש-op = add; ו-op = &add; שקולים. רוב קוד ה-C משמיט את ה-&.