الجزء 2 :¦- دورة تعليم الـC -¦: الدرس 8 -¦: الحجز الديناميكي للذاكرة
آخر
الصفحة
EdyBel

  • المشاركات: 46565
    نقاط التميز: 15853
عضوة أساسية
EdyBel

عضوة أساسية
المشاركات: 46565
نقاط التميز: 15853
معدل المشاركات يوميا: 7.4
الأيام منذ الإنضمام: 6283
  • 00:26 - 2014/07/08

الحجز الديناميكي للذاكرة
Allocation dynamique de la mémoire
Dynamic  memory allocation

 
 
كل المتغيّرات التي تعاملنا معها لحد الآن، تمّ إنشاؤها أوتوماتيكيا من طرف المترجم Compiler، لقد كانت طريقة سهلة و لهذا سنتعلّم في هذا الدرس طريقة نوعاً ما يدوية ( أقصد manual ) لإنشاء متغيّرات، هذا ما نسمّيه في لغة السي بالحجز الديناميكي للذاكرة.
 
من بين فوائد الحجز الديناميكي هو أنه يسمح لنا بتعريف جدول بحجم لا يعرفه المترجم قبل بداية الترجمة. سابقاً، كان حجم الجدول ثابتاً في قيمة واحدة طول مدّة حياة الكود، لكننا بعد تعلّم المفاهيم التي سأطرحها في هذا الدرس، سنتعلّم كيف نعرّف جداول بطريقة أكثر مرونة .
 
إنه من اللازم و الضروري أن تتقنوا التعامل مع المؤشرات pointeurs لتتمكّنوا من فهم هذا الدرس! فإن كنتم غير متأكدّين من معلوماتكم حولها، أعيدوا قراءة الدرس المتعلق بالمؤشرات .
 
عندما نقوم بتعريف ( أو إطلاق ) متغيّرة، فإننا نقول أننا : نحجز مكاناً في الذاكرة .
 
int monNombre = 0;
 
عندما يصل المترجم إلى سطر مشابه للسطر السابق، يقوم بالتالي:
- يقوم البرنامج بطلب إذن من نظام التشغيل ليحجز مكانا في الذاكرة .
- يستجيب نظام التشغيل، و ذلك بإعطاء البرنامج عنوان الخانة التي حجزها له بالذاكرة.
- عندما ينتهي البرنامج، و يصبح ليس بحاجة إلى الخانة، سيتم حذف هذه الأخيرة أوتوماتيكياً من الذاكرة ( الواقع أنها لا تحذف و إنما تصبح حرّة لاستعمالات أخرى )
- هذا تقريباً ما يحصل، أي نظام التشغيل هو المسؤول عن التحكم في الذاكرة .
 
لحد الآن كل الأمور كانت أوتوماتيكية، عندما نقوم بتعريف متغيرة فإنه سيتم استدعاء نظام التشغيل تلقائياً. ما رأيكم إذا بفعل هذا بطريقة يدوية؟ ليس لأننا نريد أن نستمتع أو لنضيّع الوقت بل لأننا أحيانا نظطرّ لذلك! في هذا الدرس سنقوم بـ:
- دراسة كيف تعمل الذاكرة ( مرّة أخرى ) ، لنعرف ما الحجم الذي تحجزه كل متغيّرة حسب نمطها .
- و لكي نهجم على الدرس، سنتعلّم كيف نطلب نحن من نظام التشغيل أن يحجز لنا مكان في الذاكرة. هذا ما سنسميه الحجز الديناميكي للذاكرة !
- و أخيراً، سنتعرّف على الفائدة من حجز الخانات في الذاكرة بطريقة ديناميكية، و ذلك بتعريف جدول ذو حجم غير معروف قبل بداية الترجمة .
 
 حجم المتغيرات :
 
