struct שמועבר לפונקציה מועתק. זה בסדר עבור נקודה של שני int, ובזבזני עבור רשומה של 200 בתים. וזה גם הופך שינוי לבלתי אפשרי, כי הפונקציה רואה רק את העותק שלה. הצבעה על struct פותרת את שתי הבעיות בבת אחת, והיא גם הדרך היחידה לבנות מבנים שגדלים: רשימות, עצים, גרפים.
לקחת מצביע ל-struct
לא צריך שום דבר מיוחד. & נותן את הכתובת, והטיפוס הוא "מצביע ל-struct":
שלושתם מדפיסים 3. שני האחרונים הם אותה פעולה שנכתבה בשתי דרכים.
למה (*ptr).x צריך את הסוגריים שלו
אולי תנסו להוריד אותם ולכתוב *ptr.x. זה מתקמפל למשהו שונה לגמרי, או לעיתים קרובות יותר לא מתקמפל בכלל, כי לאופרטור השדה . יש קדימות גבוהה יותר מאשר ל-dereference *:
*ptr.x // מתפרש כ-*(ptr.x): שגוי
(*ptr).x // קודם dereference, ואז לוקחים את השדה: נכון
ptr->x // אותו דבר, בקיצור
ptr.x מבקש שדה של המצביע, ולמצביע אין שדות. התלונה של הקומפיילר ("request for member 'x' in something not a structure or union") היא באג של קדימות בתחפושת.
מכיוון שהצורה עם הסוגריים מכוערת וקל לטעות בה, C מספקת את ->. בפועל כמעט אף פעם לא תראו (*p).x בקוד אמיתי: השתמשו ב-p->x ושכחו שהצורה הארוכה קיימת, חוץ מההסבר למה החץ אומר.
העברת struct במצביע
פרמטר מצביע נותן לפונקציה את ה-struct האמיתי של הקורא, ולכן השינויים נשמרים:
שני הרגלים ששווה לפתח כאן:
- סמנו מצביעים לקריאה בלבד כ-
const.const struct Point *pמבטיח שהפונקציה לא תשנה את מה ש-pמצביע עליו. אם מישהו יוסיף בהמשךp->x = 0בפנים, הקומפילציה תיכשל במקום שהבאג יגיע לפרודקשן. זה גם מתעד את הכוונה בנקודת הקריאה. - העבירו מצביע ל-structs גדולים גם כשרק קוראים. struct עם buffer של 64 בתים לשם עולה 64 בתים של העתקה בכל קריאה by value, ומצביע עולה 8.
עדיין בסדר להעביר structs קטנים by value: struct Point בגודל 8 בתים מועתק בזול כמו מצביע, והגרסה by value לא יכולה להיות NULL, מה שמעלים סוג שלם של כישלונות.
מצביעים לתוך מערכים של structs
אריתמטיקה של מצביעים עובדת על מערכי struct בדיוק כמו על מערכי int: p + 1 מתקדם ב-struct שלם אחד, כולל ריפוד.
שם של מערך מתדרדר למצביע לאיבר הראשון שלו, כך ש-staff כבר הוא struct Employee * בנקודת הקריאה. זו גם הסיבה שהפונקציה צריכה לקבל את n בנפרד: sizeof בתוך הפונקציה היה מודד את המצביע, לא את המערך.
הקצאת struct ב-heap
structs ב-stack מתים בסוף ה-scope שלהם. כדי ליצור struct שחי יותר מהפונקציה שיצרה אותו, או כדי ליצור כמה שהקלט דורש, הקצו עם malloc:
ארבעה פרטים בפונקציה הקטנה הזו נושאים משקל:
sizeof *e, ולאsizeof(struct Employee). זה נקרא "הגודל של מה ש-eמצביע עליו", כך שאם הטיפוס ישתנה אי פעם, ההקצאה תעקוב אחריו אוטומטית. אין דרך שהשניים יתרחקו זה מזה.- בדקו
NULL.mallocמחזירהNULLכשהיא לא יכולה לספק את הבקשה. כתיבה ל-e->idדרך מצביע null היא segmentation fault. - אתחלו כל שדה.
mallocלא מאפסת זיכרון, וה-struct מגיע מלא בזבל. (calloc(1, sizeof *e)מאפסת אותו בשבילכם.) - מישהו חייב לעשות לו
free. החזרת מצביע שהוקצה מעבירה את החובה הזו לקורא. כתבו את זה בהערה: כלל בעלות לא ברור הוא האופן שבו דליפות זיכרון מתחילות.
השכר: צומת של רשימה מקושרת
הנה הדבר ש-structs לא יכולים לעשות בלי מצביעים. struct לא יכול להכיל את עצמו, כי הוא היה גדול לאין סוף, אבל הוא יכול להכיל מצביע ל-struct מאותו סוג, והטריק היחיד הזה בונה כל מבנה נתונים מקושר ב-C.
שימו לב ש-struct Node *next; מתייחס ל-struct Node בזמן ש-struct Node עדיין מוצהר. זה חוקי בדיוק כי למצביע יש גודל ידוע, לא משנה על מה הוא מצביע, והקומפיילר עוד לא צריך את ההגדרה המלאה. זה גם המקום היחיד שבו אתם חייבים להשתמש בתגית struct Node גם אם כתבתם typedef: שם ה-typedef עוד לא קיים בתוך ההגדרה של עצמו.
הלולאה של free_list שומרת את head->next לפני הקריאה ל-free(head). קריאה של head->next אחרי השחרור היא use-after-free: הזיכרון כבר לא שלכם, והערך שתקראו הוא מה שהמקצה שם שם.
טעויות נפוצות
p->xעל מצביע לא מאותחל אוNULL. הגורם הנפוץ ביותר לקריסה בקוד עמוס ב-structs. אתחלו מצביעים ל-NULLובדקו לפני dereference.- החזרת מצביע ל-struct מקומי.
struct Point *bad(void) { struct Point p = {1,2}; return &p; }מחזירה כתובת של זיכרון שמפסיק להתקיים ברגע שהפונקציה חוזרת. החזירו את ה-struct by value, או הקצו אותו. - שחרור פעמיים, או שכחה לשחרר. כל
mallocמתאים ל-freeאחד בדיוק. אחרי השחרור, השימוNULLבמצביע, כדי ששימוש מקרי מאוחר יותר יקרוס ברעש במקום להשחית זיכרון בשקט. - העתקה של struct שמכיל מצביע. השמה של struct מעתיקה את ערך המצביע, לא את מה שהוא מצביע עליו. שני ה-structs מתייחסים עכשיו לאותו buffer, ומי שמשחרר אותו ראשון משאיר את השני תלוי.
שאלות נפוצות
מה עושה האופרטור -> ב-C?
-> ב-C?p->x ניגש לשדה x דרך המצביע p. זה שקול בדיוק ל-(*p).x: מבצעים dereference למצביע, ואז לוקחים את השדה. החץ קיים כי מצביעים ל-structs נפוצים כל כך, שהצורה הארוכה מעמיסה על הקוד.
למה *p.x לא עובד עם מצביע ל-struct?
*p.x לא עובד עם מצביע ל-struct?כי . נקשר חזק יותר מ-*. *p.x מתפרש כ-*(p.x): לקחת את השדה x של p (שהוא מצביע, לא struct) ולבצע לו dereference. צריך (*p).x, או פשוט לכתוב p->x.
האם להעביר struct by value או במצביע ב-C?
העבירו במצביע כשהפונקציה חייבת לשנות את ה-struct של הקורא, או כשה-struct גדול והעתקה שלו תהיה בזבזנית. העבירו by value עבור structs קטנים לקריאה בלבד, שבהם ההעתקה זולה והקוד פשוט יותר. סמנו פרמטרים של מצביע לקריאה בלבד כ-const struct T *, כדי שהקומפיילר יאכוף את ההבטחה.
איך מקצים struct עם malloc ב-C?
struct Node *n = malloc(sizeof *n);: הצורה sizeof *n מבקשת את הגודל של מה ש-n מצביע עליו, ולכן היא נשארת נכונה גם אם תשנו את הטיפוס אחר כך. בדקו את התוצאה מול NULL, אתחלו כל שדה, ועשו free(n) בסיום.