Smart-World Surf

יחידה 9: ניהול זיכרון מתקדם

הבנה ושליטה בניהול זיכרון דינמי.
מצביעים והפניותהקצאה ושחרור זיכרון (new/delete)מצביעים חכמים (Smart Pointers)בעיות זיכרון נפוצות (דליפותמצביעים תלויים)

ברוכים הבאים ליחידת הלימוד "ניהול זיכרון מתקדם" בקורס "תכנות מונחה עצמים". יחידה זו חיונית להבנה מעמיקה של אופן פעולת תוכניות C++ והשפעתה על ביצועים ויציבות. שליטה בניהול זיכרון דינמי היא מיומנות קריטית לכל מתכנת, ובמיוחד ב-C++ שבה הזיכרון מנוהל באופן ידני או באמצעות כלים מודרניים כמו מצביעים חכמים. נצלול לעקרונות, לכלים ולבעיות הנפוצות, תוך התמקדות בהיבטים הרלוונטיים לבחינה בטכניון.

יסודות ניהול זיכרון דינמי

ב-C++, אנו עובדים עם שני אזורי זיכרון עיקריים: ה-Stack (מחסנית) וה-Heap (ערימה). בעוד שה-Stack מנוהל אוטומטית עבור משתנים מקומיים, ה-Heap מאפשר הקצאת זיכרון דינמית, כלומר, בזמן ריצה, ודורש ניהול ידני.

מצביעים והפניות (Pointers and References)

מצביעים והפניות הם כלים בסיסיים לעבודה עם זיכרון ב-C++. הם מאפשרים לנו לגשת לנתונים במיקומים ספציפיים בזיכרון.

מצביע (Pointer): משתנה המכיל כתובת זיכרון של משתנה אחר. ניתן לשנות את כתובת הזיכרון אליה הוא מצביע, והוא יכול להצביע על nullptr.
הפניה (Reference): כינוי (alias) למשתנה קיים. חייבת להיות מאותחלת בעת ההצהרה ולא ניתנת לשינוי כדי שתצביע על אובייקט אחר. אינה יכולה להיות nullptr.

מצביעים

יכולים להצביע על nullptr. ניתן לשנות את האובייקט שהם מצביעים עליו. דורשים Dereferencing (באמצעות * או ->) כדי לגשת לערך. מאפשרים אריתמטיקת מצביעים.

הפניות

חייבות תמיד להצביע על אובייקט חוקי (לא יכולות להיות nullptr). לא ניתן לשנות את האובייקט שהן מפנות אליו לאחר אתחול. גישה לערך מתבצעת ישירות (כמו למשתנה רגיל). לא מאפשרות אריתמטיקת מצביעים.

הקצאה ושחרור זיכרון (new/delete)

כדי להקצות זיכרון ב-Heap, אנו משתמשים באופרטור new. שחרור הזיכרון מתבצע באמצעות delete.

new: אופרטור המקצה זיכרון ב-Heap עבור אובייקט או מערך ומחזיר מצביע לזיכרון שהוקצה.
delete: אופרטור המשחרר זיכרון שהוקצה באמצעות new. חובה להשתמש ב-delete[] עבור זיכרון שהוקצה באמצעות new[]. אי-התאמה תוביל להתנהגות בלתי מוגדרת.

בעיות זיכרון נפוצות

ניהול זיכרון ידני טומן בחובו סיכונים רבים העלולים להוביל לקריסות, התנהגות בלתי צפויה ופגיעה בביצועים.

דליפת זיכרון (Memory Leak): מצב שבו זיכרון שהוקצה דינמית אינו משוחרר לעולם, גם לאחר שאין בו שימוש. לאורך זמן, הדבר עלול להוביל לניצול יתר של זיכרון המערכת ולקריסת התוכנית.
מצביע תלוי (Dangling Pointer): מצביע המכיל כתובת זיכרון שכבר שוחררה או שהאובייקט אליו הצביע אינו קיים עוד. ניסיון לגשת לזיכרון דרך מצביע תלוי מוביל להתנהגות בלתי מוגדרת (Undefined Behavior).
שחרור כפול (Double Free): ניסיון לשחרר פעמיים את אותו בלוק זיכרון. מוביל להתנהגות בלתי מוגדרת ולרוב לקריסת התוכנית.
דליפות זיכרון ומצביעים תלויים: אלו הן שתי הבעיות הנפוצות והמסוכנות ביותר בניהול זיכרון ידני ב-C++. בבחינות בטכניון, מצופה מכם לזהות קוד המכיל בעיות אלו, להסביר את הסיבה להן, ולהציע פתרונות (לרוב באמצעות מצביעים חכמים או יישום נכון של RAII). הבנה מעמיקה של מתי וכיצד בעיות אלו מתרחשות היא קריטית.