على حسب نمط المتغيرة التي نريد تعريفها (int، char، float، أو حتى long ) فنحن نحتاج إلى حجم معيّن من الذاكرة .
بالفعل فإن أردنا تخزين رقم من -128 إلى 127 (نمط char) لن نحتاج إلا إلى 1octet من الذاكرة، و هذا حجم صغير للغاية . بالنسبة للنمط int، فهو يحجز حوالي 4octets في الذاكرة، بينما النمط double يحجز 8octets . المشكل هو أن هذه القيم تتغيّر من حاسوب إلى آخر، فقد نجد منكم من عنده، النمط int يحجز 8octets ، من يعلم ؟ هدفنا هنا أن نتعرّف كم يحجز كلّ نمط من حجم في ذاكرة حاسوبكم. هذا أمر بسيط، لأننا سنستعمل العامل ()sizeof .
على عكس الظاهر، فإن ()sizeof ليست بدالة، بل عبارة عن إحدى الوظائف الأساسية من لغة السي، يكفي أن تضعوا بين قوسين اسم النمط الذي تريدون معرفة الحجم الذي يحجزه بالذاكرة. مثلا، لو أردنا معرفة حجم عدد طبيعي int، نكتب التالي :
 
sizeof(int)
 
أثناء الترجمة، سيتم استبدال هذا الكود بالرقم الذي يمثل الحجم الذي يحجزه int في الذاكرة . بالنسبة لي، النمط int يحجز 4octets. و على الأرجح ستكون نفس القيمة بالنسبة لحاسوبكم، على أية حال فإنها ليست قاعدة. يمكنكم اختبار القيمة التي يرجعها العامل  ()sizeof على طريق إظهارها على الشاشة :
 
printf("char : %d octets\n", sizeof(char));
printf("int : %d octets\n", sizeof(int));
printf("long : %d octets\n", sizeof(long));
printf("double : %d octets\n", sizeof(double))
 
بالنسبة لي ، هذا ما سيظهر في الشاشة :
 
char : 1 octets
int : 4 octets
long : 4 octets
double : 8 octets
 
لم أختبر كل الأنماط الموجودة، يمكنكم فعل ذلك بأنفسكم من باب التجربة و الفضول.
 
أنتم تلاحظون أن النمطين int و long يحجزان نفس الحجم من الذاكرة، أي أنه بإمكاننا القول أن تعريف متغيرة من نمط long يعود إلى تعريف متغيّرة من نمط int. في الواقع النمط long هو مكافئ إلى نمط نسميه long int، و الذي هو مكافئ للـint نفسه، و لكنه يسمح بتخزين عدد طبيعي أكبر من الذي يخزّنه الـint، على أي حال فإن كلّ هذه الأنماط بأهداف متشابهة، كنا نستعملها في الوقت الذي كانت حواسيبنا تحمل ذاكرة ضعيفة، فكنا نشحّ على كل خانة نحجزها، لكن الآن حواسيبنا تحمل مواصفات فائقة و هي ليست بحاجة لكلّ هذه التعقيدات، فهل تعرفون مثلا، كم من الممكن أن تخزّن ذاكرة حاسوبكم من رقم int؟!!!!!
 
هل بإمكاننا أن نـُظهر حجم متغيّرة قمنا نحن بإنشائها ؟ ( هيكل مثلاُ )
نعم ! فالـ()sizeof تعمل حتى مع الهياكل !
 
typedef struct Coordonnees Coordonnees;
struct Coordonnees
{
    int x;
    int y;
};
int main(int argc, char *argv[])
{
    printf("Coordonnees : %d octets\n", sizeof(Coordonnees));
    return 0;
}
 
النتيجة :
 
Coordonnees : 8 octets
 
كل ما احتوى الهيكل من متغيّرات داخلة كلّما كبـُر حجمه، أليس أمراً منطقياً؟
 
طريقة أخرى لننظر بها إلى الذاكرة :
لحد الآن، كل المخططات التي قدّمتها لكم عن الذاكرة، لم تكن دقيقة، سنرجعها أصحّ و أدقّ بما أننا تعلّمنا الآن كم يأخذ كل نمط من حجم بالذاكرة .
لو نعرّف متغيرة من نمط int:
 
int nombre = 18;
 
و مثلا ()sizeof تعطينا الرقم 4 ، أي أن هذه المتغيرة تحجز 4octets في الذاكرة ! لنفترض أن المتغيرة nombre محجوزة بالعنوان رقم 1600 من الذاكرة، سيكون لدينا إذا المخطط التالي للذاكرة :
 


