לכל מערך שכתבתם עד עכשיו היה גודל שנקבע בזמן קומפילציה: int scores[10]; שומר עשרה תאים בין אם צריך שלושה או שלוש מאות. זה עובד עד שהגודל ידוע רק בזמן שהתוכנית רצה: כמה שורות יש בקובץ, כמה רשומות המשתמש מזין. זיכרון דינמי פותר את זה: מבקשים את הבתים שצריך, כשצריך אותם, ומחזירים אותם כשמסיימים.
הכלים נמצאים ב-<stdlib.h>:
#include <stdlib.h>
למה ה-heap קיים
C נותנת לתוכנית שלכם שני אזורי זיכרון עיקריים. משתנים מקומיים חיים באחסון אוטומטי (המחסנית, stack): הקומפיילר מחליט על הגודל שלהם, והם נעלמים ברגע שהפונקציה שלהם מסתיימת. הקצאות דינמיות חיות ב-heap: אתם מחליטים על הגודל בזמן ריצה, והבלוק נשאר חי עד שמשחררים אותו במפורש.
התכונה השנייה הזו היא הסיבה האמיתית להשתמש ב-malloc. פונקציה יכולה להקצות בלוק, להחזיר את המצביע, והזיכרון עדיין תקף אצל מי שקרא לה, משהו שמערך מקומי לעולם לא יכול לעשות. שני האזורים מושווים בפירוט בעמוד על stack מול heap.
malloc: בקשת בתים
malloc מקבלת מספר בתים ומחזירה void * שמצביע על כמות כזו של זיכרון לא מאותחל, או NULL אם הבקשה נכשלת.
ארבעה דברים בתוכנית הקצרה הזו הם כל המשמעת:
- ביטוי הגודל הוא
n * sizeof *scores. קראו אותו כ-"n פעמים מה שלא יהיה ש-scoresמצביע עליו". - התוצאה נבדקת מול
NULLלפני כל שימוש. - משתמשים בבלוק בדיוק כמו במערך:
scores[i]עובד כי גישה לפי אינדקס היא חשבון מצביעים. - הוא משוחרר פעם אחת, ואז המצביע מקבל
NULL.
שימו לב שאין המרה על ערך ההחזרה. ב-C, void * מומר לכל מצביע לאובייקט באופן אוטומטי, וכתיבת (int *)malloc(...) מוסיפה רעש ומסתירה הכללה חסרה של <stdlib.h>. (C++ דורשת את ההמרה; C לא.)
ניב ה-sizeof
למה sizeof *scores ולא sizeof(int)? כי הוא לא יכול להתיישן.
int *a = malloc(n * sizeof *a); /* n ints, whatever int is here */
long *b = malloc(n * sizeof *b); /* same line shape, right size */
/* the fragile form */
long *c = malloc(n * sizeof(int)); /* compiles, allocates too little */
השורה האחרונה היא באג אמיתי שאף קומפיילר לא מזהיר עליו: c הוא long *, ההקצאה בגודל של int, וכל כתיבה אחרי החצי הראשון רצה מעבר לסוף. עם sizeof *c הגודל עוקב אחרי ההצהרה באופן אוטומטי.
sizeof *p לא ניגש לתוכן של p: sizeof מחושב בזמן קומפילציה מהטיפוס בלבד, ולכן זה בטוח גם כש-p לא מאותחל או NULL.
זיכרון לא מאותחל
malloc לא מנקה את מה שהיא נותנת לכם. הבתים מחזיקים את מה שהיה שם קודם.
כשרוצים שהבלוק יאופס בשבילכם, calloc עושה את זה בצעד אחד; ראו calloc ו-realloc.
free: להחזיר את הזיכרון
free(p) מחזירה את הבלוק למקצה. שלושה כללים:
- העבירו את המצביע המדויק ש-
mallocהחזירה, לא כזה שקידמתם.free(p + 1)היא התנהגות לא מוגדרת. - שחררו כל בלוק בדיוק פעם אחת. פעמיים זה double free, שמשחית את הרישום הפנימי של המקצה.
- אחרי השחרור, המצביע כבר לא תקף. שימוש בו הוא use-after-free.
int *p = malloc(sizeof *p);
free(p);
*p = 5; /* use-after-free - undefined behavior */
free(p); /* double free - undefined behavior */
אף אחת מהשורות לא חייבת לקרוס מיד, וזה מה שהופך אותן למסוכנות: התוכנית עשויה לרוץ דקות ואז להיכשל במקום שלא קשור בכלל. ההגנה הזולה היא לאפס את המצביע:
free(p);
p = NULL;
free(NULL) מוגדרת במפורש כפעולה שלא עושה כלום, כך ש-free(p) שנייה אחרי זה לא מזיקה, ו-*p הופך לקריסה מיידית בנקודת התקלה האמיתית, במקום השחתה שקטה.
מערך דינמי מההתחלה ועד הסוף
זו הצורה של הקצאה אמיתית: קוראים מספר בזמן ריצה, קובעים לפיו את גודל הבלוק, משתמשים בו ומשחררים אותו.
ההמרה ל-size_t של המספר חשובה ברגע שהגדלים גדלים: n * sizeof *values עם n מטיפוס int רגיל יכול לגלוש עוד לפני שהמכפלה מגיעה ל-malloc, ולייצר בלוק קטן בהרבה ממה שהתכוונתם. הכפלה ב-size_t מונעת את זה.
הקצאה בתוך פונקציה
כלל אורך החיים של ה-heap הוא מה שהופך את זה לחוקי, ומה שהופך החזרה של מערך מקומי ללא חוקית:
len + 1 משאיר מקום לתו ה-null המסיים, אותו כלל גודל כמו בכל מקום במחרוזות. ושימו לב להערה מעל הפונקציה: כשפונקציה מחזירה זיכרון מוקצה, מי שמשחרר אותו הוא חלק מהחוזה שלה. לכתוב את זה אינו בירוקרטיה; זה הדבר היחיד שמונע דליפה.
structs ב-heap
אותו ניב עובד לכל טיפוס:
שימו לב שענף הכישלון השני משחרר את p לפני ההחזרה. ברגע שפונקציה מחזיקה יותר מהקצאה אחת, כל מסלול שגיאה צריך לשחרר את אלה שכבר נלקחו, וזה המקור הנפוץ ביותר לדליפות שמתוארות בעמוד על דליפות זיכרון.
רשימת בדיקה
- תמיד בדקו את
mallocמולNULLלפני השימוש הראשון. - קבעו גודל עם
n * sizeof *p, והכפילו ב-size_t. - בלי המרה על ערך ההחזרה ב-C.
- זיכרון של
mallocלא מאותחל; כתבו לפני שאתם קוראים. - שחררו בדיוק פעם אחת, עם המצביע המקורי, ואז הציבו בו
NULL. - כל מסלול שגיאה אחרי הקצאה חייב לשחרר את מה שהוא כבר מחזיק.
שאלות נפוצות
מה malloc עושה ב-C?
malloc(n) מבקשת n בתים של זיכרון מה-heap ומחזירה מצביע לתחילת הבלוק הזה, או NULL אם אי אפשר למלא את הבקשה. הבלוק חי עד שמעבירים את אותו מצביע ל-free: בניגוד למשתנה מקומי, הוא לא משתחרר כשהפונקציה מסתיימת.
איך משתמשים ב-malloc וב-free ב-C?
מקצים עם int *p = malloc(n * sizeof *p);, בודקים if (p == NULL) לפני שנוגעים בו, משתמשים בו כמו במערך, ואז free(p); בדיוק פעם אחת כשמסיימים. הצבת p = NULL אחר כך הופכת שימוש מקרי מאוחר יותר לקריסה נקייה במקום השחתה שקטה.
למה malloc(n * sizeof *p) עדיף על malloc(n * sizeof(int))?
כי sizeof *p עוקב אחרי הטיפוס של המצביע באופן אוטומטי. אם p הופך מאוחר יותר ל-long * או ל-struct Point *, גודל ההקצאה מתעדכן מעצמו; כתיבה מפורשת של sizeof(int) משאירה גודל שכבר שגוי, והקומפיילר לא יסמן אותו.
מה קורה אם לא משחררים זיכרון ב-C?
הבלוק נשאר מוקצה לכל אורך חיי התהליך: דליפת זיכרון. תוכנית קצרה יוצאת מזה בשלום כי מערכת ההפעלה מחזירה לעצמה הכול ביציאה, אבל תוכנית שרצה זמן רב דולפת קצת בכל סיבוב ובסוף ממצה את הזיכרון. ראו דליפות זיכרון.