בכל שפת תכנות יש פה ושם "מוזרויות", תחביר חסר היגיון שאין ברירה אלא ללמוד בעל-פה. כך, למשל, ההגדרה בשפת C של מצביע לפונקציה – טריק מתקדם יחסית שמאפשר לנו לבנות קוד גמיש מאוד, לשלוח פונקציות כפרמטרים (callback) ועוד. הנה הסבר והדגמה שיעזרו לנו להבין כיצד עושים זאת.
מצביע פשוט (תזכורת)
בתוכנית הבאה, i הוא משתנה רגיל ו-p הוא מצביע שמקבל (בשורה 9) את הכתובת של i. המספרים בפלט של התוכנית יהיו "2" ואז "3". אפרופו, כל התוכניות בפוסט זה הן למחשב, לא למיקרו-בקר, ומכאן השימוש החופשי ב-printf.
בכל הגדרה של מצביע יש שלושה חלקים: משמאל – הטיפוס שאליו מצביעים והמאפיינים שלו. באמצע – כוכבית שאומרת שזה מצביע, ומימין – השם של המצביע והמאפיינים שלו. הכוונה במאפיינים היא מה שנקרא רשמית "Type qualifiers" – כל המגבלות האופציונליות ששפת C מאפשרת לנו לקבוע עבור משתנים. למשל, בשורה הבאה:
המצביע, ששמו p, מצביע לטיפוס int קבוע (כי מילת המפתח const היא משמאל לכוכבית!), אבל הוא עצמו לא קבוע אלא volatile. אם תכתבו את השורה הזו במקום שורה 6 בקוד למעלה, הקומפיילר יודיע על שגיאה בשורה 10, כי מתבצע שם שינוי ערך של המוצבע, והרי אסור לשנות const.
מצביע לפונקציה
פונקציה היא יצור מורכב יותר מאשר משתנה רגיל: יש לה טיפוס ערך חוזר (או void), ורשימת פרמטרים (או void). איך כוללים את כל זה בתוך הגדרת מצביע? התשובה היא, כאמור, תחביר שרירותי שחייבים לשנן. בגדול הוא נראה כמו הגדרת פונקציה, אבל עם כוכבית לפני השם שלה, וסוגריים סביבם. כמו כן, לא חייבים לכתוב את שמות הפרמטרים, אלא רק את הטיפוס שלהם. הנה תוכנית לדוגמה, עם מצביע-לפונקציה שנקרא funcPtr (מוגדר בשורה 9). המצביע הזה מקבל (בשורה 11) את הכתובת של הפונקציה helloPrint וקורא לה עם הערך 123 (שורה 12). לכן, הפלט של התוכנית יהיה "Hello 123".
פרמטר שהוא מצביע לפונקציה
אפשר להשתמש בצורת ההגדרה המגושמת הזו גם ברשימת פרמטרים של פונקציה רגילה. בקוד הבא, הפונקציה callbackUser מקבלת פרמטר בשם callback, שהוא בעצמו מצביע לפונקציה. שימו לב שצורת ההגדרה שלו (בשורה 7) זהה לזו של funcPtr בקוד למעלה. גם כאן הפלט יהיה "Hello 123".
אבל זה מסורבל – דמיינו הגדרה של פרמטר, שהוא מצביע לפונקציה שמקבלת שלושה או ארבעה פרמטרים שונים! כדי ליצור קוד נקי יותר ונוח יותר לקריאה ולשינויים, עדיף ליצור טיפוס של מצביע לפונקציה הזו. יצירת טיפוסים נעשית באמצעות typedef. הנה תוכנית דומה, רק עם typedef למצביע-לפונקציה (מוגדר בשורה 3, ונעשה בו שימוש בשורה 9):
מערך של מצביעים לפונקציות
מרגע שהגדרתי טיפוס, לא משנה של מה, אני יכול כמובן ליצור מערך של משתנים מטיפוס זה. לדוגמה,
הגדרה זו יוצרת מערך בגודל 2, של מצביעים לפונקציות שמקבלות פרמטר const int ולא מחזירות כלום.
אבל אפשר גם להגדיר את המערך עצמו ב-typedef, וכך ליצור קבוצת מצביעים שלמה ולהעביר אותה, לפי הצורך, בתור פרמטר. איפה לדעתכם צריך לשים בהגדרה את הסוגריים המרובעים שיוצרים מערך? התשובה, שאפשר להתווכח לגבי ההיגיון שבה, היא מיד אחרי שם המערך. הנה תוכנית הדגמה שעושה את זה. שורה 3 מגדירה טיפוס של מערך של מצביעים לפונקציות, ובשורה 4 מוגדר משתנה אחד מטיפוס זה. בשורות 22 ו-23 המערך מאוכלס בהפניות לפונקציות שונות, ושורה 24 שולחת אותו כפרמטר לפונקציה "callbackUser", שעוברת על כולו ומריצה את הפונקציות שבו בזו אחר זו.
לא צריך להוסיף & בקריאה?
כל הדוגמאות בפוסט נבדקו בשטח, אז התשובה הפשוטה היא "כנראה שלא" 😀 אבל קצת יותר לעומק, בשפת C הפונקציה עצמה נחשבת מין פוינטר. כמו שאפשר לשלוח פוינטר למערך על ידי אזכור השם שלו, במקום הכתובת-של-התא-הראשון-בו.
הפלט של התוכנית בסעיף "מצביע לפונקציה" יהיה Hello 123,
ולא 123.
למעשה טעיתי באותו אופן בעוד כמה סעיפים 🙂 התכוונתי למספרים אבל ברור שהייתי צריך לנסח אחרת. תוקן, תודה!