الآن يمكننا فعلاً أن نرى بأن المتغيرة nombre ذات القيمة 18 تحجز 4octets من الذاكرة، فهي تبدأ من العنوان رقم 1600 و تنتهي عند العنوان رقم 1603، المتغيرة القادمة، لن يتم حجزها إلا إبتداءاً من الخانة ذات العنوان 1604!

لو جربنا نفس الشيئ مع النمط char، فالمتغيرة لن تأخذ سوى octect واحد في الذاكرة :

 

 

تخيّلوا الآن جدول من نمط int! كل خانة من الجدول ستحجز 4octets ، و لو أن الجدول مثلاُ يحتوي 100 خانة :

int tableau[100];
 
سنحجز إذا 100*4=400 octets في الذاكرة .
 
ماذا لو كان الجدول فارغاً، هل سيحجز 400octets ؟

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

 

Coordonnees tableau[100];
 
8*100=800 octets محجوزة في الذاكرة .
 
من المهمّ فهم هذه الحسابات البسيطة لنواصل مع الدرس.
 
الحجز الديناميكي للذاكرة :
فلندخل بسرعة إلى صلب الموضوع، سأذكّركم بهدف الدرس : طلب حجز الخانات يدوياً .
سنحتاج إلى استيراد المكتبة stdlib.h، لو كنتم قد اتّبعتم نصائحي، لكنتم قد استوردتموها في كلّ برامجكم السابقة، على أي حال فهذه المكتبة تحتوي على دالّتين سنحتاج إليهما في هذا الدرس :
- malloc ( أي memory allocation أو allocation de la mémoire) : تطلب الإذن من نظام التشغيل لحجز مكان في الذاكرة.
- free : ( تحرير أو libérer ) : هذه تقول لنظام التشغيل بأننا لم نعد بحاجة إلى ذلك المكان المحجوز، و لذلك بالإمكان تحريره لاستعمالات أخرى ببرنامج آخر مثلا.
 
عندما تقومون بحجز مكان في الذاكرة بطريقة ديناميكية، فأنتم بحاجة إلى اتباع الخطوات التالية :
- مناداة الدالة malloc من أجل حجز الخانات.
- اختبار القيمة التي تم ارجاعها من طرف malloc لنرى ما إن تمكّن نظام التشغيل من القيام بذلك.
- ما إن ننتهي من العمل بالمتغيرة التي حجزنا لها مكانا بالذاكرة، يجب علينا تحرير هذه الأخيرة باستعمال free، فإن لم نفعل، فيمكن للبرنامج أن يحجز مكانا ضخما بالذاكرة، هو ليس بحاجة إليه.
 
يجدر بهذه الخطوات الثلاث ان تذكّركم بدرس الملفات، فالمبدئ واحد : حجز، اختبار، عمل، تحرير .
 
malloc، لنطلب الإذن لحجز الذاكرة :
فلنلقي نظرة على نموذج الدالة malloc:
 
