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

malloc في لغة C: تخصيص الذاكرة الديناميكي وfree وsizeof

لماذا توجد الكومة، وكيف يسلّمك malloc ذاكرة يُقرَّر حجمها أثناء التشغيل، وصيغة p = malloc(n * sizeof *p)، وفحص NULL، والتحرير مرة واحدة بالضبط، ومصفوفة ديناميكية من البداية إلى النهاية.

تحتوي هذه الصفحة على محررات قابلة للتشغيل - حرّر، شغّل، وشاهد النتيجة فوراً.

كل مصفوفة كتبتها حتى الآن كان حجمها مخبوزًا وقت الترجمة: فـ int scores[10]; يحجز عشرة مواضع سواء احتجت ثلاثة أم ثلاثمئة. وينجح ذلك حتى يصير الحجم معروفًا فقط أثناء تشغيل البرنامج - كم سطرًا في ملف، وكم سجلًّا أدخل المستخدم. والذاكرة الديناميكية تحلّ ذلك: تطلب البايتات التي تحتاجها، حين تحتاجها، وتعيدها حين تنتهي.

والأدوات في <stdlib.h>:

#include <stdlib.h>

لماذا توجد الكومة

تمنح C برنامجك منطقتين رئيسيتين من الذاكرة. المتغيّرات المحلية تعيش في التخزين التلقائي (المكدّس): يقرّر المترجم حجمها، وتزول لحظة عودة دالتها. أما التخصيصات الديناميكية فتعيش في الكومة: تقرّر الحجم أنت أثناء التشغيل، وتبقى الكتلة حيّة حتى تحرّرها صراحةً.

وتلك الخاصية الثانية هي السبب الحقيقي لاستخدام malloc. فيمكن لدالة أن تخصّص كتلة وتُعيد المؤشّر، وتبقى الذاكرة صالحة لدى المستدعي - وهو ما لا تستطيعه مصفوفة محلية أبدًا. والمنطقتان مقارنتان بالتفصيل في المكدّس مقابل الكومة.

malloc: طلب البايتات

يأخذ malloc عدد بايتات ويُعيد void * يشير إلى ذلك العدد من بايتات الذاكرة غير المهيّأة، أو NULL إن فشل الطلب.

أربعة أشياء في ذلك البرنامج القصير هي الانضباط كله:

  1. تعبير الحجم هو n * sizeof *scores. اقرأه على أنه "n من أيًّا كان ما تشير إليه scores".
  2. تُفحَص النتيجة مقابل NULL قبل أي استخدام.
  3. تُستخدَم الكتلة تمامًا كمصفوفة - فـ scores[i] تعمل لأن الفهرسة حساب مؤشّرات.
  4. تُحرَّر مرة واحدة، ثم يُضبَط المؤشّر على NULL.

لاحظ أنه لا تحويل على القيمة المُعادة. ففي C يُحوَّل void * إلى أي مؤشّر كائن تلقائيًا، وكتابة (int *)malloc(...) تضيف ضجيجًا وتخفي غياب تضمين <stdlib.h>. (و++C تتطلّب التحويل؛ أما C فلا.)

صيغة sizeof

لماذا sizeof *scores بدل sizeof(int)؟ لأنها لا يمكن أن تتقادم.

int  *a = malloc(n * sizeof *a);     /* n من الأعداد الصحيحة، أيًّا كان int هنا */
long *b = malloc(n * sizeof *b);     /* شكل السطر نفسه، والحجم صحيح */

/* الصيغة الهشّة */
long *c = malloc(n * sizeof(int));   /* تُترجَم، وتخصّص أقلّ من اللازم */

السطر الأخير علّة حقيقية لا يحذّر منها أي مترجم: فـ c من نوع long *، والتخصيص محسوب على مقاس int، وكل كتابة بعد النصف الأول تخرج عن النهاية. ومع sizeof *c يتبع الحجم التعريف تلقائيًا.

وsizeof *p لا تفكّ إشارة p - فـ sizeof تُقيَّم وقت الترجمة من النوع وحده، فهي آمنة حتى حين يكون p غير مهيّأ أو NULL.

الذاكرة غير المهيّأة

لا يمسح malloc ما يعطيك إياه. فالبايتات تحمل ما كان فيها من قبل.

