malloc עונה על שאלה אחת: תן לי כך וכך בתים. שתי חברות ב-<stdlib.h> עונות על השאלות שמגיעות אחר כך: תן לי כך וכך בתים, מנוקים (calloc), ואני צריך שהבלוק שכבר יש לי יהיה גדול יותר (realloc).
calloc: מספר, גודל ואפסים
void *calloc(size_t count, size_t size);
שני ארגומנטים במקום אחד, והתוצאה ממולאת בבתים של אפס.
זה המקרה הקלאסי: היסטוגרמה או מערך ספירה שבו כל תא חייב להתחיל מאפס. עם malloc הייתם צריכים לולאה; calloc עושה את זה כחלק מההקצאה, הרבה פעמים בחינם, כי מערכת ההפעלה ממילא מחלקת דפים חדשים שכבר מאופסים.
טיעון הגלישה
הפיצול לשני ארגומנטים אינו קוסמטי. חשבו על מספר איברים שנקרא מקובץ:
size_t n = huge_value_from_input;
int *a = malloc(n * sizeof *a); /* the product can wrap around */
int *b = calloc(n, sizeof *b); /* required to detect the overflow */
אם n * sizeof *a חורג ממה ש-size_t יכול להכיל, הוא מתגלגל למספר קטן, malloc מצליחה עם בלוק זעיר, וכל כתיבה אחר כך רצה הרחק מעבר לסוף. לפי התקן, calloc חייבת להיכשל ולהחזיר NULL במקום זאת. כשגודל מגיע מחוץ לתוכנית שלכם, הבדיקה הזו שווה את זה.
מה "מאופס" אומר בפועל
calloc כותבת בתים של אפס. עבור מספרים שלמים ותווים זה הערך אפס, וזה מה שרוצים. עבור מצביעים וערכי נקודה צפה, כל הביטים אפס הם מצביע null ו-0.0 בכל פלטפורמה מרכזית, אבל תקן C לא מבטיח את זה. קוד שחייב להיות נייד לחלוטין משים את הערכים האלה במפורש.
calloc מול malloc ועוד memset
int *a = calloc(n, sizeof *a);
int *b = malloc(n * sizeof *b);
if (b != NULL) {
memset(b, 0, n * sizeof *b);
}
הם מפיקים את אותה תוצאה. העדיפו את calloc: זו שורה אחת, היא מבצעת את בדיקת הגלישה, ובבלוקים גדולים היא יכולה להימנע מלגעת בזיכרון בכלל. העדיפו malloc רגילה כשאתם עומדים לדרוס כל בית בכל מקרה: לאפס מגה-בייט שתמלאו מיד הוא בזבוז טהור.
realloc: שינוי הגודל
void *realloc(void *p, size_t newsize);
realloc מחזירה בלוק של newsize בתים שהתוכן שלו תואם לבלוק הישן עד הקטן מבין שני הגדלים. היא עשויה להרחיב את הבלוק במקומו, או להקצות בלוק חדש, להעתיק ולשחרר את הישן. אי אפשר לדעת מה מהשניים, כך שהמצביע היחיד שמותר להשתמש בו אחר כך הוא זה שהיא החזירה.
ניב המצביע tmp
שלוש השורות סביב tmp הן כל הסיבה שלפונקציה הזו יש מוניטין. הקיצור המפתה שבור:
/* BUG: leaks the original block when realloc fails */
p = realloc(p, newsize);
if (p == NULL) {
return;
}
כש-realloc נכשלת היא מחזירה NULL ומשאירה את הבלוק המקורי מוקצה וללא שינוי. השמת התוצאה ישירות ל-p דורסת את המצביע היחיד לבלוק הזה, כך שלעולם אי אפשר יהיה לשחרר אותו: דליפה, וכזו שקורית בדיוק כשהזיכרון כבר נדיר. השימו ל-tmp, בדקו את tmp, ואז קבעו:
void *tmp = realloc(p, newsize);
if (tmp == NULL) {
/* p is still valid; handle the failure however suits the caller */
return 0;
}
p = tmp;
עוד שתי התנהגויות שכדאי להכיר: realloc(NULL, n) מתנהגת בדיוק כמו malloc(n), וזה מאפשר לפונקציית הגדלה לטפל בקריאה הראשונה שלה בלי מקרה מיוחד. והגדלה משאירה את הבתים החדשים לא מאותחלים: רק התוכן הישן נשמר.
הגדלת מערך: התבנית המקובלת
זו התבנית שכמעט כל מבנה נתונים ב-C משתמש בה: אורך, קיבולת, והכפלה כשהם נפגשים.
שתי החלטות עיצוב נושאות כאן את המשקל.
הכפלה, לא הוספה של אחד. הגדלה בכמות קבועה גורמת לכל הוספה להעתיק את כל המערך, כך שמילוי n איברים עולה בערך n² העתקות. הכפלה הופכת את ההעתקות לנדירות מספיק כדי שכל הוספה תעלה זמן קבוע בממוצע. ההתחלה cap == 0 ? 4 : cap * 2 מטפלת בהוספה הראשונה בלי ענף נפרד, כי realloc(NULL, n) היא פשוט malloc.
ה-struct מחזיק הקצאה אחת. v.data משוחרר בדיוק פעם אחת, על ידי מי שמחזיק את ה-struct. לכתוב את כלל הבעלות הזה, בהערה או בזיווג של כל vec_init עם vec_free, זה מה שמונע ממבנה שגדל לדלוף כשהוא עובר בין פונקציות.
הקטנה
realloc עם גודל קטן יותר מקצצת בלוק, וזה שימושי אחרי שקראתם כמות לא ידועה של נתונים לחוצץ גדול מדי:
הקטנה שנכשלה אינה שגיאה שכדאי לעצור בגללה: הבלוק הישן והגדול יותר עדיין תקף ועדיין נכון. זה המקרה היחיד שבו התעלמות מהכישלון היא ההחלטה הנכונה, אבל שימו לב שעדיין עוברים דרך tmp, כי הכלל לא לדרוס את המצביע החי לא משתנה.
באיזו מהן להשתמש
| מה אתם רוצים | במה להשתמש |
|---|---|
| בתים, התוכן לא משנה | malloc(n * sizeof *p) |
| מספר פריטים, מאופסים | calloc(n, sizeof *p) |
| מספר שמגיע מקלט לא אמין | calloc, בשביל בדיקת הגלישה |
| הבלוק שיש לכם, גדול או קטן יותר | realloc דרך מצביע tmp |
| הקצאה ראשונה בלולאת הגדלה | realloc(NULL, n), בלי מקרה מיוחד |
את כל הארבע משחררים עם אותה free, בדיוק פעם אחת.
טעויות נפוצות
p = realloc(p, n): דולף במקרה של כישלון. תמיד עברו דרךtmp.- שמירת מצביע ישן לתוך הבלוק. אחרי
reallocהנתונים אולי זזו, כך שכל מצביע פנימי שנשמר או כתובת שחושבה מאינדקס אינם עדכניים. אינדקסים שורדים מעבר; מצביעים לא. - הנחה שהבתים החדשים הם אפס. רק
callocמאפסת.reallocמשאירה את אזור ההגדלה לא מאותחל. calloc(n * size, 1): זה זורק את בדיקת הגלישה שבשבילה קיימת הצורה עם שני הארגומנטים.- שחרור המצביע הישן אחרי
reallocשהצליחה.reallocכבר שחררה אותו;freeשנייה היא שחרור כפול.
שאלות נפוצות
מה ההבדל בין calloc ל-malloc ב-C?
שני הבדלים. calloc(n, size) מקבלת את מספר האיברים ואת גודל האיבר בנפרד ובודקת שהמכפלה שלהם לא גולשת, בעוד ש-malloc(n * size) יכולה להתגלגל בשקט. ו-calloc ממלאת את הבלוק באפסים, בעוד ש-malloc משאירה בו את מה שהיה שם קודם.
איך realloc עובדת ב-C?
realloc(p, newsize) מחזירה בלוק בגודל החדש שבו התוכן הקיים שלכם נשמר עד הקטן מבין שני הגדלים. היא עשויה להגדיל את הבלוק במקומו או להעביר אותו, כך שהמצביע שהוחזר הוא היחיד שמותר להמשיך להשתמש בו: הישן אולי כבר שוחרר.
למה צריך להשים את התוצאה של realloc למצביע זמני?
כי p = realloc(p, n) דורס את p עם NULL כשהקריאה נכשלת, והבלוק המקורי עדיין מוקצה בלי שום דבר שמצביע עליו: דליפה. השימו קודם ל-tmp, בדקו אם הוא NULL, ורק אז כתבו p = tmp.
מתי כדאי להשתמש ב-calloc במקום malloc?
כשבאמת צריך זיכרון מאופס (מערך מונים, struct שהשדות שלו צריכים להתחיל ריקים, חוצץ שתמלאו רק חלקית) או כשהגודל הוא מספר איברים כפול גודל איבר שעלול להיות גדול. אם ממילא תדרסו כל בית, malloc חוסכת את עבודת האיפוס.