לדלג לתוכן

C++/זיכרון דינמי

מתוך ויקיספר, אוסף הספרים והמדריכים החופשי
< C++

מהי הקצאת זיכרון דינמית

[עריכה]
הקצאת זיכרון דינמית
היא פעולה של הקצאת זיכרון בזמן ריצת התוכנה. פעולה זו מבוצעת בדרך כלל כאשר גודל הזיכרון הנדרש אינו ידוע מראש לפני הרצת התוכנה ויתברר רק לאחר קבלת קלט מהמשתמש, קריאת תוכן של קובץ כלשהו וכדומה.

הקצאה ושחרור זיכרון ב־C++‎

[עריכה]

מאחר ובשפת C++‎ יש תמיכה מלאה בכל הספריות הסטנדרטיות של C, ניתן להשתמש בפונקציות של C כגון malloc ו-free, אך הדבר אינו מומלץ, כיוון שב־C++‎ יש כלים מיוחדים למטרה זו התומכים במחלקות (כלומר מריצות את הבנאים ומפרקים, למד בהמשך).

ב־C++‎ מקצים ומשחררים זיכרון באופן דינמי באמצעות האופרטורים new ו-delete עבור משתנים בודדים ו-[]new ו-delete[] עבור מערכים.

משתנה בודד

[עריכה]

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

int *ptr = new int;
cin >> *ptr;
/*...*/
delete ptr;

כך נקצה משתנה מטיפוס int בזיכרון הדינמי ונשמור את המצביע אליו במשתנה ptr. לאחר השימוש בזיכרון זה נשחרר אותו ע"י האופרטור delete.

מערך

[עריכה]

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

int length;
cin >> length;

int *array = new int[length];
for(int i = 0; i < length; i++)
    cin >> array[i];
/*...*/
delete[] array;

שימו לב שבזכות ההקצאה הדינמית, אנו יכולים לציין את גודל המערך לא בהכרח כקבוע, כך נוכל לקבוע בזמן ריצת התוכנית את גודל הזיכרון המוקצה.

פרטים על האופרטורים new ו-delete

[עריכה]
  • כאשר אין אפשרות להקצות את הזיכרון הנדרש האופרטור new יזרוק חריגה bad_alloc (למד על חריגות בהמשך). אם נרצה שאופרטור זה יחזיר 0 (NULL) במקרה זה נציין nothrow בסוגריים עגולים אחרי המילה new.
  • זיכרון שהוקצה ע"י new צריך לשחרר ע"י delete וזיכרון שהוקצה ע"י []new צריך לשחרר ע"י []delete, אחרת אופן פעולת התוכנית לא מוגדר והיא עלולה לקרוס.
  • אם נשתמש בפונקציות של C להקצות זיכרון, אין לשחרר אותו באמצעות אופרטורי ++C, ולהיפך.
  • על אף שלא ניתנת הודעת שגיאה על אי שחרור זיכרון שהוקצה ומערכת ההפעלה תשחרר, בדרך כלל, את הזיכרון באופן אוטומטי בעת סיום פעולת התוכנית, רצוי לשחרר את הזיכרון ע"י delete ישירות לאחר סוף השימוש בו. הסיבה לכך היא מניעת דליפת זיכרון וצורך להפעיל את המפרקים של המחלקות (למדו בהמשך).
  • נוכל לציין את גודל הזיכרון המוקצה למערך שווה ל-0. דבר זה לא נחשב לשגיאה. זיכרו כי נצטרך לשחרר זיכרון זה כמו כל זיכרון אחר, הסיבה לכך היא המבנה של הערימה.

שינוי גודל הזיכרון שהוקצה

[עריכה]

פעולה תכנותית נפוצה היא הוספה ומחיקה של פריטים למערך. פעולה זו הופכת לבעייתית כאשר אנו מגיעים לסוף המערך של N איברים וברצוננו להוסיף איבר נוסף. במקרה זה עלינו להגדיל את נפח הזיכרון שכבר הוקצה עבור המערך.

מתכנתי C רגילים לפונקציית realloc שמבצעת פעולה זו. בC++‎ אין לא אופרטור מיוחד ולא פונקציה מיוחדת לצורך זה. ישנן שתי דרכים להשיג את המטרה הרצויה:

  • שימוש ב-new ו-delete - ניתן לדמות את פעולת הפונקציה realloc מ-C. דרך זו לא מקובלת ולא מומלצת. להלן פונקציה שעושה את זה:
char *my_realloc(char *old_memory, int old_size, int new_size)
{
    char *new_memory = new char[new_size];
    for(int i = 0; i < min(old_size, new_size); i++)
        new_memory[i] = old_memory[i];

    delete[] old_memory;
    return new_memory;
}

לפעולה זו שני חסרונות. חסרון אחד הוא הצורך ב-(old_size + new_size) זיכרון פנוי רצוף תמיד. חיסרון שני הוא הצורך בהעתקת כל האיברים כל פעם שאנו רוצים להגדיל את המערך לפחות ב-1. לפונקציה realloc ישנה גישה ישירה לערימה (heap) ולכן היא יכולה לחסוך את שני החסרונות הללו כשיש אזור פנוי בזכרון הבא ישירות אחרי האזור המוקצה.

  • שימוש ב-vector מ-STL - בספריה התקנית של ++C קיימת מחלקה vector אשר תפקידה לאפשר עבודה נוחה עם מערכים. מחלקה זו מכילה ממשק הכולל הכנסת איברים ומחיקת איברים ממערכים. להלן קטע תוכנית לדוגמה:
#include <vector>
/*...*/

vector<int> v(8);       // ווקטור (מערך) של 8 איברים
size_t i, new_size;

// קלט 8 איברים ראשונים
for(i = 0; i < v.size(); i++)
    cin >> v[i];

// קלט מספר והגדלת המערך למספר זה של איברים
cin >> new_size;
v.resize(new_size);

// קלט איברים חדשים
for(; i < v.size(); i++)
    cin >> v[i];

// פלט
for(i = 0; i < v.size(); i++)
    cout << v[i];

להבנה מעמיקה יותר של קטע זה יש ללמוד תחילה על מחלקות, תבניות וממשק של std::vector.


הפרק הקודם:
מבנים ואיגודים
זיכרון דינמי הפרק הבא:
מחלקות