التعدادات
;1 typedef enum Volume Volume
2 enum Volume
{3
4 LOW, MEDIUM, HIGH
;} 5
تلاحظ أننا نستعمل typedefهنا أيضا ،مثلما رأينا لحد الآن.
لـكي نقوم بتعر يف تعداد نستعمل الكلمة المفتاحية . enumاسم التعداد هنا هو . Volumeإن ّه نوع
مخ ّصص قمنا بتعر يفه يمكن له أن يأخذ واحدة من الثلاث قيم ال ّتي وضعناها :إما LOWأو MEDIUMأو
. HIGH
يمكننا إذن أن نعر ّف متغيرا اسمه musicمن نوع Volumeلتخزين مستوى صوت الموسيقى.
يمكننا تهيئة الموسيقى على المستوى : MEDIUM
;1 Volume music = MEDIUM
يمكننا لاحقا ًفي الشفرة ،أن نغيّر قيمة مستوى الصوت و وضعها إمّا على HIGHأو على . LOW
ارفاق قيم التعداد بأعداد
قد لاحظت أن ّي كتبت القيم الممكنة بأحرف كبيرة .هذا يفترض به أن يذكّرك بالثوابت و ، #defineأليس
كذلك ؟
في الواقع ،إ ّن هذا مشابه كثيرا و لـكن ّه ليس نفس الشيء.
المترجم يقوم تلقائيا بارفاق قيم التعداد بأعداد موافقة لها.
في حالة تعدادنا LOW ، Volumeسيتم ارفاقها بالقيمة MEDIUM ،0بالقيمة ،1و HIGHبالقيمة .2
الإرفاق يتم ّ تلقائيّا انطلاقا من .0
خلافا لـ ، #defineفالمترجم هو من يرفق MEDIUMبـ 1مثلا ،وليس المعالج القبلي .لـك ّن هذا سيكون
تقريبا مكافئا له.
بطبيعة الحال ،عندما هي ّئنا المتغيّر musicعلى ، MEDIUMفإنّنا قد وضعنا القيمة 1في خانة الذاكرة الموافقة.
عملي ّا ،هل يهمّنا أن نعرف أ ّن MEDIUMتساوي HIGH ،تساوي ،إلخ .؟
لا .فهذا حقيقة لا يعنينا .المترجم هو من سيقوم تلقائيّا بإرفاق العدد المناسب إلى ك ّل قيمة .بفضل هذا،
ليس عليك سوى كتابة :
201
الفصل ب .6.أنشئ أنواع متغيرات خا ّصة بك
)1 if (music == MEDIUM
{2
3 // Play music with medium volume
}4
لا يه ّم ما هي قيمة ، MEDIUMستترك المترجم يهتم ّ بالأعداد.
الفائدة من ك ّل هذا ؟ هي أنها تجعل الشفرة قابلة للقراءة جي ّدا .في الواقع ،أ ّي شخص يمكنه بسهولة قراءة
ifالسابق )نفهم جي ّدا أ ّن الشرط يعني ”إن كانت الموسيقى بمستوى صوت متو ّسط”(.
ارفاق قيمة محددة
حالي ّا ،كان المترجم هو من يقر ّر إرفاق العدد 0ثم 1،2،3بالترتيب.
من الممكن طلب إرفاق قيمة مح ّددة لك ّل عنصر من التعداد.
ما الفائدة التي يمكن تحصيلها من هذا ؟ حسنا فلنفرض أن ّه في حاسوبك ،الصوت يتم تحديده بقيمة بين 0
و = 0) 100لا صوت 100% = 100 ،من الصوت( ،فسيكون من الجيد ارفاق قيمة مح ّددة بك ّل عنصر :
;1 typedef enum Volume Volume
2 enum Volume
{3
4 LOW = 10, MEDIUM = 50, HIGH = 100
;} 5
هنا ،المستوى LOWيوافق 10%من المستوى ،المستوى MEDIUMيوافق ،50%إلخ.
يمكننا بسهولة إضافة بعض القيم الأخر مثل . MUTEنرفق في هذه الحالة MUTEبالقيمة ! 0 ...لقد فهمت.
مل ّخص
• الهيكل هو نوع بيانات مخ ّصص يمكنك إنشائه و استخدامه في برامجك .يجب عليك تعر يفه ،عكس الأنواع
القاعدي ّة مثل intو doubleال ّتي نجدها في ك ّل البرامج.
• الهيكل يتكوّن من ”متغيّرات داخلي ّة” تكون عادة من أنواع قاعدي ّة مثل intو ، doubleو أيضا
من الجداول.
• نستطيع الوصول إلى أحد مركّبات الهيكل بفصل اسم المتغيّر و المر ّكب بنقطة . player.firstName :
• إذا كن ّا نتعامل مع مؤشر نحو هيكل و أردنا الوصول إلى أحد مركّباته ،نستخدم السهم بدل النقطة :
. playerPointer->firstName
• التعداد هو نوع مخ ّصص يمكنه فقط أخذ إحدى القيم المسبقة التعر يف MEDIUM ، LOW :أو HIGH
مثلا.
202
الفصل ب7.
قراءة و كتابة الملفات
المشكل مع استعمال المتغيّرات ،هو أنها موجودة فقط في الذاكرة العشوائية .RAMبخروجنا من البرنامج،
ك ّل المتغيّرات يتم حذفها من الذاكرة و لن يصبح ممكنا إستعادة قيمها .كيف يمكننا إذن أن نحتفظ بأحسن
العلامات التي تح ّصلنا عليها في لعبة ؟ كيف يمكننا إنشاء محرر نصوص إذا كان ك ّل الن ّص المكتوب يختفي بمجر ّد
إيقاف البرنامج ؟
لحسن الح ّظ يمكننا القراءة من الملفا ّت و كذا الكتابة فيها في لغة .Cهذه المل ّفات ُمخز ّنة في القرص الصلب
) (Hard diskالخاص بالحاسوب :الشيء الإيجابيّ إذن هو أنها تبقى محفوظة ،حت ّى عند إيقاف البرنامج أو
الحاسوب.
للقراءة من الملفات و الكتابة فيها ،سنحتاج إلى استعمال ك ّل ما درسناه حت ّى الآن :المؤشرات ،الهياكل،
السلاسل المحرفي ّة ،الخ.
ب 1.7.فتح و غلق ملف
للقراءة و الكتابة في المل ّفات ،سنستعمل دوالا ًمعر ّفة في المكتبة stdioالتي استعملناها سابقا ً.
نعم ،هذه المكتبة تحتوي على الدالتين scanfو printfاللتان نعرفهما جي ّدا ! لـكن ليس هذا فحسب :
يوجد بها الـكثير من الدوال الأخرى ،خصوصا التي تعمل على الملفات.
كل المكتبات التي استعملناها حت ّى الآن ) ، math.h ، stdio.h ، stdlib.h
(... string.hتش ّكل ما نسميه بالمكتبات القياسية ) ،(standard librariesو هي مكتبات تأتي
تلقائيا مع البيئة التطوير ية التي تستخدمها و لديها الميزة في أ ّنها تعمل على كل أنظمة التشغيل .بالإمكان
استعمالها في أ ّي مكان ،سواء كنت في ،Windowsأو GNU/Linuxأو Macأو غير ذلك .المكتبات
القياسي ّة ليست كثيرة و لا تم ّكننا من القيام بأكثر من بعض الأمور الأساسي ّة ،كما فعلنا لغاية الآن.
للحصول على وظائف أكثر تق ّدما ،كفتح النوافذ ،يجب تحميل و ٺثبيت مكتبات جديدة .سنرى ذلك
قريبا !
203
الفصل ب .7.قراءة و كتابة الملفات
تأكّد إذن ،للبدأ ،أن تقوم بتضمين المكتبتين stdio.hو stdlib.hعلى الأقل أعلى ملفكم : .c
>1 #include <stdlib.h
>2 #include <stdio.h
هاتان المكتبتان ضروريتان و أساسي ّتان لدرجة أن ّي أنصحك بتضمينهما في ك ّل البرامج التي تكتبها في المستقبل،
أيّا كانت.
حسنا ًو بعدما قمنا بتضمين المكتبتين ،يمكننا أن ننطلق في بالأمور الج ّدي ّة .إليك الخطوات التي يجب إتّباعها
دائما ًحينما تريد العمل على ملف ،سواء للقراءة منه أو للكتابة فيه :
• نقوم بمناداة دالة فتح الملف fopenالتي تقوم بإرجاع مؤش ّر نحو هذا الملف.
• نتأكّد من نجاح عملي ّة الفتح )أي إن كان المل ّف موجودا( باختبار قيمة المؤشر الذي أرجعته الدالة .فإن
كان المؤشر يساوي ، NULLفهذا يعني أ ّن فتح الملف لم ينجح ،في هذه الحالة لا يمكننا الإكمال )يجب
أن نظهر رسالة خطا(.
• إذا تم الفتح بنجاح )أي أن قيمة المؤشر تختلف عن ،( NULLسنستمتع بالكتابة على الملف أو القراءة
منه ،و ذلك باستخدام دوال سنراها لاحقا ً.
• بمجر ّد أن ننهي العمل على الملف ،يجب تذكّر ”غلقه” باستعمال الدالة . fclose
سنتعلّم كخطوة أولى كيف نستخدم fopenو ، fcloseحينما ٺتعلّم هذا ،سنتعلّم كيف نقرأ محتواه و نكتب
ن ّصا فيه.
: fopenفتح ملف
في فصل السلاسل المحرفي ّة ،كنا نستعين بنماذج الدوال مثل ”دليل استخدام” .هذا ما يفعله المبرمجون غالبا :
يقرؤون نموذج دالة و يفهمون كيف يستخدمونها .مع ذلك ،أعلم أنّنا بحاجة إلى بعض الشروحات البسيطة !
لهذا فلنرى قليلا ًنموذج : fopen
;)1 FILE* fopen(const char* fileName, const char* openMode
هذه الدالة تنتظر معاملين :
• اسم الملف الذي نريد فتحه.
• أسلوب فتح الملف ،أي دلالة تذكر ما ال ّذي تريد فعله :القراءة من الملف ،أو الكتابة فيه ،أو كليهما.
204
فتح و غلق ملف
هذه الدالة ترجع ...مؤش ّرا على ! FILEإن ّه مؤش ّر على هيكل من نوع . FILEهذا الهيكل متواجد في
المكتبة . stdio.hيمكنك فتح الملف لترى مما يتكوّن النوع ، FILEلـكن هذا ليس ما يهمّنا.
لـكن ل ِم َ اسم الهيكل كله بأحرف كبيرة؟ اعتقدت أن الأسماء بالأحرف الـكبيرة حجزناها للثوابت و
لـ #define؟
هذه ”القاعدة” ،أنا من قمت بتحديدها )و كثير من المبرمجـين يتيعونها( ،و لـكنّها لم تكن أبدا مفروضة .و
يبدو أ ّن من برمجوا stdio.hلا يتبعون نفس القواعد !
هذا لا يجب أن يشوّشك كثيرا .سوف ترى أ ّن المكتبات ال ّتي سندرسها لاحقا ٺتب ّع نفس القواعد التي أتّبعها،
أي أن اسم الهيكل يبتدئ فقط بحرف واحد كبير.
لنعد إلى دالتنا ، fopenإنها تقوم بارجاع * . FILEإنه من المهم ج ّدا استرجاع هذا المؤش ّر كي نتم ّكن
لاحقا ًمن القراءة و الكتابة في الملف .و لهذا سنقوم بإنشاء مؤش ّر على ، FILEفي بداية دالتنا ) mainمثلا(
:
1 )][int main(int argc, char *argv
2
{
3 ;FILE* file = NULL
4
;return 0
}5
لقد هي ّأنا المؤش ّر على NULLمن البداية .أذكّرك بأ ّن هذه قاعدة أساسي ّة أن تهي ّأ ك ّل المؤش ّرات على NULL
إ ّن لم تكن لديك قيمة أخرى لإعطائها .إن لم تفعل ذلك ،فأنت تزيد كثيرا خطر وجود أخطاء لاحقا.
إنه ليس ضرور يا ًأن تكتب ، struct FILE* file = NULLلأن منشئي stdio.hقد وضعوا
typedefكما عل ّمتك منذ م ّدة قصيرة .لاحظ أن شكل الهيكل قد يتغيّر من نظام تشغيل إلى آخر
)لا تملك بالضرورة نفس المركّبات في كل الأنظمة( .لهذا فلن نع ّدل محتوى FILEمباشرة )لا نقوم
بـ file.elementمثلا( .بل سنكتفي باستدعاء دوال ،ٺتعامل مع FILEنيابة عنا ً.
الآن سنقوم بمناداة الدالة ، fopenو استرجاع القيمة ال ّتي تعيدها في المؤشر . fileو لـكن قبل هذا
يجب أن اشرح لك كيف تستخدم المعامل الثاني . openModeفي الواقع ،هناك شفرة تد ّل للحاسوب على أنك
تريد أن تفتح الملف بوضع القراءة فقط ،الكتابة فقط أو الاثنين معا ً.
هذه هي أوضاع فتح الملف المختلفة :
• ” : ”rقراءة فقط ) .(read onlyيمكنك قراءة محتوى الملف ،و لـكن لا يمكنك الكتابة فيه .يجب أن
يكون الملف موجودا ً من قبل.
205
الفصل ب .7.قراءة و كتابة الملفات
• ” : ”wكتابة فقط ) .(write onlyيمكنك الكتابة في الملف ،لـكن لا يمكنك قراءة محتواه .إذا لم يكن
الملف موجودا ً من قبل ،فإنه سيتم إنشاؤه.
• ” : ”aإلحاق ) .(appendيمكنك الكتابة في الملف ،إنطلاقا من نهايته .إن لم يكن الملف موجودا ً ،فسيتم
إنشاؤه.
• ” : ”r+قراءة و كتابة ) .(read and writeيمكنك القراءة من الملف و الكتابة فيه .يجب أن يكون
الملف موجودا ً من قبل.
• ” : ”w+قراءة و كتابة مع مسح المحتوى أ ّولا .سيتم تفر يغ الملف من محتواه أولا ً ،ثم بإمكانك الكتابة فيه
و قراءة محتواه بعد ذلك .إن لم يكن الملف موجودا ً من قبل ،سيتم إنشاؤه.
• ” ”a+إلحاق مع القراءة /الكتابة في آخر الملف .يمكنك القراءة و الكتابة إنطلاقا من نهاية الملف .إن لم
يكن موجودا ً ،سيتم إنشاؤه.
لمعلوماتك ،أنا عرضت لك بعضا من أوضاع فتح ملف .في الحقيقة ،يوجد ضعفها ! من أجل كل
وضع رأيناه هنا ،إن أضفت ” ”bبعد المحرف الأول ) ”، ”wb+” ، ”rb+” ، ”ab” ، ”wb” ، ”rb
” ،( ”ab+فإن الملف سيتم فتحه بالوضع الثنائي ) .(binaryهذا وضع خاص قليلا ًفلن ندرسه هنا .في الواقع
وضع النص يخت ّص بتخزين ...النص ،تماما كما يوحي الاسم )فقط المحارف القابلة للعرض( .أما الوضع الثنائي،
يسمح بتخزين المعلومات بايتا بايتا )) (Byte by byteأرقام بشكل أساسي( .هذا مختلف كثيرا .على أي حال
فطر يقة العمل هي تقريبا نفس ال ّتي سنراها هنا.
شخصيا ً ،أستعمل كثيرا ً الأوضاع ) ”r” :قراءة() ”w” ،كتابة() ”r+” ،قراءة و كتابة في آن واحد(.
وضع ” ”w+خطر قليلا ًلأنه يقوم بمسح محتوى الملف مباشرة ،بدون أن يطلب التأكيد قبل القيام بذلك .إن
هذا الوضع ليس مفيدا ً إلا إذا أردنا أن نعيد تهيئة الملف أ ّولا .وضع الإلحاق ) ” ( ”aيمكنه أن يفيد في بعض
الحالات ،إذا كنت تريد إضافة معلومات إلى نهاية الملف.
إن كنت تريد قراءة مل ّف ،فمن المستحسن وضع ” . ”rبالطبع ،الوضع ” ”r+يعمل أيضا ،لـكن
بوضع ” ”rفأنت تضمن أ ّن المل ّف لا يمكن تعديله ،هذا نوع من الحماية.
إن كتبت دالة ً ) loadLevelلتحميل مستوى في لعبة مثلا( ،الوضع ” ”rكا ٍف ،أما إن أردت أن
كتابة دالةٍ ) saveLevelلحفظ المستوى( فستستعمل الوضع ”. ”w
الشفرة التالية ستفتح الملف test.txtفي وضع ”) ”r+قراءة و كتابة( :
)][1 int main(int argc, char *argv
{2
;3 FILE* file = NULL
206
فتح و غلق ملف
4 ;)”file = fopen(”test.txt”, ”r+
5 ;return 0
}6
المؤش ّر fileيصبح إذن مؤشرا ً على الملف . test.txt
أين يجب أن يكون الملف test.txt؟
يجب أن يكون في نفس المجل ّد الذي يتواجد به الملف التنفيذي ) .( .exe
من أجل متطل ّبات هذا الفصل ،أطلب منك أن تقوم بإنشاء ملف test.txtفي نفس المساؤ الذي به
، .exeمثلما أفعل أنا )الشكل الموالي(.
كما ترى فأنا أستعمل حالي ّا بيئة التطوير Code::Blocksالأمر الذي يفسّر وجود ملف المشروع بصيغة
) .cbpفي مكان الصيغة .slnإن كنت تستعمل Visual C++مثلا ً( .باختصار ،الأمر المهم هو أن
برنامجي ) ( tests.exeموجود في نفس مجل ّد الملف الذي نريد قرائته أو كتابته ) .( test.txt
هل يجب أن يكون الملف بصيغة .txt؟
لا ،الأمر يعود إليك في اختيار صيغة الملف عندما تفتحه .أي أنه بإمكانك أن تخـترع صيغتك الخا ّصة
.levelلحفظ مستو يات ألعابك مثلا ً.
هل من الواجب أن يكون الملف الذي نريد فتحه في نفس دليل الملف التنفيذي ؟
لا أيضا .يمكنه أن يكون داخل مجل ّد بذات الدليل :
207
الفصل ب .7.قراءة و كتابة الملفات
;)”1 file = fopen(”directory/test.txt”, ”r+
هنا ،الملف test.txtفي مجل ّد داخليّ اسمه . directoryهذه الطر يقة التي نسميها المسار النسبي
عملي ّة أكثر .هكذا ،يمكن للبرنامج أن يعمل أينما كان مثب ّتا.
من الممكن أيضا فتح مل ّف أينما كان في القرص الصلب .في هذه الحالة يجب كتابة المسار الكامل )ما نسميه
المسار المطلق( :
;)”1 file = fopen(”C:\\Program Files\\Notepad++\\readme.txt”, ”r+
هذه الشفرة تفتح الملف readme.txtالموجود بـ . C:\Program Files\Notepad++
تعمّدت استعمال شرطتبن خلفي ّتين \ كما تلاحظ .في الواقع ،إن كتبت اشارة واحدة ،سيعتقد
الحاسوب أنني أريد أن استخدم رمزا خاصا )مثل الـ \nأو الـ .( \tلكتابة شرطة خلفي ّة في سلسلة،
يجب كتابتها إذن م ّرتين ! هكذا يمكن أن يفهم أن ّك تريد استخدام الرمز \ .
المشكل مع المسارات المطلقة ،هو أنها لا تعمل إلا مع نظام معي ّن ،فهي ليست حل ّا محمولا إذن .أي أنه لو
كنت تعمل على GNU/Linuxلكان عليك كتابة مسار كهذا مثلا :
;)”1 file = fopen(”/home/mateo/directory/readme.txt”, ”r+
لهذا فأنا أنصحك بكتابة مسارات نسبية .لا تستعمل المسارات المطلقة إلا في حالة كان البرنامج مخصص
لنظام تشغيل معي ّن ،ليع ّدل على ملف معي ّن في القرص الصلب.
اختبار فتح ملف
المؤش ّر fileيجب أن يحوي عنوان الهيكل من نوع ، FILEو الذي نستعمله كواصف )(descriptor
للملف .هذا الواصف تم تحميله من أجلك في الذاكرة من طرف الدالة . fopenبعد هذا ،هناك احتمالان :
• إمّا أن تنجح عملية الفتح ،فسنتمكن من المواصلة )أي البدء في القراءة و الكتابة في الملف(.
• إمّا ألّا تنجح لأن الملف ليس موجودا ً أو أنه مستخدم من طرف برنامج آخر .في هذه الحالة ،سنتوقف عن
العمل على الملف.
مباشرة بعد فتح الملف ،يجب التأكد ما إن تمت العملية بنجاح ،أم لا .هذا أمر بسيط :إذا كانت قيمة
المؤشر تساوي ، NULLفإن الفتح قد فشل .إن كانت قيمته تساوي شيئا غير ، NULLفقد تم الفتح بنجاح.
سنتبع إذن هذا المخطط التالي :
208
فتح و غلق ملف
1 )][int main(int argc, char *argv
2
{
;3 FILE* file = NULL
;)”4 file = fopen(”test.txt”, ”r+
)5 if (file != NULL
{6
7 // We can read or write in the file
}8
9 else
{ 10
11 // We display an error message if we want
;)”12 printf(”Can’t open the file test.txt
} 13
;14 return 0
} 15
إفعل هذا دائما عند فتح أي ملف .إن لم تفعل و الملف غير موجود ،فأنت تخاطر بتوق ّف البرنامج بعدها.
: fcloseغلق الملف
إذا نجحت عملية فتح الملف ،يمكننا القراءة و الكتابة فيه )سنرى كيف نفعل هذا لاحقا ً(.
ما إن نكمل العمل على الملف ،يجب علينا ”غلقه” .نستعمل من أحل هذا الدالة fcloseالتي تقوم بتحرير
الذاكرة .يعني أن ّه سيتم حذف الملف المحمّل في الذاكرة العشوائية.
نموذج الدالة :
;)1 int fclose(FILE* pointerOnFile
هذه الدالة تأخذ معاملا واحدا :المؤشر نحو الملف.
تقوم بإرجاع ، intو الذي يأخذ القيم :
• : 0إذا نجح غلق الملف.
• : EOFإذا فشل الغلق EOF .هي عبارة عن #defineموجودة في stdio.hو هي توافق
عددا ً خاصا ً ،ي ُستعمل للقول أنه حصل خطأ ،أو أننا وصلنا إلى نهاية الملف .في حالتنا هذه ،هذا يعني
حدوث خطأ.
في غالب الأحيان ،تنجح عملية غلق الملف :هذا ما يدفعني إلى عدم اختبار إن كانت fcloseقد عملت.
رغم هذا ،يمكنك فعل ذلك إن أردت.
لإغلاق الملف ،نكتب إذن :
209
الفصل ب .7.قراءة و كتابة الملفات
;)1 fclose(file
في النهاية ،المخ ّطط الذي نت ّبعه لفتح و غلق ملف سيكون كالتالي :
)][1 int main(int argc, char *argv
{2
;3 FILE* file = NULL
;)”4 file = fopen(”test.txt”, ”r+
)5 if (file != NULL
{6
7 // We read and we write in the file
8 // ...
9 fclose(file); // We close the opened file
} 10
;11 return 0
} 12
لم استعمل elseلأظهر رسالة خطأ في حال لم ينجح الفتح ،يمكنك فعل ذلك إن أردت.
يجب دائما التفكير في غلق الملف الذي فتحته بمجر ّد الإنتهاء من العمل عليه .هذا سيسمح بتحرير الذاكرة.
إن نسيت تحرير الذاكرة ،قد يأخذ برنامجك حجما كبيرا ً من الذاكرة بدون أن يستخدمه .في مثال صغير كهذا
الأمر غير خطير ،لـكن مع برنامج كبير ،مرحبا ًبالمشاكل !
نسيان تحرير الذاكرة أمر يقع .بل سيحدث لك هذا كثيرا .في هذه الحالة نقول أن ّه قد حدث تسريب
للذاكرة .هذا يجعل برنامجك يستخدم قدرا من الذاكرة أكبر من اللازم بدون أن تفهم سبب حصول ذلك .في
غالب الأحيان ،يكون السبب واحدا أو إثنين من الأمور ”الثانو ية” مثل نسيان . fclose
ب 2.7.طرق مختلفة للقراءة و الكتابة في الملفات
و الآن مادمنا تعل ّمنا كيف نفتح و نغلق ملفا ،لم يبق سوى أن نضيف الشفرة ال ّتي تقوم بالقراءة و الكتابة
عليه.
سنبدأ برؤ ية كيفي ّة الكتابة في مل ّف )الأمر الأبسط قليلا( ،ثم ّ نمر ّ يعدها إلى كيفي ّة القراءة من مل ّف.
الكتابة في ملف
توجد الـكثير من الدوال التي تسمح بالكتابة في ملف .يبقى عليك أن تختار أيها الأنسب لك لتستخدمها .هذه
الثلاث دوال ال ّتي سنتعل ّمها :
• : fputcتكتب حرفا في المل ّف )حرف واحد في المرة(.
• : fputsتكتب سلسلة محرفي ّة في الملف.
• : fprintfتكتب سلسلة ”من ّسقة ً” في الملف ،طر يقة عملها مطابقة تقريبا للدالة . printf
210
طرق مختلفة للقراءة و الكتابة في الملفات
fputc
هذه الدالة تكتب حرفا واحدا في المر ّة في الملف .نموذجها :
;)1 int fputc(int character, FILE* pointerOnFile
و هي تأخذ معاملين :
• المحرف الذي يجب كتابته )من نوع ، intمثلما قلت فاستعماله يعود تقريبا ً إلى استعمال ، charإلا
أن عدد المحارف الممكن استعمالها هنا أكبر( .يمكنك إذن أن تكتب مباشرة ’ ’Aكمثال.
• المؤش ّر نحو الملف الذي نريد أن نكتب فيه .في مثالنا ،المؤشّر اسمه . fileاستعمال المؤش ّر في ك ّل مرة
يساعدنا لأنه بإمكاننا أن نفتح العديد من الملفات في آن واحد ،و نقرأ و نكتب في ك ّل واحد من هذه
المل ّفات .لست مح ّددا بفتح مل ّف واحد في المر ّة.
الدالة تقوم بإرجاع ، intو هو رمز الخطأ .هذا الـ intيساوي EOFإذا فشلت الكتابة ،و إلّا فسيأخذ
قيمة أخرى.
بما أ ّن المل ّف قد تم ّ فتحه بنجاح ،فليس من عادتي إختبار إن كانت ك ّل واحدة من fputcقد نجحت ،ولـكن
يمكنك فعل ذلك إن أردت.
الشفرة التالية تسمح بكتابة الحرف ’ ’Aفي الملف ) test.txtإذا كان موجودا ً من قبل فإنه سيتم
استبداله ،أمّا إن لم يكن موجودا ً سيتم إنشاؤه( .الشفرة تحتوي كل الخطوات التي تكلّمنا عنها سابقا ً :فتح
الملف ،اختبار الفتح ،الكتابة و الغلق :
)][1 int main(int argc, char *argv
{2
;3 FILE* file = NULL
;)”4 file = fopen(”test.txt”, ”w
)5 if (file != NULL
{6
7 fputc(’A’, file); // Write the character A
;)8 fclose(file
}9
;10 return 0
} 11
افتح بنفسك الملف . test.txtماذا ترى ؟
إن هذا سحر ّي ،الملف يحتوي الآن على الحرف ’ ’Aكما ترى في الشكل التالي :
211
الفصل ب .7.قراءة و كتابة الملفات
fputs
هذه الدالة شبيهة جدا ًبالدالة ، fputcإلا أنها تسمح بكتابة سلسلة محرفي ّة كاملة ،و هذا عادة أحسن من الكتابة
حرفا ًحرفا ً.
لـكن fputcتبقى ضرور ي ّة حينما نحتاج إلى الكتابة محرفا بمحرف ،و هذا يحدث كثيرا.
نموذج الدالة :
;)1 char* fputs(const char* string, FILE* pointerOnFile
المعاملان سهلا الفهم :
• : stringالسلسلة ال ّتي نريد كتابتها .تلاحظ أن النوع هنا هو * : const charإضافة الكلمة
constفي النموذج تشير إلى أن السلسة ال ّتي سنعطيها للدالة ت ُفترض ثابتة .أي أ ّن الدالة لن تقوم بتغييرها.
هذا أمر منطقي عندما نفكّر فيه fputs :يجب أن تقرأ السلسلة بدون تعديلها .هذه إذن معلومة لـكم
)و حماية( أ ّن سلسلتكم لن يتم ّ إدخال أي ّة تعديلات عليها.
• : pointerOnFileمثل fputc ،تحتاج هذه الدالة إلى مؤشر من نوع * FILEنحو الملف ال ّذي
فتحته.
الدال ّة تعيد القيمة EOF ،في حالة وجود خطأ ،و إلّا ،فهذا يعني أ ّنها عملت على ما يرام .و هنا أيضا ،لن
أقوم عادة باختبار القيمة التي ترجعها الدالة.
فلنجرب كتابة سلسلة في ملف :
212
طرق مختلفة للقراءة و الكتابة في الملفات
1 )][int main(int argc, char *argv
2
{
;3 FILE* file = NULL
;)”4 file = fopen(”test.txt”, ”w
)5 if (file != NULL
{6
;)7 fputs(”Hello my friends\nHow are you ?”, file
;)8 fclose(file
}9
;10 return 0
} 11
الشكل التالي يظهر الملف بعد التعديل عليه من طرف البرنامج :
fprintf
إليك نوعا ً آخرا ً من الدالة . printfهذه تستخدم للكتابة في ملف .هذه الدالة تستعمل بنفس الطر يقة التي
نستعمل بها ، printfإلا أنه يجب إعطاؤها المؤشر نحو FILEكمعامل أ ّول.
الشفرة التالية تطلب من المستخدم إدخال عمره ،ثم ّ تقوم بكتابته في الملف :
)][1 int main(int argc, char *argv
{2
;3 FILE* file = NULL
213
4 الفصل ب .7.قراءة و كتابة الملفات
5
6 ;int age = 0
7 ;)”file = fopen(”test.txt”, ”w
8 )if (file != NULL
9 {
10
11 // Request the age
12 ;)” ? printf(”How old are you
;)scanf(”%d”, &age
13 // Write the age on the file
14 fprintf(file , ”The mister who uses the PC has %d
15
} 16 ;)years”, age
;)fclose(file
}
;return 0
يمكنك إذا إعادة استعمال ما تعرفه عن printfللكتابة في ملف ! لهذا السبب أنا غالبا ما استعمل
fprintfللكتابة في المل ّفات.
القراءة من ملف
لدينا أيضا ًثلاث دوال للقراءة من ملف ،اسمها مختلف قليلا ًفقط عن دوال الكتابة :
• : fgetcقراءة محرف.
• : fgetsقراءة سلسلة محرفي ّة.
• : fscanfقراءة سلسلة من ّسقة.
سأسرع قليلا ًفي شرح هذه الدوال :إذا كنت قد فهمت ما كتبته من قبل ،فلن تجد أي صعوبة مع هذه
الدوال.
fgetc
أولا ً ،النموذج :
;)1 int fgetc(FILE* pointerOnFile
214
طرق مختلفة للقراءة و الكتابة في الملفات
هذه الدالة تقوم بإرجاع : intإنه المحرف الذي تم ّت قراءته .إذا لم تقرأ أ ّي محرف ،فستعيد القيمة
. EOF
لـكن كيف لنا أن نعرف المحرف الذي نقرؤه ؟ ماذا لو أردنا قراءة المحرف الثالث و أيضا العاشر ،كيف
نفعل هذا ؟
في الواقع ،في ك ّل مرة تقرأ فيها مل ّفا ،فهناك ”مؤشر” )) (cursorمثل المؤش ّر الذي يغمز في محرر النصوص(
يتحر ّك في ك ّل مرة .و هذا المؤشّر افتراضي طبعا ً ،لن تتمكن من رؤيته على الشاشة .و هو يشير إلى أين وصلنا في
قراءة الملف.
سنتعلم لاحقا ًكيف نعرف الوضعية التي وصل إليها المؤشّر بالضبط و أيضا كيف نحر ّكه من مكانه )و ذلك
لـكي نقوم بتحر يكه إلى بداية الملف مثلا ،أو إلى مكان محرف مح ّدد ،كالمحرف العاشر(.
fgetcتقوم بتحر يك المؤشر بمحرف واحد في ك ّل مرة تقرؤ فيها واحدا .أي أنك إن استدعيت fgetc
مرة ثانية ،فستقرأ المحرف الثاني ،ثم الثالث و هكذا .و بهذا يمكنك استعمال حلقة تكرار ية لقراءة محارف الملف
واحدا واحدا.
سنقوم بكتابة شفرة تقرأ ك ّل محارف الملف واحدا واحدا و في ك ّل م ّرة تكتبها على الشاشة .الحلقة ستتوقف
حينما تعيد fgetcالقيمة ) EOFو ال ّتي تعني ” ”End Of Fileأي ”نهاية الملف”(.
1 )][int main(int argc, char *argv
2
{
3 ;FILE* file = NULL
4
;int currentCharacter = 0
;)”5 file = fopen(”test.txt”, ”r
)6 if (file != NULL
{7
8 // A loop to read the characters one by one
9 do
{ 10
11 currentCharacter = fgetc(file); // Read the
character
12 printf(”%c”, currentCharacter); // Display it
13 } while (currentCharacter != EOF); // Continue while
14 )fgets didn’t return EOF (End Of File
15 ;)fclose(file
16 }
} 17 ;return 0
الـكونسول ستقوم بإظهار محتوى الملف كاملا ً ،مثلا :
! Hello, I’m the content of the file test.txt
215
الفصل ب .7.قراءة و كتابة الملفات
fgets
هذه الدالة تقوم بقراءة سلسلة من ملف .هذا يجنبك قراءة ك ّل محارف الملف واحدا واحدا .الدالة تقرأ على
الأكثر سطرا ً واحدا ً )ٺتوقف عند ملاقاة أول ،( \nإن أردت قراءة العديد من الأسطر ،فعليك استعمال
حلقة.
هذا نموذج الدالة :
;)1 char* fgets(char* string, int nbOfCharsToRead, FILE* pointerOnFile
هذه الدالة ٺتطلب معاملا خا ّصا نوعا ًما ،و لـكن ّه سيكون عملي ّا ج ّدا :عدد المحارف التي نريد قراءتها .هذا
ما يطلب من الدالة fgetsالتوقف عن قراءة السطر إذا كان يحوي أكثر من Xمن المحارف .الفائدة :هذا
يسمح لنا بضمان عدم حدوث تجاوز في الذاكرة ! في الواقع ،إذا كان حجم السطر أكبر من أن تسعه السلسلة
المحرفي ّة ،فمن الممكن أن تقرأ عددا من المحارف أكثر مم ّا يسمح به المكان المتوف ّر ،و هذا قد يسبب تع ّطل البرنامج.
سنتعلّم كيف نقرأ سطرا ً واحدا ً باستخدام ) ، fgetsثم بعدها سنرى كيفية قراءة ملف كامل(.
لهذا فسنقوم بتعر يف سلسلة محرفي ّة كبيرة كفاية لتخزين السطر المراد قراءته )على الأقل نتمن ّى ذلك ،لا
يمكننا أن نكون متأكّدين .(100%سترى فائدة استخدام الـ #defineفي تعر يف حجم جدول :
1 #define MAX_SIZE 1000 // A table of size 1000
2 )][int main(int argc, char *argv
3
{
4 ;FILE* file = NULL
5
char string[MAX_SIZE] = ””; // Empty string of size MAX_SIZE
;)”6 file = fopen(”test.txt”, ”r
)7 if (file != NULL
{8
9 fgets(string, MAX_SIZE, file); // Read at maximum MAX_SIZE
10 ”characters from the file, store them in ”string
11 printf(”%s”, string); // Display the string
12 ;)fclose(file
13 }
} 14 ;return 0
النتيجة هي نفسها النتيجة السابقة ،مع العلم أ ّن المحتوى ي ُكتب في الـكونسول :
! Hello, I’m the content of the file test.txt
الفرق هو أننا هنا لم نستعمل حلقة تكرار ية .نقوم بإسترجاع محتوى الملف كاملا في م ّرة.
أنت تلاحظ بك ّل تأكيد الآن فائدة استعمال #defineفي شفرتك لتعر يف الحجم الأقصى لجدول مثلا ً.
في الواقع MAX_SIZE ،مستعمل في مكانين مختلفين في الشفرة :
216
طرق مختلفة للقراءة و الكتابة في الملفات
• المرة الأولى لتعر يف حجم الجدول الذي نريد إنشاءه.
• م ّرة اخرى في الـ fgetsلنقوم بتحديد عدد المحارف التي نقرؤها.
الفائدة هنا ،هي أن ّه في حال ما وجدت أن السلسلة المحرفي ّة غير كبيرة كفاية لقراءة الملف ،فلن يكون عليك
سوى تعديل سطر الـ #defineو إعادة الترجمة .هذا سيجن ّبك البحث عن ك ّل مكان من الشفرة وضعت فيه
حجم الجدول .المعالج القبلي سيقوم باستبدال كل تكرار لـ MAX_SIZEبالقيمة الجديدة.
كما قلت فإن fgetsتقرأ على الأكثر سطرا ً واحدا في المر ّة .ٺتوقف عن قراءة السطر عندما تتجاوز عدد
المحارف الذي سمحت لها بقراءتها.
نعم و لـكن :حالي ّا ،نحن لا نجيد سوى قراءة سطر واحد باستخدام . fgetsكيف لنا أن نقرأ كل
الملف ؟ الجواب بسيط :بحلقة تكرار ية !
الدالة fgetsتعيد NULLفي حالة لم تستطع قراءة ما طلبته منها.
أي أن الحلقة يجب أن تنتهي بمجر ّد أن تعيد fgetsالقيمة . NULL
ليس علينا سوى استعمال الحلقة whileلـكي نقوم بالتكرار ما دامت fgetsلم ترجع : NULL
1 #define MAX_SIZE 1000
)][2 int main(int argc, char *argv
{3
;4 FILE* file = NULL
;”” = ]5 char string[MAX_SIZE
;)”6 file = fopen(”test.txt”, ”r
)7 if (file != NULL
{8
9 while (fgets(string, MAX_SIZE, file) != NULL) // Read the
)file while there’s no error (NULL
{ 10
11 printf(”%s”, string); // Display the string that we’ve
12 read
13 }
14 ;)fclose(file
15 }
} 16 ;return 0
هذه الشفرة تقوم بقراءة الملف سطرا ً سطرا ً و إظهار الأسطر.
السطر الأكثر لفتا ًللانتباه في الشفرة هو :
)1 while (fgets(string, MAX_SIZE, file) != NULL
سطر الـ whileيقوم بأمرين :قراءة سطر من الملف و التأكد أن fgetsلم ت ُع ِد . NULLيمكن ترجمة
هذا كالتالي ” :إقرأ سطرا ً جديدا ً ما دمنا لم نصل إلى نهاية الملف”.
217
الفصل ب .7.قراءة و كتابة الملفات
fscanf
مبدأ هذه الدالة مشابه تماما ًلمبدأ نظيرتها ، scanfهنا أيضا.
هذه الدال ّة تقوم بقراءة مل ّف تمت كتابته بشكل مح ّدد.
لنفترض أن الملف يحتوي على ثلاثة أعداد مفصولة بفراغ ،و هي مثلا أكبر ثلاثة نقاط تم التحصل عليها في
لعبتك . 15 20 30 :
أنت تريد أن تسترجع ك ّل واحد من هذه الأعداد في متغير من نوع . int
الدالة fscanfستسمح لك بالقيام بهذا بشكل سر يع.
1 )][int main(int argc, char *argv
2
{
3 ;FILE* file = NULL
4
int score[3] = {0}; // Table of the 3 best scores
;)”5 file = fopen(”test.txt”, ”r
)6 if (file != NULL
{7
;)]8 fscanf(file, ”%d %d %d”, &score[0], &score[1],&score[2
9 printf(”The best scores are : %d, %d and %d”, score[0], score[1],
;)]score[2
;)10 fclose(file
} 11
;12 return 0
} 13
The best scores are : 15, 20 and 30
كما ترى ،فالدالة fscanfتنتظر ثلاث أعداد مفصولة بفراغ ) ” .( ”%d %d %dستقوم بتخزينهم في
جدولنا ذو الخانات الثلاث.
نقوم لاحقا ًبإظهار ك ّل القيم المسترجعة.
حت ّى الآن ،لم استعمل سوى رمز %dواحد في الدالة . scanf
اليوم اكتشف َت بأنه بإمكانك أن تستعمل العديد منها .إذا كان الملف مكتوبا بطر يقة مح ّددة جي ّدا ،فهذا
يسمح لك بالإسراع لاسترجاع ك ّل واحدة من هذه القيم.
ب 3.7.التحرك داخل ملف
كنت قد كلّمتك عن وجود ”مؤش ّر” افتراضي ) (virtual cursorقبل قليل .سنقوم الآن بدراسته بشكل
أكثر تفصيلا ً.
218
التحرك داخل ملف
في ك ّل مرة تفتح فيها ملفا ،فهناك مؤشّر يشير إلى وضعيتك في الملف .و لتتخي ّله تماما مثل مؤشر محرر
النصوص .يد ّل على الكان ال ّذي أنت فيه من الملف ،أي أين ستقوم بالكتابة.
كتلخيص ،نظام المؤشر يسمح لك بالكتابة و القراءة في وضعية محددة من الملف.
توجد ثلاث دوال لتتعرف عليها :
• : ftellتدل ّنا على الوضعية التي نحن بها حاليا ًفي الملف.
• : fseekت ُموضع المؤش ّر في مكان محدد.
• : rewindتقوم بإرجاع المؤش ّر إلى بداية الملف )هذا مكافئ للطلب من الدالة fseekأن تموضع
المؤشّر في البداية(.
: ftellالموضع في الملف
هذه الدالة بسيطة الاستعمال ج ّدا .تعيد الموضع الذي يتواجد به المؤشّر حاليا بنوع : long
;)1 long ftell(FILE* pointerOnFile
العدد ال ّذي يتم ارجاعه يد ّل على موضع المؤشر في الملف.
: fseekالتموضع داخل الملف
نموذج fseekهو التالي :
;)1 int fseek(FILE* pointerOnFile, long deplacement, int origin
الدال ّة fseekتسمح بتحر يك المؤش ّر بـعدد من المحارف )يد ّل عليها ( deplacementإنطلاقا من الموضع
ال ّذي يد ّل عليه . origin
• العدد deplacementيمكن له أن يكون عددا ً موجبا ً)للتقدم إلى الأمام( ،معدوما )= (0أو سالبا ً
)للرجوع إلى الخلف(.
• أمّا بالنسبة للعدد originفهو يأخذ إحدى القيم التالية :
– SEEK_SETتعني بداية الملف.
– SEEK_CURتعني الموضع الحالي نفسه.
– SEEK_ENDتعني نهاية الملف.
219
الفصل ب .7.قراءة و كتابة الملفات
إليك بعض الأمثلة لـكي تفهم جي ّدا كيف ٺتلاعب بـ deplacementو : origin
• هذه الشفرة تضع المؤشر محرفين بعد بداية الملف :
;)1 fseek(file, 2, SEEK_SET
• هذه الشفرة تضع المؤشّر أربع محارف قبل الوضعية الحالية :
;)1 fseek(file, −4, SEEK_CUR
لاحظ أن قيمة deplacementسالبة لأننا نتحر ّك إلى الوراء.
• الشفرة التالية تضع المؤشّر في نهاية الملف :
;)1 fseek(file, 0, SEEK_END
إذا كتبت ،بعد القيام بـ fseekتحر ّكك إلى نهاية الملف ،فذلك سيضيف معلومات إلى نهاية الملف
)الملف سيتم ّ إكماله(.
بالمقابل ،إذا وضعت المؤشّر في بداية الملف وكتبت ،فهذا سيستبدل الن ّص الموجود هناك .لا توجد طر يقة
لـ”إدراج” نص في ملف .إلا إن قمت بنفسك ببرمجة دالة تقرأ المحارف لتتذكّرها قبل إستبدالها !
لـكن كيف لي أن أعرف أ ّي موضع يجب أن أذهب إليه للقراءة و الكتابة في الملف ؟
هذا يعود إليك .إن كان ملفا ًقمت أنت بكتابته ،فأنت تعرف كيف تم ّ بناءه .أنت تعرف أين تذهب للبحث
عن المعلومة :مثلا ،أحسن النتائج المسجلة في اللعبة في الموضع ،0أسماء آخر اللاعبين في الموضع ،50الخ.
سنقوم بعمل تطبيقي لاحقا ً حيث ستفهم ،إذا لم تكن قد فهمت بالفعل الآن ،كيف نذهب للبحث عن
معلومة تهمّنا .لا تنس بأن ّك أنت من يعر ّف كيفي ّة بناءه .إذن عليك أن تقول ” :أضع نتيجة أحسن لاعب في
السطر الأ ّول ،الخاصة بثاني أحسن لاعب في السطر الثاني ،إلخ”.
الدالة fseekقد ٺتعامل بشكل غريب مع الملفات المفتوحة بوضع النص ) .(Text modeعادة ،نحن
نستعملها أكثر مع الملفات المفتوحة بالوضع الثنائي ) .(Binary modeعند القراءة و الكتابة في ملف
بوضع الن ّص ،فإنّنا عادة ما نفعل ذلك محرفا ً محرفا ً .الشيء الوحيد الذي نسمح به غالبا في وضع الن ّص
مع fseekهو العودة إلى البداية أو التموضع في نهاية الملف فقط.
: rewindالرجوع إلى البداية
هذه الدالة مكافئة لاستخدام fseekلإرجاعنا إلى الموضع 0في الملف :
;)1 void rewind(FILE* pointerOnFile
طر يقة الاستعمال بسيطة كالنموذج .أنت لست بحاجة إلى شرح إضافيّ.
220
إعادة تسميه و حذف ملف
ب 4.7.إعادة تسميه و حذف ملف
ننهي هذا الفصل بن ُع ُومة عن طر يق دراسة دالتين بسيطتين للغاية :
• : renameإعادة تسمية ملف.
• : removeحذف ملف.
الشيء الخا ّص في هاتين الدالتين هو أنهما لا تحتاجان مؤشرا ً نحو الملف لـكي تعملا .يكفيهما فقط اسم الملف
المراد حذفه أو تغيير اسمه.
: renameإعادة تسمية ملف
إليك نموذج هذه الدالة :
;)1 int rename(const char* oldName, const char* newName
الدالة تعيد 0إذا نجحت في اعادة التسمية ،و إلّا فستعيد قيمة مختلفة عن .0هل من اللازم أن أعطيك
مثالا ؟ إليك واحدا :
)][1 int main(int argc, char *argv
{2
;)”3 rename(”test.txt”, ”test_rename.txt
;4 return 0
}5
: removeحذف ملف
هذه الدالة تقوم بحذف ملف دون ترك أي أثر :
;)1 int remove(const char* fileToDelete
كن حذرا جدا ً عند استعمالك لهذه الدالة ! هي تحذف الملف بدون أن تطلب منك أ ّي تأكيد ! الملف
لن يوضع في سلة المحذوفات ،بل يسحذف حرفي ّا من القرص الصلب .لن يمكنك استعادة مل ّف محذوف
بهذه الطر يقة )إلّا باستعمال أدوات خا ّصة باسترجاع المل ّفات ،لـك ّن هذه العملية قد تكون طو يلة ،مع ّقدة
و قد لا تنجح(.
هذه الدالة مناسبة لإنهاء الفصل ،فلم أعد في حاجة إلى الملف ، test.txtيمكنني الآن حذفه :
1 )][int main(int argc, char *argv
2
{
;)”3 remove(”test.txt
;4 return 0
}5
221
الفصل ب .7.قراءة و كتابة الملفات
222
الفصل ب8.
الحجز الح ّي للذاكرة )Dynamic memory
(allocation
كل المتغيّرات التي أنشأناها لحد الآن تم ّ إنشاؤها تلقائيّا من طرف المترجم الخا ّص بلغة .Cلقد كانت الطر يقة
البسيطة .رغم ذلك ،توجد طر يقة يدو ية أكثر لإنشاء متغيّرات و نسمّيها بالحجز الح ّي ).(Dynamic allocation
من بين فوائد الحجز الح ّي هو السماح لبرنامج بحجز مكان لازم لتخزين جدول في الذاكرة لا ي ُعرف حجمه
قبل بداية الترجمة .في الواقع ،حت ّى الآن ،كان حجم جداولنا ثابتا ًفي الشفرة المصدر ي ّة .بعد قراءة هذا الفصل،
ستستطيع إنشاء جداول بطر يقة أكثر مرونة !
من الضروري أن ٺتقن التعامل مع المؤشرات لتتم ّكن من قراءة هذا الفصل ! إن كانت لديك بعض
الشكوك حول المؤشرات ،أنصحك بالذهاب لإعادة قراءة الفصل الموافق قبل البدأ.
عندما نقوم بالتصريح عن متغيّر ،فإننا نقول أننا طلبنا حجز مكان في الذاكرة :
;1 int myNumber = 0
عندما يصل المترجم إلى سطر مشابه للسطر السابق ،يقوم بالأمور التالية :
• يقوم البرنامج بطلب إذن من نظام التشغيل ) (...Mac OS ،GNU/Linux ،Windowsليحجز شيئا من
الذاكرة.
• يستجيب نظام التشغيل بإعطاء البرنامج عنوان الخانة حيث يمكنه تخزين المتغيّر )يعطيه العنوان ال ّذي حجزه
له(.
• عندما تنتهي الدال ّة ،المتغيّر يتم حذفه من الذاكرة .برنامجك يقول لنظام التشغيل ” :أنا لم أعد بحاجة إلى
المكان في الذاكرة ال ّذي حجزته في ذلك العنوان ،شكرا ! التاريخ لا يح ّدد إن كان البرنامج قد قال فعلا
”شكرا” لنظام التشغيل ،لـك ّن هذا في مصلحته لأ ّن نظام التشغيل هو ال ّذي يتحكم في الذاكرة !
223
الفصل ب .8.الحجز الح ّي للذاكرة )(Dynamic memory allocation
لحد الآن كل الأمور كانت تلقائيّة .عندما نصرّح عن متغير فإن نظام التشغيل يتم ّ استدعاءه تلقائيا ً من
طرف البرنامج .ما رأيك إذا بفعل هذا بطر يقة يدو ية ؟ ليس لأننا نريد أن نستمتع بفعل شيء مع ّقد ،بل لأننا
أحيانا نظطر ّ لفعل ذلك !
في هذا الفصل سنقوم بـ :
• دراسة كيف تعمل الذاكرة )نعم ،م ّرة أخرى !( لنعرف ما الحجم الذي يحجزه كل متغيّر حسب نوعه.
• ثم ّ ندخل في موضوعنا الأساسي :سنرى كيف نطلب من نظام التشغيل يدويّا أن يحجز لنا مكانا في
الذاكرة .هذا ما سنسميه الحجز الح ّي للذاكرة.
• و أخيرا ً ،سنكتشف الفائدة من القيام بالحجز الح ّي بتعلّم إنشاء جدول ذو حجم غير معروف إلّا عند اشتغال
البرنامج.
ب 1.8.حجم المتغيرات
بحسب نوع المتغير التي نريد إنشاءه ) (... float ، char ، intفنحن نحتاج إلى حجم معي ّن من
الذاكرة.
في الواقع ،لتخزين عدد من −128إلى ( char ) 127لن نحتاج إلا إلى بايت واحد من الذاكرة .هذا حجم
صغير للغاية.
بالمقابل int ،يحجز عادة حوالي 4بايتات من الذاكرة .بينما doubleيحجز 8بايتات.
المشكل هو ...أن هذا ليس دائما صحيحا .هذا يعتمد على الأجهزة :فقد يكون intيحجز 8بايتات.
من يعلم ؟
هدفنا هنا أن نتعر ّف كم يحجز ك ّل نوع من حجم في الذاكرة على حاسوبك.
توجد وسيلة سهلة ج ّدا لمعرفة هذا :استعمال العامل )(. sizeof
على عكس الظاهر ،فهو ليس دالة ،بل عبارة عن إحدى الوظائف الأساسية من لغة الـ ،Cيجب عليك فقط أن
تضع بين القوسين النوع الذي تريد تحليله.
لمعرفة حجم ، intيجب كتابة التالي :
)1 sizeof(int
عند الترجمة ،سيتم استبدال هذه الشفرة بعدد :عدد البايتات ال ّتي يحجزها intفي الذاكرة .بالنسبة لي،
) sizeof(intتساوي ،4و هذا يعني أ ّن intيأخذ 4بايتات .بالنسبة لك ،ستكون نفس القيمة على
الأرجح ،لـكنّها ليست قاعدة .جرّب لترى ،بعرض القيمة عن طر يق printfمثلا :
224
حجم المتغيرات
;))1 printf(”char : %d bytes\n”, sizeof(char
;))2 printf(”int : %d bytes\n”, sizeof(int
;))3 printf(”long : %d bytes\n”, sizeof(long
))4 printf(”double : %d bytes\n”, sizeof(double
بالنسبة لي ،هذا يظهر على الشاشة :
1 char : 1 bytes
2 int : 4 bytes
3 long : 4 bytes
4 double : 8 bytes
لم أختبر كل الأنواع ال ّتي نعرفها ،أتركك لتجر ّب أحجام الأنواع الأخرى.
أنت تلاحظ أن intو longيحجزان نفس الحجم من الذاكرة .إنشاء longيعود تماما إلى إنشاء
، intهذا يأخذ 4بايتات من الذاكرة.
في الواقع ،النوع longهو مكافئ لنوع نسميه ، long intو الذي هو مكافئ لنوع int ...نفسه.
باختصار ،فإن هذه أسماء كثيرة مختلفة لأجل أشياء ليست بالـكبيرة ،في النهاية ! امتلاك أنواع مختلفة
كثيرة كان أمرا مهمّا في الوقت الذي لم ّ تكن الحواسيب تملك كثيرا من ذاكرة .كنا نبحث دائما لاستخدام
الح ّد الأدنى من الذاكرة باستخدام النوع المناسب.
اليوم ،هذا لم يعد مفيدا كثيرا لأ ّن ذاكرة الحاسوب صارت كبيرة ج ّدا .بالمقابل ،هذه الأنواع لا تزال
مفيدة إذا كنت تنشئ برامج للأنظمة المضمّنة ) (Embedded systemsحيث الذاكرة المتوف ّرة أقل.
أظن مثلا في البرامج المو ّجهة للهواتف المحمولة ،الألي ّات ،إلخ.
هل بإمكاننا أن ن ُظهر حجم نوع مخ ّصص قمنا نحن بإنشائه )هيكل( ؟
نعم ! )( sizeofتعمل مع الهياكل أيضا !
; 1 typedef struct Coordinates Coordinates
2 struct Coordinates
{3
;4 int x
;5 int y
;} 6
7 )][int main(int argc, char *argv
8
{
;))9 printf(”Coordinates : %d bytes\n”, sizeof(Coordinates
;10 return 0
} 11
Coordinates : 8 bytes
225
الفصل ب .8.الحجز الح ّي للذاكرة )(Dynamic memory allocation
كلما احتوى الهيكل من مركّبات كلّما أخذ حجما أكثر من الذاكرة .الأمر منطقي تماما ،أليس كذلك ؟
طر يقة أخرى للنظر إلى الذاكرة
لحد الآن ،كل المخططات التي ق ّدمتها لك عن الذاكرة لم تكن دقيقة .سنجعلها أخيرا دقيقة حقا و صحيحة بما
أننا تعل ّمنا الآن كم يأخذ كل نوع من حجم بالذاكرة.
إن صرّحنا عن متغير من نوع : int
;1 int number = 18
و ) sizeof(intيعطينا 4بايت على حاسوبنا ،هذا يعني أن المتغير يحجز 4بايت في الذاكرة !
لنفترض أن المتغير numberمحجوز بالعنوان 1600من الذاكرة .سيكون لدينا إذا المخطط التالي للذاكرة :
هنا ،يمكننا فعلا ًأن نرى بأن المتغير numberمن النوع intيحجز 4بايت من الذاكرة .فهو يبدأ من
العنوان 1600و ينتهي عند العنوان ،1603المتغير القادم لن يتم تخزينه إلا إبتداء ً من العنوان ! 1604
إن جربنا نفس الشيء مع ، charفالمتغير لن يأخذ سوى بايت واحد في الذاكرة )الشكل التالي( :
226
حجم المتغيرات
تخي ّل الآن جدولا من ! int
كل ”خانة” من الجدول ستحجز 4بايت .إن كان الجدول يحوي مثلا ً 100خانة :
;]1 int table[100
سنحجز إذن 100 ∗ 4 = 400بايت في الذاكرة.
ماذا لو كان الجدول فارغا ً ،هل سيحجز بايت ؟
نعم بالطبع ! فالمكان في الذاكرة قد تم ّ حجزه ،و لا يملك أي برنامج الح ّق في استخدام هذه الخانات )غير هذا
البرنامج( .بمجر ّد التصريح عن متغيّر ،سيأخذ مكانه مباشرة المكان في الذاكرة.
لاحظ لو أننا ننشئ جدولا من نوع : Coordinates
;]1 Coordinates table[100
سيستخدم هذه المر ّة 8 ∗ 100 = 800بايت.
من المه ّم الفهم الجي ّد لهذه الحسابات البسيطة لنواصل بقي ّة الفصل.
227
الفصل ب .8.الحجز الح ّي للذاكرة )(Dynamic memory allocation
ب 2.8.الحجز الح ّي للذاكرة
فلندخل إلى صلب الموضوع .سأذكّرك بهدفنا :تعلّم كيفي ّة طلب الذاكرة يدو يا ً.
سنحتاج إلى تضمين المكتبة . stdlib.hإن كنت قد اتّبعت نصائحي ،فقد ضم ّنتها في ك ّل برامجك .هذه
المكتبة تحتوي على دال ّتين سنحتاج إليهما :
• ”Memory ALLOcation”) mallocبمعنى ”حجز الذاكرة”( :تطلب الإذن من نظام التشغيل لاستخدام
الذاكرة.
• ) freeتحرير( :تسمح للإشارة لنظام التشغيل بأننا لم نعد بحاجة إلى الذاكرة ال ّتي طلبناها .المكان في
الذاكرة تم ّ تحريره ،يستطيع برنامج آخر الآن استخدامها عند الحاجة.
عندما تقوم بحجز يدوي للذاكرة ،فعليك اتباع الخطوات التالية :
.1استدعاء mallocمن أجل طلب الذاكرة.
.2اختبار القيمة التي تم ارجاعها من طرف mallocلمعرفة ما إن نجح نظام التشغيل في حجز الذاكرة.
.3ما إن ننتهي من استخدام الذاكرة ،يجب علينا تحريرها باستعمال . freeإن لم نفعل هذا ،فسنتعر ّض
لتسريبات ذاكرة ،أي أ ّن البرنامج يخاطر بحجز كثير من الذاكرة مع أن ّه ليس بحاجة إلى ك ّل هذا المكان.
هل تذكّرك هذه الخطوات الثلاث بفصل الملفات ؟ نعم يجب أن تفعل ! المبدأ واحد تماما :نحجز ،نختبر
إن نجح الحجز ،ثم ّ نحرر عندما ننتهي من الاستعمال.
mallocلنطلب الإذن لحجز الذاكرة
نموذج الدالة mallocهزليّ ج ّدا ،سترى :
;)1 void* malloc(size_t numberOfNecessaryBytes
الدالة تأخذ معاملا واحدا :عدد البايتات ال ّتي يجب حجزها .هكذا ،يكفي أن كتابة ) sizeof(intلحجز
مكان من أجل تخزين . int
و لـك ّن الشيء الذي يثير الفضول ،هو القيمة التي ترجعها الدالة :إ ّنها تعيد ! void* ...إذا لازلت ٺتذكّر
فصل الدوال ،كنت قد قلت لك بأن الكلمة voidتعني ”الفراغ” و نستعملها لنشير إلى أن الدالة لا ت ُعيد أية
قيمة.
228
الحجز الح ّي للذاكرة
إذن هنا ،لدينا دالة ت ُعيد ” ...مؤشّرا ً نحو فراغ” ؟ هذه نكتة جيدة !
يبدو أن هؤلاء المبرمجـين لديهم ح ّس فكاهي متطوّر.
كن متأكّدا ،يوجد سبب .في الحقيقة ،هذه الدالة تعيد عنوان الخانة التي حجزها نظام التشغيل من أجل
متغيّرك .إن استطاع النظام إيجاد مكان لك في العنوان ،1600فالدالة ستعيد مؤش ّرا يحوي العنوان .1600
المشكل هو أن الدالة mallocلا تعرف نوع المتغير التي نريد إنشاءه .في الواقع ،أنت لا تعطيها سوى
معامل واحد :عدد البايتات في الذاكرة ال ّتي تحتاجها .فإذا طلبت 4بايت ،فهذا يمكن أن يعني intأو ربما
longمثلا !
بما أ ّن mallocلا تعرف أ ّي نوع يجب عليها أن تعيد ،فهي تعيد النوع * . voidسيكون مؤش ّرا نحو أ ّي
نوع كان .يمكننا أن نقول أن ّه مؤشّر جامع.
لننتقل إلى التطبيق.
إذا كنت أريد الاستمتاع بإنشاء متغير من نوع intيدويّا في الذاكرة ،يجب أن أشير للـ mallocأنني أحتاج
إلى ) sizeof(intبايت في الذاكرة.
أسترجع قيمة mallocفي مؤشر على : int
1 int* allocatedMemory = NULL; // Create a pointer on int
2
allocatedMemory = malloc(sizeof(int)); // The function malloc puts
the allocated address in the pointer.
في نهاية هذه الشفرة allocatedMemory ،هو مؤش ّر يحتوي على عنوان حجزه نظام التشغيل لك ،لنقل
مثلا القيمة 1600للاكمال من مخططاتي السابقة.
اختبار المؤش ّر
الدالة mallocأعادت في المتغير allocatedMemoryعنوان الخانة التي تم حجزها بالذاكرة .هناك
احتمالان :
• إذا نجح الحجز ،فالمؤشّر سيحتوي عنوانا.
• إذا فشل الحجز ،فالمؤش ّر سيحتوي العنوان . NULL
إنه من النادر أن تفشل عملية حجز الذاكرة ،لـكن هذا ممكن .تخي ّل أنك تطلب حجز Go 34من الذاكرة
العشوائية ،في هذه الحالة ،ستفشل عملية الحجز على أغلب الظن.
من المستحسن دائما ًأن نختبر ما إن تمت العملية بنجاح .سنفعل هذا :إن فشل الحجز ،فهذا يعني أن المساحة
الحر ّة من الذاكرة العشوائية لم تكن كافية )هذه حالة حرجة( .في حالة كهذه ،يجب إيقاف البرنامج فورا لأن ّه،
على أية حال ،لن يكون قادرا ً على الاستمرار بشكل عاد ّي.
229
الفصل ب .8.الحجز الح ّي للذاكرة )(Dynamic memory allocation
سنستعمل دالة قياسي ّة لم يسبق لنا رؤيتها حت ّى الآن . exit() :هذه الأخيرة توقف البرنامج فورا.
إ ّنها تأخذ معاملا :القيمة ال ّتي يجب إعادتها من طرف البرنامج )هذا في الحقيقة يوافق الـ returnالخاص
بالـ .( main
1 )][int main(int argc, char *argv
2
{
;3 int* allocatedMemory = NULL
;))4 allocatedMemory = malloc(sizeof(int
5 if (allocatedMemory == NULL) // If the allocation has failed
{6
7 exit(0); // Stop the program
}8
9 // Else, we can continue the program normally.
;10 return 0
} 11
إذا كان المؤشر مختلفا عن ، NULLيمكن للبرنامج أن يواصل العمل ،و إلا فيجب إظهار رسالة خطأ أو
حت ّى إنهاء البرنامج لأن ّه لن يتم ّكن من الاستمرار بشكل صحيح إن ل ّم يكن هناك مكان في الذاكرة.
: freeتحرير الذاكرة
مثلما استعملنا الدالة fcloseلنغلق ملفا ً لم نعد في حاجة إليه ،سنستعمل الدالة freeمن أجل تحرير
الذاكرة التي لم نعد بحاجة إليها.
;)1 void free(void* pointer
الدالة freeبحاجة فقط إلى عنوان الذاكرة المراد تحريرها .سنرسل لها إذن مؤش ّرنا ،أي allocatedMemory
في مثالنا.
إليكم المخطط الكامل و النهائي ،مشابه بشكل كبير لما رأينا في الفصل الخاص بالملفات :
)][1 int main(int argc, char *argv
{2
3 ;int* allocatedMemory = NULL
4
;))allocatedMemory = malloc(sizeof(int
5 if (allocatedMemory == NULL) // Verify if the memory has been
allocated
{6
! 7 exit(0); // Error: Stop everything
}8
9 // We can use the memory here
10 free(allocatedMemory); // No need for the memory anymore,
11 free it.
} 12 ;return 0
230
الحجز الح ّي للذاكرة
مثال استخدام واقعي
سنبرمج شيئا درسناه منذ زمن طو يل :الطلب من المستخدم تزويدنا بع ُمره ثم ّ عرضه له .الشيء المختلف عم ّا كن ّا
نفعله سابقا هو أن المتغيّر هنا سيتم ّ حجزه يدو يا )نقول أيضا حيويّا( و ليس تلقائيّا كالسابق .إذن نعم ،في المر ّة
الأولى ،الشفرة أصعب قليلا .لـكن قم بالمجهود اللازم لفهمها جي ّدا ،هذا ضرور ّي :
1 )][int main(int argc, char *argv
2
{
3 ;int* allocatedMemory = NULL
4
allocatedMemory = malloc(sizeof(int)); // Allocation of the
5 memory
6 )if (allocatedMemory == NULL
7 {
8
9 ;)exit(0
10 }
11 // Using the memory
12 ;)” ? printf(”How old are you
13 ;)scanf(”%d”, allocatedMemory
14
} 15 ;)printf(”You are %d years old\n”, *allocatedMemory
free(allocatedMemory); // Freeing the memory
;return 0
How old are you ? 31
You are 31 years old
حذار :بما أن allocatedMemoryهو مؤش ّر ،فلا نستعمله بنفس الطر يقة التي نستعمل بها
متغيّرا حقيقي ّا .للحصول على قيمة المتغير يجب وضع نجمة أمامه ) *allocatedMemory :لاحظ
.( printfبينما للحصول على العنوان ،يكفي فقط أن كتابة اسم المؤش ّر allocatedMemory
)لاحظ .( scanf
ك ّل هذا تم شرحه في فصل المؤش ّرات .رغم ذلك ،أعرف أن هذا سيأخذ وقتا و من الممكن أن تخلط
بينهما .إن كانت هذه حالتك ،فعليك بإعادة قراءة فصل المؤشّرات ،فهو أساسي.
لنعد إلى الشفرة .لقد قمنا بحجز حيّ لمتغيّر من نوع . intفي النهاية ،ما كتبناه يعود تماما لاستخدام الطر يقة
”التلقائيّة” ال ّتي نعرفها الآن جي ّدا :
1 )][int main(int argc, char *argv
2
{
( 3 int myVariable = 0; // Allocation of the memory
)automatically
4 // Using the memory
;)” ? 5 printf(”How old are you
;)6 scanf(”%d”, &myVariable
231
الفصل ب .8.الحجز الح ّي للذاكرة )(Dynamic memory allocation
;)7 printf(”You are %d years old\n”, myVariable
;8 return 0
)9 } // Freeing the memory (automatically at the end of the function
How old are you ? 31
You are 31 years old
كملخص ،لدينا طر يقتان لإنشاء متغير ،أي لحجز الذاكرة .إمّا أن نقوم بذلك :
• تلقائيّا :هي الطر يقة ال ّتي تعرفها و التي استعملناها لغاية الآن.
• يدو يا )حيويّا( :هي الطر يقة ال ّتي أعل ّمكم إيّاها في هذا الفصل.
أنا أجد أن الطر يقة الحي ّة مع ّقدة و بلا فائدة !
أكثر تعقيدا ...بالتأكيد .لـكن بدون فائدة ،لا ! أحيانا نكون مجـبرين على حجز الذاكرة يدويّا كما سنرى
الآن.
ب 3.8.الحجز الح ّي لجدول
لحد الآن استعملنا الحجز الحي لإنشاء متغيّر صغير .عادة لا نستخدم الحجز الح ّي في هذا .نستخدم الطر يقة
التلقائيّة ال ّتي هي أبسط.
متى نحتاج للحجز الح ّي ،ٺتسائلون ؟ أكثر شيء ،نستخدمه لإنشاء جدول لا نعرف حجمه قبل تشغيل البرنامج.
لنتخيل مثلا برنامجا ًيقوم بتخزين أعمار أصدقاء المستخدم في جدول ،يمكنك فعل ذلك هكذا :
;]1 int friendsAge[15
لـكن من قال أنه لديه 15صديق ؟ ربما لديه أكثر من هذا ! عندما تكتب الشفرة المصدر ي ّة ،لا يمكنك
معرفة حجم الجدول قبل التشغيل ،عندما تطلب من المستعمل إدخال عدد الأصدقاء.
فائدة الحجز الح ّي موجودة هنا :تطلب من المستخدم إدخال عدد الأصدقاء ،ثم تنشئ جدولا لديه تماما الحجم
اللازم )لا أصغر و لا أكبر( .إن كان للمستخدم 15صديقا ،فسننشئ جدولا من ، int 15إن كان لديه
،28فسننشئ جدولا من ، int 28إلخ.
كما عل ّمتكم ،من الممنوع في لغة الـ Cإنشاء جدول بتحديد حجمه باستخدام متغيّر :
;]1 int friends[friendsNumber
232
الحجز الح ّي لجدول
! من المنصوح به عدم استخدامها،هذه الشفرة قد تعمل في بعض المترجمات لـكن في حالات معي ّنة
و هذا بفضل. friendsNumber هي أن ّه يمكننا من إنشاء جدول حجمه تماما المتغيّر،فائدة الحجز الح ّي
! شفرة تعمل في ك ّل مكان
: بايت في الذاكرةfriendsNumber * sizeof(int) حجزmalloc سنطلب من
1 friends = malloc(friendsNumber * sizeof(int));
! حجمه بوافق تماما عدد الأصدقاءint هذه الشفرة تسمح بإنشاء جدول من نوع
: هذا ما يقوم به البرنامج بالترتيب
. نطلب من المستخدم كم من صديق لديه.1
.( malloc ذو حجم يساوي عدد أصدقائه )باستخدامint إنشاء جدول من.2
. و نقوم بتخزينها في الجدول، نطلب كم عمر ك ّل واحد من أصدقائه واحدا واحدا.3
. نظهر أعمار الأصدقاء محتوى الجدول لنتأكد بأننا خزّننا ك ّل شيء.4
. free نقوم بتحرير بالدالة، و بما أننا لسنا بحاجة إلى الجدول الذي يحوي أعمار الأصدقاء، في النهاية.5
1 int main(int argc, char *argv[])
2
{
3 int friendsNumber = 0, i = 0;
4 int* friendsAge = NULL; // We use it after calling malloc
5 // Request for friends number
6 printf(”How many friends do you have ? ”);
7 scanf(”%d”, &friendsNumber);
8 if (friendsNumber > 0) // The user must have at least one friend (
or i will be upset :p)
9{
10 friendsAge = malloc(friendsNumber * sizeof(int)); // Allocate the
memory for the table
11 if (friendsAge == NULL) // Verify the allocation
12 {
13 exit(0); // Stop everything
14 }
15 // Request for the ages one by one
16 for (i = 0 ; i < friendsNumber ; i++)
17 {
18 printf(”How old is the friend number %d ? ”, i + 1);
19 scanf(”%d”, &friendsAge[i]);
20 }
21 // Display the stored ages
22 printf(”\n\nYour friends have the next ages :\n”);
233
الفصل ب .8.الحجز الح ّي للذاكرة )(Dynamic memory allocation
)23 for (i = 0 ; i < friendsNumber ; i++
{ 24
;)]25 printf(”%d years\n”, friendsAge[i
} 26
27 // Free the memory
;)28 free(friendsAge
} 29
;30 return 0
} 31
How many friends do you have ? 5
How old is the friend number 1 ? 16
How old is the friend number 2 ? 18
How old is the friend number 3 ? 20
How old is the friend number 4 ? 26
How old is the friend number 5 ? 27
Your friends have the next ages :
16 years
18 years
20 years
26 years
27 years
هذا البرنامج عديم الفائدة :يطلب الأعمار ثم ّ يعرضها بعد ذلك .لقد اخنرن فعل هذا لأن ّه مثال ”بسيط” )هذا
إن فهمت .( malloc
أؤكّد لك :في بقي ّة هذا الكتاب ستكون لنا فرص لاستخدام mallocفي أمور أكثر إفادة من تخزين
أعمار الأصدقاء !
مل ّخص
• كل متغير يحجز مكانا مختلفا في الذاكرة و هذا حسب نوعه.
• يمكننا معرفة عدد البايتات التي يشغلها كل نوع باستعمال العامل )(. sizeof
• الحجز الح ّي هو عبارة عن حجز يدوي لمكان في الذاكرة من أجل متغير أو من جدول.
• الحجز الح ّي يتم باستعمال الدالة )( mallocو يجب خا ّصة عدم نسيان تحرير الذاكرة باستعمال )(free
بمجر ّد الانهاء من الاستخدام.
• الحجز الح ّي يسمح بشكل أساس ّي بتعر يف جدول حجمه يتم ّ تعيينه بمتغيّر في حين تشغيل البرنامج.
234
الفصل ب9.
برمجة لعبة الـPendu
أكرر دائما :التطبيق شيء ضرور ّي .هو ضرور ّي لك لأنك اكتشفت كثيرا من المفاهيم النظر ية و ،أيّا
كان ما تقول ،لن تفهمها ح ّقا بدون تطبيق.
في هذا العمل التطبيقي ،أقترح عليك إنشاء لعبة الـ .Penduو هي لعبة حروف تقليدي ّة يتم ّ فيها تخمين كلمة
سر ي ّة حرفا بحرف .و الـ Penduسيكون إذن لعبة في الـكونسول بلغة .C
الهدف هو جعلك تستخدم ك ّل ما تعل ّمته حت ّى الآن :المؤشرات ،السلاسل المحرفي ّة ،الملفات ،الجداول...
باختصار ،الأشياء الجي ّدة فقط !
ب 1.9.التعليمات
سأقوم بشرح قواعد الـ Penduالواجب إنشاءه .سأعطيك هنا التعليمات ،أي سأشرح لك بدق ّة كيف يجب
أن تعمل اللعبة التي ست ُنشئها.
أعتقد أن الجميع يعرف الـ ،Penduأليس كذلك ؟ هي ّا ،تذكير صغير لا يمكن أن يحدث ضررا :هدف
الـ Penduهو إيجاد الكلمة المخب ّأة في أق ّل من عشر محاولات )يمكنك تغيير العدد الأقصى لتغيير صعوبة اللعبة،
بالطبع !(.
سر يان الجولة
فلنفترض أن الكلمة المخب ّأة هي .RED
ستقوم باقتراح حرف على الحاسوب ،مثلا الحرف .Aسيتأكّد الحاسوب ما إن كان هذا الحرف موجودا ً في
الكلمة المخفي ّة.
تذكّر :هناك دالة جاهزة في string.hتقوم بالبحث عن حرف في كلمة ! و بالطبع أنت لست مجـبرا ً
على استخدامها )شخصي ّا ،أنا لم أفعل(.
إنطلاقا ًمن هنا ،يوجد احتمالان :
235
الفصل ب .9.برمجة لعبة الـPendu
• الحرف موجود بالفعل في الكلمة :سنكشف مكان الحرف في الكلمة.
• الحرف غير موجود في الكلمة )هذا هو الحال هنا ،لأن Aليس موجودا ًفي الكلمة : (REDسنخبر اللاعب
بأن الحرف هذا غير موجود في الكلمة ،و سننقص عدد المحاولات المتب ّقية .عندما لا ٺتبق أية محاولة )0
محاولة( ،ستنتهي اللعبة و سنخسر.
في لعبة ” Penduحقيقة” ،يفترض وجود شخص يتأ ّسف في ك ّل م ّره نخطئ فيها .في الـكونسول ،سيكون
من الصعب كثيرا رسم شخص يتأ ّسف بواسطة لاشيء غير النص ،لذا سنكتفي بعرض جملة بسيطة مثل
”بقي لك Xمحاولات قبل الموت الأكيد”.
فلنفرض الآن أن اللاعب أدخل الحرف .Dهذا الحرف موجود في الكلمة المخفي ّة ،لهذا لن نقوم بإنقاص
عدد المحاولات المتب ّقية للاعب .سنقوم بإظهار الكلمة مع الحروف ال ّتي تم إيجادها ،أي شيء كهذا :
Secret word : **D
إذا أدخل اللاعب فيما بعد الحرف ،Rو بما أن ّه موجود في الكلمة ،سنضيف الحرف إلى قائمة الحروف التي
تم إيجادها و يتم إظهار الكلمة مع الحروف ال ّتي تم ّ اكتشافها :
Secret word : R*D
حالة وجود حرف مكرر
في بعض الكلمات ،يمكن أن نجد حرفا ًمكررا ً مرتين أو ثلاث ،أو رب ّما أكثر !
مثلا :يوجد إثنان من Zفي كلمة ،PUZZLEو كذلك يوجد ثلاثة Eفي كلمة .ELEMENT
ماذا علينا أن نفعل في حالة كهذه ؟ قواعد Penduواضحة :إذا أدخل اللاعب الحرف ،Eك ّل حروف E
في كلمة ELEMENTيجب أن تظهر دفعة واحدة :
**Secret word : E*E*E
يعني أنه ليس على اللاعب أن يدخل 3مرات الحرف Eليتم إكتشاف كل تكرار له في الكلمة.
مثال عن جولة كاملة
هذا ما ستبدو عليه جولة كاملة في الـكونسول عند انتهاء البرنامج :
236
التعليمات
Welcome !
You have 10 remaining tries
What’s the secret word ? ****
Suggest a letter : B
You have 9 remaining tries
What’s the secret word ? ****
Suggest a letter : F
You have 9 remaining tries
What’s the secret word ? F***
Suggest a letter : D
You have 9 remaining tries
What’s the secret word ? F**D
Suggest a letter : O
You win ! The secret word is : FOOD
قراءة حرف من الـكونسول
.قراءة حرف من الـكونسول هي أكثر تعقيدا ً مم ّا تبدو
: يفترض أن ّك تفكّر في، لاسترجاع محرف،بديهي ّا
1 scanf(”%c”, &myLetter);
)متغيّر من نوعmyLetter و الذي سنقوم بتخزينه في،ً تعني أننا ننتظر محرفا%c . هذا جي ّد،و تماما
.( char
: يمكنك تجريب الشفرة التالية. م ّرة اخرىscanf ما دمنا لم نقم بـ...ً كل شيء يعمل جيدا
1 int main(int argc, char* argv[])
2
{
3 char myLetter = 0;
4 scanf(”%c”, &myLetter);
5 printf(”%c”, myLetter);
6 scanf(”%c”, &myLetter);
7 printf(”%c”, myLetter);
8 return 0;
9}
. و ذلك لمر ّتين،يفترض بهذه الشفرة أن تطلب حرفا ًو تظهره
فهو لا يطلب منك، البرنامج يتوق ّف مباشرة بعدها... و لـكن، نعم، ما الذي يحصل ؟ تدخل حرفا.جرّب
. الثانيةscanf المحرف الثاني ! و كأنه تم تجاهل
ما الذي حصل ؟
237
الفصل ب .9.برمجة لعبة الـPendu
في الواقع ،حينما تدخل نصا ً في الـكونسول ،فإن كل ما قمت بإدخاله يتم ّ تخزينه في الذاكرة ،بما في ذلك
الزر .( \n ) Enter
لذلك ،في أ ّول م ّرة تدخل فيها حرفا ) Aمثلا ً( ثم ّ تضغط على Enterفإن الحرف Aهو من يتم إعادته من
طرف . scanfبينما في المر ّة الثانية scanf ،سيعيد \nالموافق لـ Enterال ّذي أدخلته سابقا !
لتجنب هذا ،من الأحسن أن نكتب بأنفسنا دالتنا الخا ّصة الصغيرة )(: readCharacter
)(1 char readCharacter
{2
;3 char character = 0
4 character = getchar(); // Read the first character
5 character = toupper(character); // Convert the character to
uppercase
)6 // Read other characters until reaching \n (to erase them
; )’7 while (getchar() != ’\n
8 return character; // Return the first character that have been read
}9
هذه الدالة تستخدم )( getcharال ّتي هي دالة من stdioو هذا يعود تماما ًإلى كتابة
;) . scanf(”%c”, &letterالدالة )( getcharتقوم بإرجاع المحرف الذي قام اللاعب بإدخاله.
بعد ذلك ،أستعمل أيضا ًالدالة القياسي ّة التي لم تسنح لنا فرصة تعل ّمها في كتابنا . toupper() :هذه الدال ّة
تحوّل الحرف المعطى إلى كبير ) .(Uppercaseهكّذا ،اللعبة ستعمل حتى إن أدخل اللاعب حروفا ً صغيرة.
يجب تضمين ctype.hلتستطيع استخدام هذه الدالة )لا تنس ذلك !(.
تأتي بعد ذلك المرحلة الأكثر أهمية :و هي أن نقوم بمسح المحارف التي يمكن أن نكون قد أدخلناها .في
الواقع ،بإعادة استدعاء getcharنحصل على المحرف الثاني ال ّذي تم ّ إدخاله )مثلا .( \n
ما أقوم به بسيط و يأخذ سطرا واحدا :أستدعي الدالة getcharفي حلقة تكرار ية حتى الوصول إلى . \n
ٺتوقف الحلقة إذن ،و هذا يعني أننا ”قرأنا” ك ّل المحارف الأخرى ،سيتم ّ إذن إفراغها من الذاكرة .نقول أنّنا
نفرغ المتغير المؤقت ).(Buffer
لماذا توجد فاصلة منقوطة في نهاية الـ whileو لماذا لا نرى أية حاضنة ؟
في الواقع ،استعملت حلقة تكرار ية لا تحتوي على تعليمات )التعليمة الوحيدة ،هي getcharداخل
القوسين( .الحاضنتان ليستا ضروريّتين نظرا لأنه ليس لدينا ما نفعله غير . getcharلهذا أضع فاصلة منقوطة
لتعو يض الحاضنتين .هذه الفاصلة المنقوطة تعني ”لا تفعل شيئا ً في ك ّل دورة للحلقة” .هذا أمر غريب قليلا،
لـكنها تقني ّة يجب معرفتها ،تقني ّة يستعملها المبرمجون لانشاء حلقات بسيطة و قصيرة.
اعلم أ ّن الـ whileكان بالإمكان كتابتها هكذا :
238
التعليمات
)’1 while (getchar() != ’\n
{2
3
}4
لا يوجد شيء داخل الحاضنتين ،إ ّنها تطوّعي ّة ،نظرا لأن ّه ليس هناك شيء آخر لفعله .تقني ّتي ال ّتي تقتضي
وضع فاصلة منقوطة فقط أبسط من تلك الخا ّصة بالحاضنتين.
أخيرا ،تقوم الدالة readCharacterبإرجاع المحرف الأ ّول الذي قمنا بقراءته :المتغي ّر . character
خلاصة القول ،في شفرتك ،لا تستعمل :
;)1 scanf(”%c”, &myLetter
و إنما استعمل بدل ذلك دال ّتنا الرائعة :
;)(1 myLetter = readCharacter
قاموس الكلمات
لتجربة أولية للشفرة الخاصة بك ،أطلب منك أن تقوم بتثبيت الكلمة السر ي ّة مباشرة في الشفرة .أكتب مثلا :
;”1 char secretWord[] = ”RED
طبعا ستبقى الكلمة السر ي ّة نفسها دائما إن تركناها هكذا ،هذا ليس ممتعا .لـكني طلبت منك فعل ذلك لـكي
لا تخلط المشاكل .في الواقع ،عندما تعمل لعبة Penduجي ّدا )و فقط ابتداء من هذه اللحظة( ،يمكنك البدء
بالطور الثاني :إنشاء قاموس الكلمات.
ما هو هذا ”قاموس الكلمات” ؟
هو ملف يحتوي كثيرا من الكلمات للعبتك .Penduيجب أن تكون كل كلمة على سطر .مثلا :
HOUSE
BLUE
AIRPLANE
XYLOPHONE
BEE
BUILDING
WEIGHT
SNOW
ZERO
239
الفصل ب .9.برمجة لعبة الـPendu
في كل جولة جديدة ،يجب على برنامجك أن يفتح الملف ،و يأخذ كلمة عشوائية من القائمة .بفضل هذه
الطر يقة ،سيكون لديك ملف يمكنك التعديل عليه كلّما أردت من أجل إضافة كلمات سر ي ّة ممكنة من أجل
.Pendu
ستلاحظ أنني منذ البداية تعمّدت كتابة ك ّل الكلمات بالحروف الـكبيرة .في الواقع ،في الـ Penduلا
يتم التمييز بين الحروف الـكبيرة و الحروف الصغيرة ،و لهذا فمن المستحسن أن نقول منذ البداية ” :كل
حروف كلمات اللعبة كبيرة” .عليك أن تنب ّه اللاعب ،في دليل استخدام اللعبة مثلا ،أنه يفترض به
إدخال حروف كبيرة لا صغيرة.
بالمقابل ،نتعمّد تجنب العلامات الصوتية ) (accentsلتبسيط اللعبة )إن بدأنا اختبار ...ë ،ê ،è ،éفلن
ننتهي أبدا ً !( .عليك إذن أن تكتب كلماتك كل ّها بحروف كبيرة و بدون علامات صوتيّة.
المشكل الذي سيحدث لك سر يعا هو أنه عليك معرفة عدد الكلمات الموجودة في القاموس .في الواقع،
إن أردت إختيار كلمة عشوائية ،يجب أن يتم أخذ عدد بين 0و ،Xو أنت لا تعرف في بادئ الأمر كم من
الكلمات يحتوي الملف.
لح ّل هذا المشكل ،يوجد حل ّان .يمكنك أن تشير في السطر الأول من المل ّف إلى عدد الكلمات ال ّتي يحويها :
3
HOUSE
BLUE
AIRPLANE
إلا أن هذه الطر يقة مملة ،لأنه يجب إعادة حساب عدد الكلمات يدو يا في ك ّل م ّرة تضيف فيها كلمة )أو
إضافة 1إلى هذا العدد إن كنت ماكرا بدل إعادة الحساب ،لـكنّها تبقى طر يقة بدائيّة قليلا( .لهذا ،أقترح
عليك أن تع ّد تلقائيّا عدد الكلمات عن طر يق قراءة الملف م ّرة أولى باستخدام برنامجك .معرفة كم يوجد من
كلمات أمر بسيط :عليك ع ّد الـ ) \nالعودة إلى السطر( في الملف.
حينما تقرأ المل ّف في م ّرة أولى لع ّد ، \nفعليك القيام بـ rewindللعودة إلى البداية .لن يكون عليك إذن
سوى أخذ عدد عشوائيّ بين عدد الكلمات ال ّتي عددتها ،ثم ّ عليك تخزين هذه الكلمة في سلسلة محرفي ّة في الذاكرة.
سأتركك قليلا لتفكّر في ك ّل هذا ،لن أساعدك أكثر ،و إلّا فلن يكون عملا تطبيقيا ! و اعلم بأن ك ّل المعارف
ال ّتي تحتاجها موجودة في الفصول السابقة ،فأنت قادر تماما على إنشاء هذه اللعبة .إنه يتطل ّب منك بعض الوقت
و هو أق ّل سهولة مم ّا يبدو عليه ،و لـكن إذا ن ّظمت الأمور جي ّدا )بإنشاء قدر كاف من الدوال( سوف تصل.
بالتوفيق !
ب 2.9.التصحيح ) : 1شفرة اللعبة(
240
التصحيح ) : 1شفرة اللعبة(
بقراءتك لهذه السطور ،يعني أنك قد أكملت البرنامج ،أو أنك لم تستطع إكماله.
لقد استغرقت شخصي ّا وقتا أكبر مم ّا كنت أعتقد في إنشاء هذه اللعبة البسيطة للغاية .هكذا دائما :نقول ”هذا
بسيط” ،لـكن في الحقيقة توجد الـكثير من الحالات لدراستها.
رغم ذلك أصرّ على القول بأنك قادر على فعل هذا .يلزمك فقط بعض الوقت )بضع دقائق ،بضع ساعات
بضع أيام ؟( ،لـكن ّنا لم نكن أبدا في سباق .أنا أف ّضل أن تأخذ كثيرا من الوقت للوصول إلى الحل على ألّا
تجر ّب سوى 5دقائق و ترى التصحيح.
لا تعتقد أن ّي كتبت البرنامج من المحاولة الأولى .أنا أيضا ،كنت أعمل خطوة بخطوة .بدأت بشيء بسيط
ج ّدا ،ثم ّ شيئا فشيئا ح ّسنت الشفرة للوصول إلى النتيجة النهائيّة.
قمت بع ّدة أخطاء أثناء كتابة الشفرة :نسيت في لحظة ما تهيئة متغير بشكل صحيح ،نسيت كتابة نموذج دالة و
كذلك حذف متغير لم يعد مفيدا في شفرتي .و حتى أن ّي -أعترف -نسيت فاصلة منقوطة سخيفة في لحظة ما عند
نهاية تعليمة.
لماذا أقول كل هذا ؟ لـكي أخبرك أن ّني لست معصوما من الأخطاء و أن ّي أواجه تقريبا نفس المشاكل مثلك
)”أ ّيها البرنامج البائس ،هل ستعمل أم لا !؟”(.
سأعرض عليك الح ّل على جزئين.
• أ ّولا سأر يك كيف أنشأت شفرة اللعبة نفسها ،بتثبيت الكلمة المخفي ّة مباشرة في الشفرة .إخترت الكلمة
YELLOWلأ ّنها تسمح باختبار ما إن كنت تعاملت جي ّدا مع المحارف المتكر ّرة.
• يعد ذلك ،سأر يك كيف أضفت العمل بقاموس الكلمات لأخذ كلمة سرّية عشوائيّة لللاعب.
بالطبع ،يمكنني أن أر يك الشفرة دفعة واحدة و لـكن ...سيكون هذا كثيرا في م ّرة واحدة ،و البعض لن
تكون لديه الشجاعة لمحاولة فهم الشفرة.
سأحاول أن أشرح لك خطوة بخطوة طر يقة عملي .تذكّر أ ّن ما يهم ،ليس النتيجة ،و إن ّما طر يقة التفكير.
تحليل الدالة main
مثلما يعلم الجميع ،ك ّل شيء يبدأ بـ . mainبجب ألا ننسى تضمين المكتبات stdlib ، stdioو ctype
)من أجل الدالة ( toupperال ّتي سنحتاج إليها أيضا :
>1 #include <stdio.h
>2 #include <stdlib.h
>3 #include <ctype.h
4
5 )][int main(int argc, char* argv
6
{
;7 return 0
}8
241
الفصل ب .9.برمجة لعبة الـPendu
حسنا ،لح ّد الآن يجب على الجميع أن يتابعوا.
الدالة mainستش ّكل معظم اللعبة و ستقوم باستدعاء بعض الدوال حينما تحتاج إليها.
فلنبدأ بتعر يف المتغيرات الضرور ي ّة .كن متأكّدا ،لم أفكّر في ك ّل هذه المتغيرات من الوهلة الأولى ،و لقد
كان هناك أق ّل من هذا العدد في أ ّول م ّرة كتبت فيها الشفرة !
>1 #include <stdio.h
>2 #include <stdlib.h
>3 #include <ctype.h
)][4 int main(int argc, char* argv
{5
6 char letter = 0; // Stores the letter suggested by the user
7 char secretWord[] = ”YELLOW”; // The word that the user must find
8 int foundLetter[3] = {0}; // Boolean table. Each cell corresponds
to a letter in the secret word. 0 = letter not found, 1 = letter
found
)9 int remainingTries = 10; // Counting the remaining tries (0 = dead
10 int i = 0; // A little variable to browse the table
;11 return 0
} 12
لقد كتبت بمحض إرادتي تصريح ك ّل متغير على سطر و وضعت كثيرا من التعليقات لشرح دور كل متغير.
عملي ّا ،لست مضطر ّا على وضع ك ّل هذه التعليقات كما وضع الـكثير من التصريحات في نفس السطر.
أعتقد أن أغلب المتغيرات تبدوا منطقية :المتغير letterيخز ّن الحرف الذي يدخله المستخدم في
ك ّل مرة secretWord ،يحوي الكلمة الواجب اكتشافها remainingTries .يحتوي عدد المحاولات
المتب ّقية ،إلخ .المتغيّر iهو متغير صغير استعمله كي أتص ّفح الجدول مستعملا الحلقة . forفهو ليس مهمّا
ج ّدا لـكن ّه ضرور ّي إذا أردنا القيام بحلقات.
و أخيرا ً المتغير ال ّذي يجب التفكير فيه ،و الذي سيُمث ّل الفرق ،إن ّه عبارة عن جدول من القيم المنطقية
. foundLetterستلاحظ بأن ّي جعلت حجم الجدول يساوي عدد حروف الكلمة السر ي ّة ) .(6هذا ليس
أمرا ً عشوائيا ً :إذ أن ك ّل خانة من جدول القيم المنطقية تمث ّل حرفا ًمن الكلمة السر ية .هكذا ،الخانة الأولى تمث ّل
الحرف الأ ّول ،الثانية الحرف الثاني ،إلخ.
ك ّل خانات الجدول مهي ّئة في البداية على ،0و التي تعني ”الحرف لم يتم إيجاده بعد” .بتق ّدم اللعبة ،الجدول سيتم ّ
تعديله .من أجل ك ّل حرف تم ّ إيجاده من الكلمة ،الخانة التي توافقها من foundLetterستأخذ .1
مثلا ،إذا كان في مرحلة من الجولة ،لدينا العرض ،Y*LL*Wفإن جدول الـ intسيحوي القيم 101101 :
) 1لكل حرف تم ّ إيجاده(.
هذه الطر يقة تسهّل علينا معرفة متى يربح اللاعب :يكفي التح ّقق من أن جميع خانات الجدول لا تحوي سوى
.1
في الحالة الأخرى ،سيخسر اللاعب إذا وصل الع ّداد remainingTriesإلى .0
فلننتقل إلى التالي :
242
التصحيح ) : 1شفرة اللعبة(
;)”1 printf(”Welcome !\n\n
هذه رسالة ترحيب ،لا يوجد أي شيء مثير فيها .بالمقابل ،الحلقة الرئيسي ّة هي الأكثر أهمي ّة :
))1 while (remainingTries > 0 && !win(foundLetter
{2
اللعبة تستمر ّ مادام قد بقي بعض المحاولات ) ( remainingTries > 0و اللاعب لم يربح.
إذا لم تبق له أية محاولة ،فهذا يعني أنه فشل .إن ربح ،فهذا يعني ...أن ّه ربح.في كلتا الحالتين ،يجب إيقاف اللعبة،
أي إيقاف الحلقة التي تطلب قراءة حرف في ك ّل مرة.
winهي دالة تقوم بتحليل الجدول . foundLetterتقوم بإعادة ”صحيح” ) (1إذا كان اللاعب قد
ربح )أي أن الجدول foundLetterلا يحمل سوى ” ،(1خطأ” ) (0إن كان لم يربح بعد .لن أشرح لك
الآن عمل الدالة بشكل مفصل ،سنرى ذلك لاحقا ً .حالي ّا ،يجب عليك فقط معرفة ما تفعله.
باقي الشفرة :
;)1 printf(”\n\nYou have %d remaining tries”, remainingTries
;)” ? 2 printf(”\nWhat’s the secret word
)3 for (i = 0 ; i < 3 ; i++
{4
5 if (foundLetter[i]) // If the letter n° i has been found
6 printf(”%c”, secretWord[i]); // Display it
7 else
8 printf(”*”); // Else, display * for the letters that are not
found
}9
نقوم في كل مرة بإظهار عدد المحاولات المتب ّقية و كذا الكلمة السر ي ّة )مخفي ّة بـ* بالنسبة للحروف التي لم يتم ّ
إ يجادها(.
يتم إظهار الكلمة السر ي ّة المخفي ّة بـ* بفضل حلقة forحيث أننا نحل ّل ك ّل حرف لنرى إن تم ّ إيجاده
) )] .( if(foundLetter[iإن كان الشرط محققا ً ،سنظهر الحرف ،و إلا سنظهر * لإخفاءه.
الآن بعدما أظهرنا ما يجب ،سنطلب من اللاعب أن يدخل حرفا ًجديدا ً :
;)” 1 printf(”\nSuggest a letter :
;)(2 letter = readCharacter
أستدعي دالتنا )( . readCharacterهذه الدالة تقرأ الحرف الأول الذي تم ّ إدخاله ،تجعله كبيرا ً ثم
تفر ّغ المتغير المؤق ّت ،أي أ ّنها تمسح بقي ّة الحروف التي يمكن أن تبقى في الذاكرة.
243
الفصل ب .9.برمجة لعبة الـPendu
1 // if it’s NOT the right letter
))2 if (!findLetter(letter, secretWord, foundLetter
{3
4 remainingTries−−; // Decrement the remaining tries
}5
}6
نختبر ما إن كان الحرف الذي تم ّ إدخاله موجودا في . secretWordنستدعي لأجل هذا دال ّة أنشأناها
تسمّى . findLetterسنرى بعد قليل شفرة هذه الدال ّة.
حالي ّا ،ك ّل ما يجب أن تعرفه ،هو أ ّن هذه الدال ّة تعيد ”صحيح” إن كان الحرف موجودا في الكلمة” ،خطأ” إن
لم تجده.
كما تلاحظ فالـ ifيبدأ بعلامة تع ّجب ! و التي تعني ”لا” .الشرط ي ُقرأ إذن بهذه الطر يقة ” :إذا لم يتم
إيجاد الحرف”.
ماذا نفعل في حالة عدم إيجاد الحرف ؟ نقوم بتقليل عدد المحاولات المتبقية.
لاحظ أيضا ًأن الدالة findLetterتقوم بتحديث قيم الجدول . foundLetterتقوم بوضع 1في
الخانات الموافقة للحروف التي تم ّ إيجادها.
الحلقة الرئيسية في اللعبة ٺتوقف هنا .لهذا فسنعيد من بداية الحلقة و نختبر ما إن كان قد بقي شيء من
المحاولات للعب و اللاعب لم بربح بعد.
عند الخروج من الحلقة الرئيسي ّة ،لا يبقى سوى إظهار إن كان اللاعب قد نجح في اللعبة أو خسر قبل إنهاء
البرنامج :
))1 if (win(foundLetter
;) 2 printf(”\n\nYou win! the secret word is : %s”, secretWord
3 else
) 4 printf(”\n\nYou lose ! the secret word is : %s”, secretWord
;
;5 return 0
}6
سنستدعي الدالة winلنرى ما إن كان اللاعب قد ربح .إن كانت هذه هي الحالة ،نقوم بإظهار الرسالة
”ربح !” ،و إلّا ،فقد انتهت فرص اللعب ،فقد خسر.
تحليل الدالة win
فلنرى الآن الشفرة الخاصة بالدالة : win
244
التصحيح ) : 1شفرة اللعبة(
)][1 int win(int foundLetter
{2
;3 int i = 0
;4 int playerWins = 1
)5 for (i = 0 ; i < 3 ; i++
{6
)7 if (foundLetter[i] == 0
;8 playerWins = 0
}9
;10 return playerWins
} 11
هذه الدالة تأخذ جدول القيم المنطقي ّة foundLetterكمعامل .تعيد قيمة منطقية ” :صحيح” إذا ربح
اللاعب و ”خطأ” إذا خسِر.
الشفرة الخاصة بهذه الدالة بسيطة ،بفترض بك فهمها .نتص ّفح foundLetterو نختبر ما إن كانت
إحدى خانات الجدول تحوي ”خطأ” ) .(0إن كان هناك حرف واحد لم يتم ّ إيجاده فلقد خسر اللاعب :
سيتم وضع ”خطأ” ) (0في المتغير المنطقي . playerWinsو إلّا ،إن تم ّ إيجاد كل الحروف ،فالمتغيّر المنطقي
سيكون ”صحيح” ) (1و الدالة تعيد ”صحيح”.
تحليل الدالة findLetter
لهذه الدالة مهمّتان :
• إرجاع متغير منطقي يشير ما إن كان الحرف موجودا ً في الكلمة السر ية.
• تحديث )على (1خانات الجدول foundLetterفي المواضع الموافقة للحرف الذي تم ّ إيجاده.
)][1 int findLetter(char letter, char secretWord[], int foundLetter
{2
;3 int i = 0
;4 int rightLetter = 0
5 // Search for the letter in the table foundLetter
)6 for (i = 0 ; secretWord[i] != ’\0’ ; i++
{7
8 if (letter == secretWord[i]) // If it exists
{9
10 rightLetter = 1; // Memorize that it was the right one
11 foundLetter[i] = 1; // Put the correspondent value to 1 in the
table
} 12
} 13
; 14 return rightLetter
} 15
245
الفصل ب .9.برمجة لعبة الـPendu
نتص ّفح إذن السلسلة المحرفي ّة secretWordمحرفا ً محرفا ً .في ك ّل م ّرة ،نختبر ما إن كان الحرف الذي
اقترحه اللاعب حرف من الكلمة ،سيتم القيام بأمرين :
• تعديل المتغير المنطقي ، rightLetterإلى ،1لـكي تعيد الدال ّة 1لأن الحرف متواجد بالفعل في
. secretWord
• تحديث الجدول foundLetterعلى الموضع الحالي للإشارة إلى أ ّن هذا الحرف قد تم ّ إيجاده.
الشيء الجي ّد في هذه الطر يقة ،هو أننا سنتص ّفح ك ّل الجدول )لا نتوقف عند أول حرف تم إيجاده( .هذا
سيسمح لنا بتحديث الجدول foundLetterبشكل صحيح ،في الحالة التي تحتوي فيها الكلمة حرفا ًمكررا ً ع ّدة
م ّرات ،مثل حالة Lفي .YELLOW
ب 3.9.التصحيح ) : 2استعمال قاموس الكلمات(
لقد قمنا بجولة حول الوضائف الأساسي ّة لبرنامجنا .إن ّه يحتوي على ك ّل ماهو ضروري لإدارة جولة في اللعبة،
لـكنه لا يعرف كيف يختار كلمة عشوائية من قاموس كلمات .لم أضع لك شفرة المرحلة الأولى كل ّها لأنها
كانت ستأخذ حجما ًكبيرا ً كما ستكون تكرارا مع الشفرة المصدر ي ّة النهائيّة ال ّتي ستراها فيما بعد.
قبل الذهاب بعيدا ،الشيء الأ ّول الواجب فعله هو إنشاء قاموس الكلمات .و حتى إن كان قصيرا ً فهذا
ليس سي ّئا ،سيكون مناسبا للاختبارات.
سأقوم إذن بإنشاء ملف dico.txtفي نفس دليل مشروعي .حالي ّا ،سأضع فيه الكلمات التالية :
1 HOUSE
2 BLUE
3 AIRPLANE
4 XYLOPHONE
5 BEE
6 BUILDING
7 WEIGHT
8 SNOW
9 ZERO
ما إن أنتهي من كتابة البرنامج ،سأعود بالطبع إلى هذا القاموس و أملؤه بالـكثير من الكلمات الغريبة
كـ XYLOPHONEو المطوّلة كـ .ANTIDISESTABLISHMENTARIANISMلـكن حالي ّا ،لنعد إلى كتابة التعليمات.
تحضير الملفات الجديدة
قراءة ” ”dicoسيأخذ الـكثير من الأسطر )على الأقل ،لد ّي إحساس قبليّ بذلك( .لهذا فسآخذ الاحتياطات
بإضافة ملف آخر إلى مشروعي ) dico.cالذي سيتك ّفل بقراءة .(dicoكما سن َقُوم بإنشاء dico.hال ّذي
يحوي نماذج الدوال الموجودة في . dico.c
246
التصحيح ) : 2استعمال قاموس الكلمات(
في dico.cسأبدأ بتضمين المكتبات ال ّتي أنا في حاجة إليها بالإضافة إلى . dico.hأ ّولا ،كالعادة،
سأحتاج إلى stdio.hو stdlib.hهنا .بالإضافة إلى هذا ،يجب عليّ أن أقوم بسحب عشوائي لعدد
من القاموس ،سأقوم إذن بتضمين time.hمثلما فعلنا سابقا ً من أجل العمل التطبيقي الأول ”أكثر أو
أقل”.سأحتاج أيضا إلى تضمين string.hمن أجل استعمال strlenفي نهاية الدال ّة.
>1 #include <stdio.h
>2 #include <stdlib.h
>3 #include <time.h
>4 #include <string.h
5
”6 #include ”dico.h
الدالة findWord
هذه الدالة تأخذ معاملا واحدا :مؤشرا ً نحو الذاكرة حيث يمكن كتابة الكلمة .هذا المؤشّر يتم تزويدنا به عن
طر يق . main
الدالة ستعيد intو سيكون قيمة منطقية = 1 :تم ّ ك ّل شيء على مايرام = 0 ،كان هناك خطأ ما.
هذه بداية الدالة :
1 )int findWord(char *chosenWord
2
{
3 FILE* dico = NULL; // The pointer of the file
;4 int wordsNumber = 0, chosenWordNumber = 0, i = 0
;5 int readCharacter = 0
أع ّرف بعض المتغيرات ال ّتي ستكون ضرور ية لي .مثل الـ ، mainلم يخطر ببالي وضعها كل ّها من البداية،
يوجد بالتأكيد من قمت بإضافتها لاحقا حينما عرفت أنني بحاجة إليها.
أسماء الكلمات تعبّر عن نفسها .لدينا المؤش ّر على القاموس dicoو الذي سيمكننا من قراءة ، dico.txt
متغيرات مؤق ّتة ستخز ّن المحارف ،إلخ .لاحظ أنني هنا استعملت intلتخزين محرف ) ( characterRead
لأ ّن الدالة fgetcال ّتي سأستخدمها تعيد . intفمن الأفضل إذن تخزين النتيجة في . int
فلنمر إلى التالي :
1 dico = fopen(”dico.txt”, ”r”); // Open the dictionary in read mode
only
2 // Check if it’s open without a problem
3 if (dico == NULL) // If there’s a problem
{4
;)”5 printf(”\nImpossible to load words dictionary
6 return 0; // Return a zero to say that the function failed
7 // The function stops after reading the instruction return
}8
247
الفصل ب .9.برمجة لعبة الـPendu
ليس لد ّي الـكثير لأضيفه هنا .أفتح الملف dico.txtبوضع قراءة فقط ) ” ( ”rو تأكّد إن نجحت
عن طر يق إختبار إذا كان dicoيحمل القيمة NULLفإن عملية فتح الملف قد فشلت )مل ّف غير موجود أو
مفتوح من طرف برنامج آخر( .في هذه الحالة سنظهر رسالة خطأ و نقوم بـ . return 0
لماذا تضع returnهنا ؟ في الحقيقة ،التعليمة returnتضع نهاية للدالة .إذا لم يتم فتح القاموس،
فستتوقف الدالة و لن يذهب الحاسوب إلى أبعد من ذلك .إعادة 0تشير للـ mainأ ّن الدال ّة قد فشلت.
في ما يلي من الدال ّة نفترض أن فتح الملف نجح.
)1 // Count the number of words in the file (Just counting the \n signs
2 do
{3
;)4 characterRead = fgetc(dico
)’5 if (characterRead == ’\n
;6 wordsNumber++
;)7 } while(characterRead != EOF
هنا ،نتص ّفح ك ّل الملف دفعة واحدة باستعمال ) fgetcمحرفا بمحرف( .نع ّد الـ \nالتي نجدها .أي أنه
في ك ّل مرة نلتقي بـ \nنزيد قيمة المتغير . wordsNumber
بفضل هذه الشفرة سنتحصل في المتغير wordsNumberعلى عدد الكلمات الموجودة في الملف .تذكّر بأن
الملف يحتوي على كلمة في كل سطر.
1 chosenWordNumber = aleatoryNumber(wordsNumber); // Take a word by
hazard
هنا أستدعي دال ّة من إنشائي تختار لي عددا عشوائيا بين 1و ) wordsNumberالمعامل الذي ترسله
للدالة(.
إنها دالة بسيطة وضعتها أيضا ً في الملف ) dico.cسأشرحها بشكل مو ّسع لاحقا ً( .باختصار ،تقوم بإرجاع
عدد )يوافق رقم سطر الكلمة في الملف( عشوائيّ يتم تخزينه في . chosenWordNumber
1 // Start reading the file from the beginning and stop when finding
the right word
;)2 rewind(dico
)3 while (chosenWordNumber > 0
{4
;)5 characterRead = fgetc(dico
)’6 if (characterRead == ’\n
;7 chosenWordNumber −−
}8
و الآن و نحن نملك رقم الكلمة التي سنختارها ،سنعود إلى بداية الملف باستدعاء )( ، rewindو سنتص ّفح
الملف محرفا بمحرف لنحسب عدد . \nهذه المر ّة ،سنقوم بانقاص قيمة . chosenWordNumberإن اخترنا
مثلا الكلمة رقم ،5في ك ّل ادخال سيتم ّ إنقاص المتغير chosenWordNumberبواحد.
248
التصحيح ) : 2استعمال قاموس الكلمات(
سيأخذ إذن القيم 4ثم 3ثم 2ثم 1ثم .0
عندما يصل المتغير إلى ،0نخرج من الـ ، whileلأن الشرط chosenWordNumber > 0لم يعد محققا.
هذا الجزء من الشفرة ،و الذي يجب عليك فهمه حتما ،سير يك كيفية تص ّفح الملف للوصول إلى المكان
المراد .الأمر ليس مع ّقدا و لـكنه ليس ”بديهيا” أيضا ً .كن متأكّدا من فهم ما أقوم بفعله هنا.
الآن ،يفترض أن نملك مؤشّرا متموضعا تماما ً قبل الكلمة السر ي ّة ال ّتي يجب إيجادها .سنقوم بتخزينها في
) chosenWordNumberالمعامل الذي تستقبله الدالة( بفضل fgetsبسيط يقوم بقراءة الكلمة :
1 /* The cursor of the file is placed in the best place.
2 Nothing is needed more than an fgets that will read the line */
3
;)fgets(chosenWord, 100, dico
4 // We erase the \n at the end of the word
;’5 chosenWord[strlen(chosenWord) − 1] = ’\0
نحن نطلب من fgetsألا تقرأ أكثر من 100محرف )هذا هو حجم الجدول ، chosenWordالذي
قمنا بتعر يفه في الـ .( mainتذكّر أ ّن fgetsتقرأ سطرا كاملا ،بما في ذلك . \nبما أننا لا نريد إبقاء الـ \n
في الكلمة النهائية ،نحذفها باستبدالها بـ . \0لهذا تأثير القيام بقطع الكلمة قبل . \n
و ها نحن ذا ! لقد خزّنا الكلمة السر ية في الذاكرة عند عنوان . chosenWord
لم يتب ّق سوى غلق الملف ،و إعادة القيمة 1لتتوقف الدالة و تشير إلى أن كل شيء على ما ي ُرام :
;)1 fclose(dico
2 return 1; // Everything is okay, return 1
}3
انتهينا من الدالة ! findWord
الدالة aleatoryNumber
هذه الدالة التي وعدتكم بشرحها سابقا ً .نختار عددا ً عشوائيا و نعيده :
)1 int aleatoryNumber(int maxNumber
{2
;))3 srand(time(NULL
;)4 return (rand() % maxNumber
}5
السطر الأ ّول يهي ّؤ مول ّد القيم العشوائية ،مثلما تعل ّمنا فعل ذلك في العمل التطبيقي الأ ّول ”أكثر أو أقل” .أما
السطر الثاني فيقوم باختيار عدد عشوائي بين 0و maxNumberو يعيده .يمكنك ملاحظة أنني قمت بك ّل
ذلك في سطر واحد ،هذا ممكن بك ّل تأكيد ،رغم أن ّه قد يبدو أحيانا أقل قابلي ّة للقراءة.
249
Pendu برمجة لعبة الـ.9.الفصل ب
dico.h الملف
التي كنت قد طلبت منك#ifndef يمكنك أن تلاحظ ”الحماية” التي تق ّدمها.يحتوي نماذج الدوال فقط
: ( )راجع درس توجيهات المعالج في حالة الحاجة.h تضمينها في ك ّل ملفاتك ذات الإمتداد
1 #ifndef DEF_DICO
2 #define DEF_DICO
3 int findWord(char *chosenWord);
4
int aleatoryNumber(int maxNumber);
5 #endif
dico.c الملف
: كاملاdico.c هذا هو الملف
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <time.h>
4 #include <string.h>
5 #include ”dico.h”
6 int findWord(char *chosenWord)
7{
8 FILE* dico = NULL; // The pointer on the file
9
int wordsNumber = 0, chosenWordNumber = 0, i = 0;
10 int characterRead = 0;
11 dico = fopen(”dico.txt”, ”r”); // Open the dictionary in read
mode only
12 // Check if ’its open without a problem
13 if (dico == NULL) // If there’s a problem
14 {
15 printf(”\nImpossible to load words dictionary”);
16 return 0; // Return a zero to say that the function failed
17 // The function stops after reading the instruction return
18 }
19 // Count the number of words in the file (just count the \n
characters)
20 do
21 {
22 characterRead = fgetc(dico);
23 if (characterRead == ’\n’)
24 wordsNumber++;
25 }
26 chosenWordNumber = aleatoryNumber(wordsNumber); // Take a word by
hazard
27 // Start reading the file from the beginning and stop after
finding the right word
28 rewind(dico);
29 while (chosenWordNumber > 0)
250