void* malloc(size_t nombreOctetsNecessaires);
 
 لدالة تحتاج إلى خاصية : عدد الـoctets الذي يجب أن تحجزه، هنا يكفي أن نكتب (sizeof (int إن أردنا حجز مكان من أجل عدد طبيعي .
الشيئ الذي يثير الفضول، هو القيمة التي تقوم بإرجاعها الدالة، أي *void !!! لو تتذكّرون درس الدوال، كنت قد قلت لكم بأن الكلمة void تعني (فارغ) أو (vide) و نستعملها لنشير أن الدالة لا تـٌرجع إلينا أية قيمة .
هل نفهم بهذه الكتابة، أن هذه الدالة تُرجع إلينا مؤشّراً نحو الفراغ؟ يبدو الأمر غريباً . في الحقيقة، إن هذه الدالة ترجع إلينا مؤشراً نحو الخانة التي حجزها نظام التشغيل من أجلنا، فإن استطاع حجز مكان ( أي أن هناك خانات شاغرة ) سيُرجع إلينا عنوان الخانة التي حجزها أي 1600.
المشكل هو أن الدالة malloc لا تعرف نمط المتغيرة التي نريد أن نحجز لها الذاكرة، فإن أعطيتم لها الرقم 4 كخاصية، فهذا يمكن له أن يعني حجم int أو long! لهذا السبب فإن الدالة malloc تقوم بإرجاع النمط *void ، لنسميه مؤشّرا نحو نمط أياً كان.
 
لننتقل إلى التطبيق:
إذا كنت أريد الاستمتاع بحجز الذاكرة من أجل متغيرة من نمط int مثلاً، يجب أن أشير للـmalloc أنني أحتاج إلى (sizeof (int خانات في الذاكرة. و أسترجع القيمة التي تعطيها لي الدالة في مؤشر نحو int :
 
void* malloc(size_t nombreOctetsNecessaires);
 
في نهاية هذا الكود، memoireAllouee هو مؤشّر يحتوي على عنوان أول خانة من الخانات التي تم حجزها للمتغيرة. لنقل مثلا القيمة 1600 ( المثال السابق ) .
 
اختبار المؤشّر :
 
الدالة malloc قامت بإرجاع قيمة استعدناها في المتغيرة memoireAllouee، عنوان الخانة التي تم حجزها بالذاكرة، هناك احتمالين :
- إذا تم الحجز بنجاح، المؤشّر سيحتوي عنوان الخانة .
- في الحالة الأخرى، سيتم إرجاع NULL.
 
إنه من النادر أن تفشل عملية حجز الذاكرة، فتخيّلوا أنكم تودّون حجز 34 Go من الذاكرة العشوائية، في هذه الحالة، قد تفشل عملية الحجز .
و لذلك من المستحسن دائماً أن نختبر القيمة المرجعة من الدالة malloc للتأكد ما إن تمت العملية بنجاح. و لنتعبر أنها لن تفشل إلا في حالة ما إن لم تكن المساحة الحرّة من الذاكرة العشوائية كافية. في هذه الحالة، يجب أن نوقف البرنامج، على أي حال فبهذه الوضعية لن يكون قادراً على الاشتغال.
 
و لهذا سنستعمل دالة لم يسبق لنا أن استعملناها و هي ()exit ، هذه الأخيرة توقف البرنامج بشكل نهائي. و هو سيأخذ خاصية، الرقم الذي يجب إرجاعه ( نفس المبدئ بالنسبة للـreturn الخاص بالـmain ).
 
int main(int argc, char *argv[])
{
    int* memoireAllouee = NULL;
    memoireAllouee = malloc(sizeof(int));
    if (memoireAllouee == NULL) // Si l'allocation a échoué
    {
        exit(0); // On arrête immédiatement le programme
    }
    // On peut continuer le programme normalement sinon
    return 0;
}
 
إذا كان المؤشر مختلف عن الصفر، يمكن للبرنامج أن يواصل العمل، و إلا سيظهر رسالة خطأ بالشاشة ثم يتوقف عن العمل مـُجبراً باستعمال exit لأن مساحة الذاكرة المتوفّرة لا تسمح له بأن يشتغل بشكل صحيح..
 
free، لتحرير الذاكرة :
 
مثلما استعملنا الدالة fclose في درس الملفّات لنغلق ملفاً، سنستعمل الدالة ()free من أجل تحرير الخانات التي لم نعد بحاجة إليها من الذاكرة .
 
void free(void* pointeur);
 
 
 الدالة free ليس بحاجة إلا أن نشير إلى عنوان الخانة التي نريد تحريرها من الذاكرة، سنعطيها حسب مثالنا السابق المؤشر memoireAllouee.
الكود الأخير، مشابه بشكل كبير للكود الأخير الذي درسناه مع الملفات :
 
int main(int argc, char *argv[])
{
    int* memoireAllouee = NULL;
    memoireAllouee = malloc(sizeof(int));
    if (memoireAllouee == NULL) // On vérifie si la mémoire a été allouée
    {
        exit(0); // Erreur : on arrête tout !
    }
    // On peut utiliser ici la mémoire
    free(memoireAllouee); // On n'a plus besoin de la mémoire, on
la libère
    return 0;
}
 
مثال للفهم :
 
سنستعمل لهذا مثالاُ سهلاً تعوّدنا عليه، و هو أن نطلب من المستخدم تزويدنا بعُمره بينما نقوم بتخزينه في خانة بالذاكرة . الشيء الذي تغيّر هو أنه سابقاً كل الأمور كانت أوتوماتيكية، أما هذه المرة فستكون ديناميكية أي أكثر تعقيداً، لكنها في النهاية سهلة بالتدرّب :
 
int main(int argc, char *argv[])
{
    int* memoireAllouee = NULL;
    memoireAllouee = malloc(sizeof(int)); // Allocation de la mémoire
    if (memoireAllouee == NULL)
    {
        exit(0);
    }
    // Utilisation de la mémoire
    printf("Quel age avez-vous ? ");
    scanf("%d", memoireAllouee);
    printf("Vous avez %d ans\n", *memoireAllouee);
    free(memoireAllouee); // Libération de mémoire
    return 0;
}
 
النتيجة :
 
Quel age avez-vous ? 31
Vous avez 31 ans
 
إحذروا : بما أن memoireAllouee هي مؤشّر، نحن لا نستعملها بنفس الطريقة التي نستعمل بها متغيّرة عادية، أي أنه لنتحصّل على قيمة المتغيرة التي يشير إليها المؤشّر نقوم بوضع نجمة قبل اسم المؤشّر( لاحظوا الدالة printf )، بينما إن أردنا الحصول على عنوان المتغيّرة يكفي أن نكتب مباشرة اسم المؤشّر ( أي اننا لا نسبق اسمه بإشارة & ) لاحظوا ذلك في الدالة scanf . كلّ هذا تم شرحه في درس المؤشّرات،  و لكن أعرف أن الكثيرين من سيتأخرون في التعوّد على الفرق بين استعمال مؤشّر و استعمال متغيرة عادية، لكن هذا ليس بمشكل تماما، مثلما قلت التدريب ثم التدريب و لا ضير في إعادة قراءة درس المؤشرات.
 
لنعد إلى الكود، لقد قمنا بحجز ديناميكي لخانة من أجل  تخزين int . و الأمر مكافئ بالظبط للكتابة التقليدية :
 
int main(int argc, char *argv[])
{
    int maVariable = 0; // Allocation de la mémoire (automatique) 
   // Utilisation de la mémoire
    printf("Quel age avez-vous ? ");
    scanf("%d", &maVariable);
    printf("Vous avez %d ans\n", maVariable);
    return 0;
} // Libération de la mémoire (automatique à la fin de la fonction)
 
النتيجة :
 
 
Quel age avez-vous ? 31
Vous avez 31 ans
 
كملخص، لدينا طريقتين لإنشاء متغيرة، أي لحجز مكان في الذاكرة :
- طريقة أوتوماتيكية: و هي الطريقة التقليدية التي نستعمها في غالب الأحيان .
- طريقة ديناميكية أو يدوية و هو أن نحجز بأنفسنا المكان المخصص لتخزين قيمة المتغيرة.
 
هل تعتقدون أن الطريقة الديناميكية طويلة و بلا فائدة ؟
ربما تكون أكثر تعقيداً لكنها تعود إلينا بالفائدة بدون أدنى شك، و أحيانا نكون مجبورين لاستعمالها و هذا ما سنراه الآن .
 
 
حجز الذاكرة ديناميكيا من أجل جدول :
لحد الآن كان استعمال الطريقة الديناميكية في مثال لا يتطلّب منا فعل ذلك، فما الفائدة الفعلية للحجز الديناميكي؟
إننا نحتاج بشكل كبيرا لحجز الديناميكي من أجل تعريف جداول. للنتخيل مثلا برنامجاً يقوم بتخزين اعمار أصدقائك في جدول، يمكنكم فعل ذلك الطريقة التالية :
 
 
int ageAmis[15]
 
لكن من قال أنه لديك 15 صديق؟ ربما لديك أكثر !
نحن لا يمكننا تكهن حجم الجدول حتى يأتي وقت تشغيل البرنامج، عندما نطلب من المستعمل إدخال حجم الجدول، و هنا نكتشف فائدة الحجز الديناميكي : تطلبون من المستخدم إدخال حجم الجدول، تسترجعون الحجم، ثم تعرّفون الجدول على حسب الحجم، أي أنه لن تكون هناك خانات بالنقصان، و لا بالزيادة أيضاً .
 
كنت قد تطرقت من قبل و قلت أن هذا النوع من الكتابة غير مسموح به في السي :
 
int amis[nombreDAmis];
 
الكود السابق قد يعمل مع بعض برامج الترجمة، لكن ليس كلها و هذا أمر غير منصوح باستعماله على أي حال.
 
هنا سيتدخل الحجز الديناميكي، ليمكننا من حجز nombreDAmis عدد خانات . أي أننا سنحجز  (nombreDAmis * sizeof(int أوكتي octets في الذاكرة :
 
amis = malloc(nombreDAmis * sizeof(int));
 
هذا الكود يسمح بإنشاء جدول يحتوي على nombreDAmis عدد خانات بالظبط .
هذا ما يقوم به الكود بالترتيب :
- نطلب من المستخدم كم لديه من صديق.
- إنشاء جدول يحتوي عدد خانات بالحجم الذي أدخلع المستخدم .
- يطلب كم عمر الأصدقاء، واحداً واحداً ثم نقوم بتخزينها في الجدول.
- نظهر محتوى الجدول لنتأكد بأن التخزين تم بشكل صحيح.
- في النهاية، و بما أننا لنا بحاجة إلى المكان الذي يحجزه الجدول بالذاكرة، نقوم بتحرير هذه الأخيرة.
 
int main(int argc, char *argv[])
{
    int nombreDAmis = 0, i = 0;
    int* ageAmis = NULL; // Ce pointeur va servir de tableau après l'appel du malloc
    // On demande le nombre d'amis à l'utilisateur
    printf("Combien d'amis avez-vous ? ");
    scanf("%d", &nombreDAmis);
    if (nombreDAmis > 0) // Il faut qu'il ait au moins un ami (je le plains un peu sinon :p)
    {
        ageAmis = malloc(nombreDAmis * sizeof(int)); // On alloue de la mémoire pour le tableau
        if (ageAmis == NULL) // On vérifie si l'allocation a marché ou non
{
            exit(0); // On arrête tout
        }
        // On demande l'âge des amis un à un
        for (i = 0 ; i < nombreDAmis ; i++)
        {
            printf("Quel age a l'ami numero %d ? ", i + 1);
            scanf("%d", &ageAmis[i]);
        }
        // On affiche les âges stockés un à un
        printf("\n\nVos amis ont les ages suivants :\n");
        for (i = 0 ; i < nombreDAmis ; i++)
        {
            printf("%d ans\n", ageAmis[i]);
        }
        // On libère la mémoire allouée avec malloc, on n'en a plus besoin
        free(ageAmis);
    }
    return 0;
}
 
النتيجة :
 
Combien d'amis avez-vous ? 5
Quel age a l'ami numero 1 ? 16
Quel age a l'ami numero 2 ? 18
Quel age a l'ami numero 3 ? 20
Quel age a l'ami numero 4 ? 26
Quel age a l'ami numero 5 ? 27
Vos amis ont les ages suivants :
16 ans
18 ans
20 ans
26 ans
27 ans
 
قد يكون هذا البرنامج سهلاً و غير مفيد، لكني اتخذته كمثال للفهم المبسّط  . لكن إعلموا أنه من الآن وصاعداً سنستعمل الحجز الديناميكي بكثرة و في أمور أكثر جدية.
 
ملخص:
- كل متغيرة، بتغيّر نمطها، تحجز مكانا مختلفا في الذاكرة.
- يمكننا أن نعرف عدد الـoctets التي يحجزها كل نمط باستعمال العامل ()sizeof .
- الحجز الديناميكي هو عبارة عن حجز يدوي لمكان في الذاكرة من أجل متغيرة أو من أجل جدول.
- الحجز الديناميكي يتم باستعمال الدالة ()malloc، حينما لا نكون بحاجة إلى الخانات المحجوزة، نحرر الذاكرة باستعمال الدالة ()free .
- يسمح لنا الحجز الديناميكي بتعريف جدول بحجم غير معروف إلى حين تشغيل البرنامج .
 الجزء 2 :¦- دورة تعليم الـC -¦: الدرس 8 -¦: الحجز الديناميكي للذاكرة
بداية
الصفحة