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

المكدّس مقابل الكومة في لغة C: الأعمار والمؤشّرات المعلّقة ومتى تستخدم أيّهما

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

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

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

المناطق الثلاث

  high addresses
  +---------------------------+
  |  stack                    |  locals, parameters, return addresses
  |    grows downward  |      |  freed automatically on return
  |                    v      |
  +---------------------------+
  |         (unused gap)      |
  +---------------------------+
  |                    ^      |
  |    grows upward    |      |
  |  heap                     |  malloc / calloc / realloc blocks
  +---------------------------+  freed only by free()
  |  static / global data     |  globals and statics, whole run
  +---------------------------+
  |  code (text)              |  the machine code, read-only
  +---------------------------+
  low addresses
  • التخزين التلقائي (المكدّس) يحمل وسائط الدوال والمتغيّرات المحلّية غير الساكنة. تُطلَب كتلة من المكدّس عند دخول دالة وتُحرَّر عند عودتها. والحجم ثابت في وقت الترجمة.
  • التخزين الديناميكي (الكومة) يحمل كل ما يأتي من malloc وcalloc وrealloc. ويُقرَّر الحجم في وقت التشغيل؛ ولا ينتهي العمر إلا عند free.
  • التخزين الساكن يحمل المتغيّرات العامة وكل ما يُعرَّف static. وهو موجود طوال تشغيل البرنامج ويُهيَّأ بصفر قبل بدء main.

والعناوين في الرسم هي الترتيب المعتاد لا ضمانة - فالمعيار يصف الأعمار لا التوزيع.

التخزين التلقائي في العمل

يحصل كل استدعاء لـ demo على local جديدة وtable جديدة. ولا يُحرَّر شيء يدويًا، ولا يمكن أن يسرّب شيء، ويكلّف التخصيص تعليمة واحدة تحرّك مؤشّر المكدّس. ولهذا ينبغي أن تكون المتغيّرات المحلّية العادية خيارك الافتراضي: فهي أسرع تخزين وأأمنه في C.

والمأخذ هو القوس المعقوف المغلق. فبمجرّد تنفيذه، تزول تلك الذاكرة.

المؤشّر المعلّق

إليك الخطأ الذي يكتبه كل مبرمج C مرة:

/* BROKEN: returns the address of memory that no longer exists */
int *make_counter(void) {
    int count = 0;
    return &count;          /* count dies at this brace */
}

int main(void) {
    int *p = make_counter();
    *p = 5;                 /* writing into a dead stack frame */
    return 0;
}

كانت &count عنوانًا صالحًا تمامًا بينما كانت make_counter تعمل. وعند العودة، تُسلَّم مساحة المكدّس تلك لأي دالة تُستدعى بعدها، فتشير p الآن إلى متغيّرات محلّية لشخص آخر. والقراءة تعطي قمامة؛ والكتابة تفسدها. ويحذّر GCC وClang من هذا الشكل بالضبط (-Wreturn-local-addr)، فترجم مع تفعيل التحذيرات.

والخطأ نفسه يرتدي قناعًا مع المصفوفات، وهناك لا يعمل التحذير غالبًا:

والنسخة المعطوبة من تلك الدالة كانت ستبني النصّ في char buf[64] محلّية وتنفّذ return buf; - أي تُرجِع عنوان مخزن يكفّ عن الوجود في اللحظة نفسها.

ثلاث طرق لإصلاحه

1. المستدعي يوفّر المخزن (كما سبق). بلا تخصيص وبلا سؤال ملكية، وهو الأسلوب الأشيع في مكتبات C. وتأخذ الدالة الحجم لتبقى داخله.

2. أرجِع ذاكرة كومة، وقُل من يحرّرها.

تعمّر كتلة الكومة أطول من الدالة بحكم التصميم - وذلك هو كل المقصود من الذاكرة الديناميكية. والثمن هو تعليق الملكية وfree عند المستدعي.

3. استخدم التخزين الساكن، حين يكون مخزن مشترك واحد مقبولًا:

تُبقي static داخل دالة نطاقَ المتغيّر محلّيًا مع منحه عمر البرنامج، فيكون إرجاع عنوانه مشروعًا. والمقايضة أنه لا يوجد منه إلا واحد أبدًا: فكل مستدعٍ يتشاركه، ما يجعل هذا النمط غير صالح للاستخدام في شيفرة متعدّدة الخيوط ومفاجئًا حتى في شيفرة أحادية الخيط حين يحمل مستدعيان المؤشّر في آن.

الحجم: أين ينفد المكدّس

مساحة المكدّس صغيرة وثابتة. ويحصل الخيط الرئيسي عادةً على 1 ميغابايت على ويندوز و8 ميغابايت على لينكس؛ ويحصل الخيط المُنشأ غالبًا على أقلّ بكثير. أما الكومة فمحدودة بذاكرة النظام المتاحة.

