Menu
flag Ar iconالعربيةdown icon

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);     /* يمكن أن يلتفّ الحاصل */
int *b = calloc(n, sizeof *b);      /* مطلوب منها اكتشاف الفيضان */

إن تجاوز n * sizeof *a ما يستطيع size_t حمله، فإنه يلتفّ إلى عدد صغير، فينجح malloc بكتلة ضئيلة، وتمضي كل كتابة لاحقة بعيدًا بعد النهاية. أما calloc فالمعيار يلزمها بالفشل وإعادة NULL بدلًا من ذلك. وعندما يأتي الحجم من خارج برنامجك، فذلك الفحص يستحقّ الاقتناء.

ما الذي يعنيه "مصفّرة" فعلًا

تكتب calloc بايتات صفرية. وبالنسبة للأعداد الصحيحة والمحارف تكون تلك هي القيمة صفر، وهو ما تريده. أما للمؤشّرات وقيم الفاصلة العائمة، فكون البتّات كلها أصفارًا يعني مؤشّرًا فارغًا و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 ما زالت صالحة؛ عالج الفشل بما يناسب المستدعي */
    return 0;
}
p = tmp;

وثمّة سلوكان آخران يستحقّان المعرفة: realloc(NULL, n) تتصرّف تمامًا مثل malloc(n)، وهو ما يتيح لدالة نمو أن تعالج استدعاءها الأول دون حالة خاصة. والتكبير يترك البايتات الجديدة غير مهيّأة - فالمحفوظ هو المحتوى القديم فقط.

تكبير مصفوفة: النمط القياسي

إليك النمط الذي تستخدمه كل حاوية في C تقريبًا - طول وسعة ومضاعفة عند تلاقيهما.

نقطتان تصميميتان تحملان الثقل هنا.

المضاعفة، لا الزيادة بواحد. النمو بمقدار ثابت يجعل كل إضافة تنسخ المصفوفة كاملة، فيكلّف ملء n عنصرًا نحو من نسخ البايتات. أما المضاعفة فتجعل النسخ نادرًا بما يكفي ليكلّف كل إدخال زمنًا ثابتًا في المتوسط. والبداية cap == 0 ? 4 : cap * 2 تعالج الإدخال الأول دون فرع منفصل، لأن realloc(NULL, n) ليست سوى malloc.

البنية تملك تخصيصًا واحدًا. تُحرَّر v.data مرة واحدة بالضبط، على يد من يملك البنية. وكتابة قاعدة الملكية تلك - في تعليق، أو بإقران كل 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؟

عندما تحتاج فعلًا إلى ذاكرة مصفّرة (مصفوفة عدّادات، بنية يجب أن تبدأ حقولها فارغة، مخزن ستملؤه جزئيًا) أو عندما يكون الحجم عددًا مضروبًا في حجم عنصر وقد يكون كبيرًا. أما إن كنت ستكتب فوق كل بايت على أي حال، فإن malloc يوفّر عليك عمل التصفير.

Coddy programming languages illustration

تعلّم البرمجة مع Coddy

ابدأ الآن