Menu

calloc ו-realloc ב-C: זיכרון מאופס והגדלת מערך

calloc נותנת זיכרון מאופס ומכפילה את הגודל בבטחה; realloc מגדילה בלוק שכבר מילאתם. העמוד הזה מכסה את שתיהן, את ניב המצביע הזמני שמונע מ-realloc שנכשלה לגרום לדליפה, ומתי calloc עדיפה על malloc ועוד memset.

בדף הזה יש עורכים שאפשר להריץ - לערוך, להריץ ולראות את הפלט מיד.

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 חוסכת את עבודת האיפוס.

איור של שפות התכנות ב-Coddy

ללמוד תכנות עם Coddy

להתחיל