void bad(void) {
    int huge[1000000];      /* ~4 MB of stack - likely crashes on entry */
    huge[0] = 1;
}

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

ويستنفد الاستدعاء الذاتي العميق المكدّسَ بالطريقة نفسها، إطارًا إطارًا - فالدالة التكرارية الذاتية الجامحة هي أشيع سبب لفيضان المكدّس عمليًا.

التكلفة والتموضع

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

وبيانات المكدّس أيضًا مرصوصة وحديثة اللمس، فتميل إلى أن تكون في الذاكرة المخبأة. وقد تكون كتل الكومة متناثرة. ولا ينبغي لأي من الحقيقتين أن تقود تصميمًا وحدها - فصحّة العمر تأتي أولًا - لكن بين تصميمين كلاهما صحيح، يكون تصميم المكدّس أسرع عادةً.

رؤية المناطق

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

يقع العام والساكن بجوار بعضهما؛ وتقع كتلة الكومة في موضع آخر؛ ويقع المحلّي عادةً بعيدًا عن كليهما. وحوّل إلى void * لأجل %p - فذلك ما يتطلّبه محدّد التنسيق.

الاختيار

استخدم المكدّس حين:

  • يكون الحجم معروفًا في وقت الترجمة،
  • وتُحتاج البيانات داخل هذه الدالة والدوال التي تستدعيها فقط،
  • ويكون صغيرًا - بضعة كيلوبايتات لا ميغابايتات.

استخدم الكومة حين:

  • يعتمد الحجم على إدخال أو ملف أو حساب،
  • ويجب أن تعمّر البيانات أطول من الدالة التي أنشأتها،
  • أو يكون كبيرًا بما يهدّد حدّ المكدّس.

استخدم التخزين الساكن حين:

  • ينبغي وجود نسخة واحدة بالضبط طوال عمر البرنامج،
  • وتكون مشاركتها بين كل المستدعين صحيحة فعلًا.

والافتراضي هو المكدّس. والجأ إلى الكومة حين ينطبق أحد أسبابها الثلاثة، وحين تفعل، اتّبع قواعد الملكية في تسريبات الذاكرة ليُحرَّر مع ذلك ما كسبت له عمرًا أطول.

خطآن متعاكسان كالمرآة

يستحقّان التسمية معًا، لأنهما سؤال العمر نفسه مُجابًا بطريقتين:

  • المؤشّر المعلّق - ماتت الذاكرة قبل المؤشّر. كإرجاع &local، أو استخدام مؤشّر بعد free. فيقرأ البرنامج أو يكتب تخزينًا صار ملكًا لشيء آخر.
  • تسريب الذاكرة - مات المؤشّر قبل الذاكرة. كفقدان آخر مرجع إلى كتلة malloc. فلا ينكسر شيء فورًا؛ بل تنمو العملية فحسب.

وكلاهما يأتي من عدم تطابق بين المدة التي يجب أن تعيشها البيانات والمنطقة التي وضعتها فيها. قرّر العمر أولًا، فتتبعه المنطقة.

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

ما الفرق بين المكدّس والكومة في لغة C؟

يحمل المكدّس المتغيّرات المحلّية: يحدّد المترجم أحجامها، وتُنشأ عند دخول الدالة وتُدمَّر عند عودتها، ولا يكلّف التخصيص شيئًا. وتحمل الكومة كتل malloc: تختار الحجم في وقت التشغيل، وتبقى الكتلة حتى تنفّذ free، وللتخصيص تكلفة حقيقية.

لماذا لا أستطيع إرجاع مؤشّر إلى متغيّر محلّي في لغة C؟

لأن تخزين المتغيّر المحلّي يُحرَّر لحظة عودة الدالة. وما يزال المؤشّر يحمل ذلك العنوان، لكن الذاكرة صارت الآن ملك استدعاء الدالة التالي - فقراءتها تعطي قمامة، والكتابة فيها تفسد بيانات غير ذات صلة. وذلك مؤشّر معلّق. أرجِع كتلة malloc، أو دع المستدعي يوفّر المخزن.

ما حجم المكدّس في لغة C؟

عادةً من 1 إلى 8 ميغابايت للخيط الرئيسي، وأقلّ بكثير للخيوط الإضافية - أي صغير بما يكفي لأن يجعل int big[1000000]; كمتغيّر محلّي يعطّل البرنامج عند الدخول عادةً. أما الكومة فمحدودة بذاكرة النظام المتاحة، فالبيانات الكبيرة أو مجهولة الحجم تنتمي إليها.

متى أستخدم الكومة بدل المكدّس في لغة C؟

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

Coddy programming languages illustration

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

ابدأ الآن