למה לבקש זיכרון בזמן ריצה
עד עכשיו כל משתנה שיצרתם חי על ה-stack: הגודל שלו ידוע בזמן הידור, והוא מושמד אוטומטית כשהטווח שלו נגמר. זה מהיר ובטוח, אבל זה לא מתמודד עם מצב שבו לא יודעים כמה זיכרון צריך עד שהתוכנית רצה: באפר שהגודל שלו נקבע לפי קלט מהמשתמש, מבנה שצריך לחיות יותר מהפונקציה שיצרה אותו, או גרף שהצורה שלו לא ידועה מראש.
למקרים האלה C++ מאפשרת להקצות מה-heap (שנקרא גם free store) עם new, ולהחזיר אותו עם delete. הביטוי new שומר בלוק, מריץ את הבנאי ומחזיר מצביע אליו, בהמשך ישיר למצביעים שראיתם בעמוד הקודם.
המשתנה p עצמו חי על ה-stack: הוא רק מצביע. ה-int שהוא מצביע עליו חי ב-heap ונשאר בחיים עד שמבצעים עליו delete, לא משנה כמה טווחים באים והולכים.
stack מול heap
ההבחנה הזו היא כל הסיבה לקיומו של new, ולכן כדאי להפוך אותה למוחשית.
void demo() {
int a = 10; // על ה-stack: נעלם כש-demo() מחזירה
int* b = new int(10); // 'b' על ה-stack, ה-int שהוא מצביע עליו ב-heap
} // 'a' מושמד; ה-int שב-heap דולף, אף פעם לא נמחק
ההבדלים המרכזיים:
- Stack: אורך חיים אוטומטי, מהיר מאוד, גודל מוגבל (בדרך כלל כמה MB), משוחרר בשבילכם כשהטווח נגמר.
- Heap: אורך חיים ידני, קצת יותר איטי, גדול, ומשוחרר רק כשקוראים ל-
delete.
העסקה היא גמישות תמורת אחריות: זיכרון ב-heap חי בדיוק כמה שרוצים, אבל אתם אלה שחייבים לזכור לשחרר אותו.
הקצאת מערכים עם new[]
כשצריך בלוק שהאורך שלו נקבע בזמן ריצה, השתמשו בצורת המערך new T[n]. היא מחזירה מצביע לאיבר הראשון, ומשחררים אותה עם ה-delete[] התואם.
הכלל קפדני וקל לטעות בו: זיכרון מ-new משחררים עם delete, וזיכרון מ-new[] משחררים עם delete[]. ערבוב ביניהם, delete arr על משהו שהוקצה עם new[], הוא undefined behavior, גם אם נראה שזה עובד על המחשב שלכם.
שלושת הבאגים הקלאסיים
לניהול זיכרון ידני יש קבוצה קטנה של טעויות שאחראיות לרוב הבאגים ב-heap. למדו לזהות את שלושתן.
1. דליפת זיכרון: אף פעם לא קוראים ל-delete. הבלוק נשאר שמור לנצח. לא מזיק פעם אחת, קטלני בלולאה.
void leaky() {
int* p = new int(5);
// ... אין delete ...
} // p נעלם; ה-int שב-heap עכשיו גם בלתי נגיש וגם לא משוחרר
2. מצביע תלוי (dangling pointer): משתמשים בזיכרון אחרי ששחררו אותו. המצביע עדיין מחזיק את הכתובת הישנה, אבל הזיכרון הזה כבר לא שלכם.
3. Double-free: מבצעים delete על אותו בלוק פעמיים. זה משחית את הניהול הפנימי של ה-heap ובדרך כלל קורס.
int* p = new int(1);
delete p;
delete p; // double-free: undefined behavior, לעיתים קרובות קריסה
קביעת מצביע ל-nullptr אחרי המחיקה מנטרלת גם שימוש במצביע תלוי וגם double-free: dereference של nullptr קורס מיד (קל לדבג), ו-delete nullptr מוגדר במפורש כפעולה בטוחה שלא עושה כלום.
מחזור ריאליסטי של הקצאה, שימוש ושחרור
כשמחברים הכול, זו הצורה של ניהול ידני נכון: מקצים, משתמשים, משחררים בדיוק פעם אחת, ולא נוגעים במצביע אחר כך.
שימו לב ש-delete u עושה שני דברים עבור טיפוס של מחלקה: קודם הוא מריץ את ה-destructor של האובייקט, ואז משחרר את הזיכרון הגולמי. הסדר הזה חשוב ברגע שלאובייקטים שלכם יש משאבים משלהם.
מלכודת עדינה: אם נזרקת חריגה בין new ל-delete, ה-delete אף פעם לא רץ ויש דליפה. לעטוף כל הקצאה ב-try/catch כדי לטפל בזה זה מייגע ומועד לטעויות, וזו בדיוק הבעיה שהעמוד הבא פותר.
הבא בתור: Smart pointers
ראיתם עכשיו את המחיר המלא של ניהול זיכרון ידני: כל new הוא הבטחה לבצע delete בהמשך, ושחרור אחד שנשכח, שהוכפל או שקרה מוקדם מדי הוא undefined behavior. C++ מודרנית כמעט אף פעם לא נותנת את ההבטחה הזו ידנית. העמוד הבא מציג smart pointers, std::unique_ptr ו-std::shared_ptr: אובייקטים שמחזיקים הקצאה ב-heap וקוראים ל-delete בשבילכם אוטומטית כשהם יוצאים מהטווח, כך ששלושת הבאגים הקלאסיים הופכים לדברים שהמהדר ו-RAII מטפלים בהם בשבילכם.
שאלות נפוצות
מה ההבדל בין new ל-delete ב-C++?
new מקצה זיכרון ב-heap בזמן ריצה ומחזיר מצביע אליו; delete משחרר זיכרון שהוקצה עם new. לכל new חייב להתאים בדיוק delete אחד, אחרת יש דליפת זיכרון. למערכים, השתמשו ב-new[] עם delete[].
מה קורה אם שוכחים לקרוא ל-delete ב-C++?
מקבלים דליפת זיכרון: הבלוק ב-heap נשאר שמור לאורך כל חיי התוכנית, למרות ששום דבר כבר לא מצביע עליו. דליפה אחת היא בדרך כלל לא מזיקה, אבל דליפות בלולאה או בשירות שרץ לאורך זמן גדלות עד שנגמר לתוכנית הזיכרון והיא קורסת.
האם כדאי להשתמש ב-new ו-delete ישירות ב-C++ מודרנית?
לעיתים רחוקות. העדיפו מכלים כמו std::vector או smart pointers (std::unique_ptr, std::shared_ptr) שמשחררים זיכרון אוטומטית. כדאי להבין new/delete גולמיים כי smart pointers עוטפים אותם, אבל בקוד יומיומי הם מקור לדליפות ולמצביעים תלויים.