בכל שפת תכנות יש פה ושם "מוזרויות", תחביר חסר היגיון שאין ברירה אלא ללמוד בעל-פה. כך, למשל, ההגדרה בשפת 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.
סתם כדוגמא שימושית אולי – לאחרונה השתמשתי בזה כדי להעביר לספריה של מסך hd44780 את הפונקציות שעושות את הכתיבה, כדי לבחור דינאמית אם זה ממשק 4 או 8 ביט – לא משנה כרגע למה שאני ארצה להחליף ביניהם דינאמית.
זה גם לא מוסיף תקורה גדולה בדחיסות הקוד, למי שזה מפריע לו פה.
בדקתי עכשיו כדי לוודא – אכן השתמשתי במצביעים לפונקציות, בשפת פסקל, כבר בפרויקט הגמר שלי בתיכון, אי-שם לקראת סוף המילניום הקודם. לא זוכר מתי נתקלתי לראשונה ברעיון הזה, הוא באמת מגניב וקצת ממכר 🙂
גיליתי את הבלוג שלך דרך איזה פוסט שלך בפייסבוק לגבי מיקרוסקופים והאמת די התרשמתי. התחלתי ללמוד לתכנת בשפת R בשנת 2019 ולאט לאט התקדמתי בסולם השפות ל ++c כדי לתכנת ארדואינו לפרוייקטים קטנים על מטריצה. בשלב מסויים הבנתי שלתכנת בקרי AVR בשפת c כמעט תמיד לא יאפשר לנצל את הפוטנציאל האמיתי שלהם אז למדתי לכתוב אסמבלי ואז באמת התחלתי לעשות פרוייקטים רציניים ודחוסים בצורה מטורפת. הבעיה עם שפת c זה השימוש המופרז ב stack ומלא boilerplate שמבזבז לך משאבים ב flash של הבקר (שבבקרים הכי זולים זה 1kb בערך) וזה נכפה עליך גם כשזה כלל לא נדרש (כן הגיוני אולי… לקרוא עוד »
לא נראה לי שיש יותר מדי boilerplate בשפת C, ואפשר לכתוב תוכניות C נהדרות גם ל-ATtiny202 למשל (2K פלאש ו-128 בייטים SRAM) – אני יודע כי עשיתי את זה. הקומפיילרים היום יותר חכמים ממה שאולי נדמה לך.
ברור שאסמבלי נותן את השליטה האולטימטיבית על המיקרו-בקר, אבל בימינו זה פשוט שיקול זניח לעומת גורמים אחרים בפיתוח אמבדד, כמו פורטביליות, תחזוקה וגם זמן. מי שנהנה מזה כתחביב, נהדר – זה פשוט פחות מעשי.
לא צריך להוסיף & בקריאה?
כל הדוגמאות בפוסט נבדקו בשטח, אז התשובה הפשוטה היא "כנראה שלא" 😀 אבל קצת יותר לעומק, בשפת C הפונקציה עצמה נחשבת מין פוינטר. כמו שאפשר לשלוח פוינטר למערך על ידי אזכור השם שלו, במקום הכתובת-של-התא-הראשון-בו.
הפלט של התוכנית בסעיף "מצביע לפונקציה" יהיה Hello 123,
ולא 123.
למעשה טעיתי באותו אופן בעוד כמה סעיפים 🙂 התכוונתי למספרים אבל ברור שהייתי צריך לנסח אחרת. תוקן, תודה!