מצביעים חכמים (Smart Pointers)

מצביעים חכמים הם עטיפות (wrappers) למצביעים גולמיים (raw pointers) המספקות ניהול זיכרון אוטומטי באמצעות עקרון RAII (Resource Acquisition Is Initialization). הם מבטיחים שזיכרון ישוחרר אוטומטית כאשר האובייקט המצביע החכם יוצא מהסקופ.

מצביע חכם (Smart Pointer): אובייקט המנהל משאב דינמי (לרוב זיכרון) ומבטיח את שחרורו האוטומטי כאשר המצביע החכם עצמו נהרס.

std::unique_ptr

בעלות בלעדית על המשאב. לא ניתן להעתיק, רק להעביר (move). מבטיח שרק מצביע אחד מנהל את המשאב בכל רגע נתון. אידיאלי כאשר ברור מי הבעלים של המשאב.

std::shared_ptr

בעלות משותפת על המשאב. משתמש במונה התייחסויות (reference count) כדי לעקוב אחר מספר המצביעים המפנים לאותו משאב. המשאב משוחרר כאשר מונה ההתייחסויות מגיע לאפס. ניתן להעתיק.

std::weak_ptr

מצביע חלש ללא בעלות. משמש בדרך כלל עם std::shared_ptr כדי למנוע רפרנסים מעגליים (circular references) שעלולים לגרום לדליפות זיכרון ב-shared_ptr. לא מגדיל את מונה ההתייחסויות.

שאלות לדיון

  • הסבר את ההבדל בין הקצאת זיכרון ב-Stack לבין הקצאה ב-Heap, ומתי נבחר להשתמש בכל אחת מהן.
  • נתון קטע קוד המכיל מצביע גולמי. זהה דליפת זיכרון או מצביע תלוי, הסבר מדוע הבעיה מתרחשת, והצע פתרון באמצעות מצביע חכם מתאים.
  • כיצד std::shared_ptr פותר את בעיית דליפות הזיכרון במקרים של בעלות משותפת? באילו מקרים הוא עלול ליצור דליפת זיכרון בעצמו וכיצד std::weak_ptr מסייע בכך?
  • הסבר את עקרון RAII וכיצד הוא מיושם על ידי מצביעים חכמים לניהול זיכרון בטוח יותר.

נקודות לתשובת מודל

עבור שאלה העוסקת בזיהוי ותיקון בעיות זיכרון בקוד:

  • זיהוי הבעיה: ציין במפורש אם מדובר בדליפת זיכרון, מצביע תלוי, שחרור כפול וכו'.
  • הסבר הסיבה: תאר את שרשרת האירועים המובילה לבעיה (לדוגמה: "הזיכרון הוקצה באמצעות new אך לא שוחרר לפני יציאה מהפונקציה/סקופ").
  • השפעה: הסבר את ההשלכות של הבעיה (לדוגמה: "הדבר יוביל לניצול יתר של זיכרון המערכת לאורך זמן").
  • פתרון: הצג קוד מתוקן או תאר את השינויים הנדרשים.
  • הצדקת הפתרון: הסבר מדוע הפתרון המוצע (לרוב שימוש ב-std::unique_ptr או std::shared_ptr) אכן פותר את הבעיה, תוך התייחסות לעקרונות כמו RAII או מנגנון מונה ההתייחסויות.
  • בחירת מצביע חכם: נמק את הבחירה במצביע חכם ספציפי (unique_ptr לבעלות בלעדית, shared_ptr לבעלות משותפת, weak_ptr למניעת מעגלים).
מצאתם טעות או שחסר משהו?
→ הקודמת
טיפול בחריגות
הבאה ←
תבניות עיצוב (Design Patterns)