Menu

מצביע ל-Struct ב-C: אופרטור החץ ו-structs ב-Heap

איך מצביעים על struct ב-C: אופרטור החץ, למה (*p).x צריך את הסוגריים שלו, העברת structs במצביע כדי לשנות אותם או לחסוך העתקות, הקצאת struct עם malloc ובניית צומת של רשימה מקושרת.

בדף הזה יש עורכים שאפשר להריץ - לערוך, להריץ ולראות את הפלט מיד.

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?

p->x ניגש לשדה x דרך המצביע p. זה שקול בדיוק ל-(*p).x: מבצעים dereference למצביע, ואז לוקחים את השדה. החץ קיים כי מצביעים ל-structs נפוצים כל כך, שהצורה הארוכה מעמיסה על הקוד.

למה *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) בסיום.

איור של שפות התכנות ב-Coddy

ללמוד תכנות עם Coddy

להתחיל