وحين تريد الكتلة مصفّرة نيابةً عنك، يفعل calloc ذلك في خطوة واحدة - انظر calloc وrealloc.

free: إعادتها

يُعيد free(p) الكتلة إلى المخصّص. وثلاث قواعد:

  • مرّر المؤشّر المضبوط الذي أعاده malloc، لا واحدًا تقدّم. فـ free(p + 1) سلوك غير معرَّف.
  • حرّر كل كتلة مرة واحدة بالضبط. ومرتين تحرير مزدوج، يُفسد سجلّات المخصّص نفسها.
  • بعد التحرير يصبح المؤشّر متقادمًا. واستخدامه استخدام بعد التحرير.
int *p = malloc(sizeof *p);
free(p);
*p = 5;        /* استخدام بعد التحرير - سلوك غير معرَّف */
free(p);       /* تحرير مزدوج - سلوك غير معرَّف */

ولا يلزم أن ينهار أي من السطرين فورًا، وهذا ما يجعلهما خطرين: فقد يعمل البرنامج دقائق ثم يفشل في مكان لا علاقة له. والدفاع الرخيص هو تفريغ المؤشّر:

free(p);
p = NULL;

free(NULL) معرَّفة صراحةً بأنها لا تفعل شيئًا، فتصبح free(p) ثانية بعدها غير ضارّة، وتصير *p انهيارًا فوريًا عند موضع الخطأ الحقيقي بدل إفساد صامت.

مصفوفة ديناميكية من البداية إلى النهاية

إليك شكل تخصيص حقيقي: اقرأ عددًا أثناء التشغيل، وحدّد حجم الكتلة منه، واستخدمها، وحرّرها.

والتحويل إلى size_t على العدّاد يهمّ بمجرّد أن تكبر الأحجام: فـ n * sizeof *values مع n من نوع int عادي يمكن أن يفيض قبل أن يصل حاصل الضرب إلى malloc أصلًا، فيُنتج كتلة أصغر بكثير من المقصود. والضرب في size_t يتجنّب ذلك.

التخصيص داخل دالة

قاعدة عمر الكومة هي ما يجعل هذا مسموحًا - وما يجعل إعادة مصفوفة محلية ممنوعة:

تترك len + 1 مكانًا لمحرف الإنهاء الصفري - وهي قاعدة التحجيم نفسها في كل مكان في السلاسل النصية. ولاحظ التعليق فوق الدالة: فحين تُعيد دالة ذاكرة مخصّصة، يكون من يحرّرها جزءًا من عقدها. وكتابة ذلك ليست بيروقراطية؛ بل هي الشيء الوحيد الذي يمنع تسريبًا.

البنى على الكومة

الصيغة نفسها تعمل لأي نوع:

لاحظ أن فرع الفشل الثاني يحرّر p قبل العودة. فبمجرّد أن تحمل دالة أكثر من تخصيص واحد، يجب على كل مسار خطأ تحرير ما أُخذ سلفًا - وهو أشيع مصدر للتسريبات الموصوفة في تسريبات الذاكرة.

قائمة تحقّق

  • افحص دائمًا malloc بحثًا عن NULL قبل أول استخدام.
  • حدّد الحجم بـ n * sizeof *p، مع الضرب في size_t.
  • لا تحويل على القيمة المُعادة في C.
  • ذاكرة malloc غير مهيّأة؛ فاكتب قبل أن تقرأ.
  • حرّر مرة واحدة بالضبط، بالمؤشّر الأصلي، ثم اضبطه على NULL.
  • كل مسار خطأ بعد تخصيص يجب أن يحرّر ما يحمله سلفًا.

الأسئلة الشائعة

ماذا يفعل malloc في لغة C؟

يطلب malloc(n) عدد n بايتًا من الذاكرة من الكومة ويُعيد مؤشّرًا إلى بداية تلك الكتلة، أو 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؟

تبقى الكتلة مخصّصة مدى حياة العملية - أي تسريب ذاكرة. وينجو برنامج قصير من ذلك لأن نظام التشغيل يستعيد كل شيء عند الخروج، لكن برنامجًا طويل الأمد يسرّب قليلًا في كل مرور ويستنفد الذاكرة في النهاية. انظر تسريبات الذاكرة.

Coddy programming languages illustration

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

ابدأ الآن