هذه المقالة محمية ضمن الحقوق الفكرية لشركة In|Framez Technology Corp.، ومرخصة للعرض فقط ضمن الشبكة العربية لمطوري الألعاب مع الموافقة الصريحة من المؤلف وشركة In|Framez Technology Corp.. لا يسمح بإعادة نشر هذه المقالة أو تعديلها دون الرجوع للمؤلف. يمنع النسخ والاقتباس دون ذكر المصدر والموافقة من المؤلف.
حمل الملفات المرفقة مع هذه المقالة
حسناً، لقد بنيتَ برنامجك الخالي من الأخطاء الهجائية (syntax errors) و أخطاء البناء (build errors)، بقي عليك الآن حلّ تلك المشاكل المنطقية (logical errors) التي تمنع برنامجك من العمل بصورة سليمة، يسمى هذا الموضوع : Debugging.
يوفر Visual Studio من Microsoft مجموعة تسهيلات تتيح لك تقصي هذه الأخطاء المنطقية و كشفها. فمثلاً، يمكنك إيقاف تنفيذ البرنامج في أي وقت لمراقبة قيم المتغيرات و تتبع أين يقع الـ code في خطأ غير متوقع.
إن التقنيات و الأمثلة الموجودة في هذا المقال قد تم تطبيقها و تجريبها ضمن بيئة Microsoft Visual Studio .NET 1.0 المتكاملة. هذه المفاهيم أغلبها ينطبق أيضاً على الإصدارات الأقدم من البرنامج نفسه، و على أي بيئة تطوير (development environment) أخرى تحترم نفسها.
لماذا تمّ تقسيم هذا الموضوع إلى عدة أجزاء؟
لأن الموضوع (ببساطة) واسع جداً و يحتوي على الكثير من الأفكار و التقنيات التي ستحتاج إلى تطبيقها يوماً ما في أحد المواقف العويصة التي تواجهك أثناء تطوير تطبيقاتك.
النقاط التي ستتم تغطيتها اليوم
-
تعريف عملية كشف الأخطاء و تقصيها (Debugging).
-
إعداد تطبيقك للـ Debugging بشكل سليم.
-
التعرف على الأوامر الرئيسية لاستخدام الـ Debugger في Visual Studio.
و الآن، فلنبدأ ...
تعريف عملية كشف الأخطاء و تقصيها (Debugging)
ضمن عملية تطوير البرنامج (application development)، يواجه المبرمجون نوعين من الأخطاء. أخطاء هجائية (syntax errors) -أحياناً تسمى أخطاء بناء (build errors) و هو تعبير أشمل- ، و النوع الآخر و الأخطر من الأخطاء، هو الأخطاء المنطقية (logical errors).
تكمن خطورة الأخطاء المنطقية في عدم قدرة المترجم (compiler) على كشفها. لذا فقد يرتكبها المبرمج دون قصد و ينجح في بناء برنامجه بدون أي تحذير.
يبذل مترجم C/C++ قصارى جهده للكشف عن مثل هذه الأخطاء و التنبيه لها. و تندرج الكثير من تحذيراته (compiler warnings) تحت هذا الموضوع . مثلاُ التحذير رقم C4700 يظهر لينبهك إلى أنك قد قرأت قيمة من متغير غير مهيأ (uninitialized)، كما في المثال التالي:
int iNum1; // Declared, but not initialized.
int iNum2;
iNum2 = iNum1; // warning C4700: local variable 'iNum1' used without having been initialized
في المثال السلبق حاولنا إسناد قيمة iNum1 إلى المتغير iNum2. و لكن ما هي قيمة iNum1 أصلاً؟ ماذا يحتوي؟ إن C++ لا تضمن وجود قيمة معينة في أي متغير تعلن عنه. لذا فإن المثال السابق سيظهر نتائج غير متوقعة عند التنفيذ.
المثال السابق هو مثال عن حالة استطاع المترجم الكشف عنها، و لكنه في أغلب الأحيان يفشل في ذلك، و يبقى المبرمج (في غيـّهم يعمهون) ! كما في المثال الآتي:
float fNum1 = 0.0f;
float fNum2 = 10.0f;
if (fNum1 = fNum2)
printf("The numbers are equal...\n");
سأعطيك عشر ثوان لتحزر الناتج من تنفيذ البرنامج السابق.
.
.
.
.
.
كي لا أقفشك، احتفظ بالإجابة لنفسك و قارنها مع ما سأقوله الآن ...
بعد أن تقوم بترجمة المثال السابق، سينجح البناء بدون أي مشاكل (إلا في حالة خاصة سأذكرها في الأجزاء القادمة). نفذ البرنامج و ستجد أنه (يا للمفاجأة!) يظهر الرسالة (The numbers are equal...) دائماً!
كيف ؟! إن fNum1 يساوي 0 ، و fNum2 يساوي 10 !! متى قرر العالم أن 10 تساوي 0 ؟!
هدئ من روعك... دقق معي في السطر الذي يقوم بالمقارنة:
هل لاحظت أنه يستخدم معامل الإسناد (=) بدلاً من معامل المساواة (==)؟!
ما يقوم به البرنامج إذاً (في الحقيقة) أنه يعطي fNum1 القيمة 10.0 ثم يقوم بختبارها إذا كانت true أو false. و بالنسبة للقيمة 10.0 فإنها تعتبر true، و سيتحقق الشرط و ينجح الاختبار و تنفذ التعليمة printf.
إن المثال السابق هو مجرد مثال بسيط. إن وجود أخطاء منطقية في تطبيقاتك ليس عيباً، فالإنسان يخطئ طالما بقي بشراً. العيب هو أن تلاحظ وجودها و تتجاهلها ! الأمر الذي سيؤدي إلى فشل المنتج النهائي و تفادي استخدامه من قبل الزبائن.
إذن، عملية كشف الأخطاء (Debugging) هي عملية تتضمن (ليس دائماً) استخدام Debugger، و هو أداة قوية تعينك على مراقبة سير البرنامج أثناء عمله (at run time) لكشف الأخطاء المنطقية فيه.
يعتمد الكثير من المبرمجين على استخدام أوامر الخرج البسيطة (مثل MessageBox، printf وَ cout) من أجل إظهار قيم المتغيرات أثناء عمل البرنامج. إن هذا الأسلوب هو أسلوب ناجح و فعال، و لكنه يعاني من مشكلة أنك بحاجة إلى إزالة كل السطور الإضافية التي كتبتها عندما تنتهي من التجريب. بل أسوأ من ذلك، أحياناً تجد نفسك في موقف لا تستطيع فيه أن تضيف أي سطر آخر إلى الـ code. عندئذ يأتي دور الـ Debugger.
إعداد تطبيقك للـ Debugging بشكل سليم
قبل أن تبدأ في هذه العملية، هناك عدة خطوات عليك اتباعها ضمن Visual Studio كي تستطيع العمل بشكل سليم. سنقوم بإنشاء مشروع بسيط (وهمي) نتعلم منه سوية ما هي الإعدادات السليمة.
أنشئ مشروعاً جديداً في Visual Studio من النوع Console Application (سمِّـه ما تشاء).
بالزر اليمين للمؤشر، اضغط على اسم المشروع في نافذة Solution Explorer. إذا كانت هذه النافذة غير موجودة، فاضغط Ctrl+Alt+L و ستظهر ثانية. من القائمة التي ظهرت، اختر Add ثم Add New Item….
اختر Visual C++ من Categories، و C++ File (.cpp) من Templates ثم عين اسماً جديداً للملف و اضغط Open.
سيتم فتح ملف cpp جديد. سنقوم بكتابة برنامجنا ضمن هذا الملف الصغير. الآن أكتب البرنامج التالي:
#include <stdio.h>
int main(void)
{
char cMyChar = 'w';
char cHisChar;
int iInput;
printf("Guess what is the ASCII number of the character '%c' ? ",cMyChar);
// Request the user for input
scanf("%d",&iInput);
cHisChar = (char)iInput; // Cast int to char
// Show what he entered
printf("The value %d corresponds to the character '%c'...\n",iInput,cHisChar);
// Check if his answer is correct
if (cHisChar = cMyChar)
printf("Correct! Did you really memorize the whole ASCII table ?!\n\n");
else printf("Wrong! It's %d . Better luck next time!\n\n",cMyChar);
}
ملحوظة: ملفات المشروع جميعها تجدها مرفقة مع المقال.
إضغط Ctrl+F5 كي يقوم Visual Studio ببناء و تنفيذ البرنامج. لاحظ وجود نفس الخطأ المنطقي الذي ارتكبناه في آخر مثال، مما يؤدي دائماً إلى نجاح الشرط. دعه كما هو الآن.
عند إنشاء مشروع جديد في Visual Studio، ستلاحظ أنه قد قام وحده بإنشاء إعدادات لبناء مشروعك Solution Configurations. و هي Debug وَ Release. يمكنك إضافة ما تشاء لها، لكننا الآن مهتمين بالأول: Debug. تأكد أن هذا هو الإعداد الفعال لبناء المشروع عن طريق قائمة Build، فالأمر Configuration Manager. ستظهر لك نافذة كما في الصورة في الأسفل. تأكد من اختيار Debug من القائمة المنسدلة (drop-down list) المسماة Active Solution Configuration.
إذا أردت أن تطـّـلِـع على الخيارات المستخدمة لبناء المشروع في وضع Debug، اضغط بالزر الأيمن للفأرة على اسم المشروع في نافذة Solution Explorer، ثم اختر الأمر Properties من القائمة . سنمرّ على بعض هذه الخيارات في المقالات القادمة بإذن الله.
الآن أصبحنا جاهزين لاستخدام الـ Debugger.
الأوامر الرئيسية لاستخدام الـ Debugger في Visual Studio
إذا نظرنا إلى الأوامر الموجودة اعتيادياً (by default) في القائمة Debug، فإننا نكون قد أخذنا لمحة خاطفة عن أهم أوامر الـ Debugger الموجودة في Visual Studio. أحد هذه الأوامر هو Start Without Debugging و الذي استخدمناه لتنفيذ البرنامج منذ لحظات (تذكر Ctrl+F5). الأمر (command) الآخر الذي سنستعمله اليوم هو Step Over، و أمر آخر غير مذكور في القائمة يدعى Run To Cursor و يمكن استدعاؤه عن طريق الاختصار Ctrl+F10.
حسناً، فلنقم بالمحاولة الأولى :
ضع المؤشر ضمن الـ code على أول سطر قابل للتنفيذ، و هو:
الآن سننفذ الأمر Run To Cursor، اضغط Ctrl+F10 و سيبدأ البرنامج بالعمل حتى يصل إلى السطر الذي أوقفت عنده المؤشر.
رويداً رويداً... لقد تغير الوضع كثيراً الآن و ظهرت أوامر و نوافذ جديدة لم تكن ظاهرة مسبقاً... ما القصة؟
أولاً، تجنب كتابة أو تعديل أي شيء في الـ code كي لا تضطر لمواجهة ميزة Edit And Continue.
لاحظ أولاً أن شريط عنوان (title bar) Visual Studio يظهر كلمة break. و هذا معناه أن تنفيذ برنامجك متوقف الآن (suspended) أو "معطل".
لاحظ السهم الأصفر الصغير الذي يظهر إلى يسار نافذة الـ code. يدل هذا السهم على المكان الذي يقف فيه البرنامج الآن، و التعليمة التالية التي ستنفذ هي السطر الذي يقف عنده السهم (أي char cMyChar=’w’;).
أما في الجزء الأسفل من Visual Studio فهناك المزيد من الألعاب. في الحالة الاعتيادية (default) يمكنك إحصاء سبع أو ثمان نوافذ مجتمعة، اثنين منها يكون ظاهراً عادة. قارن ما تراه عندك بالصورة السابقة، يمكنك إظهار هذه النوافذ من قائمة Debug، ثم القائمة الفرعية Windows . و يمكنك إخفاء أي منها بالضغط عليها بزر الفأرة الأيمن و اختيار Hide من القائمة.
تفيد النافذة Autos بإظهار المتغيرات المستخدمة في السطر الحالي من الـ code مع عرض قيم كل منها. سنستخدم هذه النافذة بشكل رئيسي اليوم.
في Locals يمكنك مراقبة جميع المتغيرات المحلية (local variables) في الإجراء (function) الحالي.
في Watch1,2,3… يمكنك كتابة اسم متغير ما في برنامجك لتتابع قيمته باستمرار، سواء كان محلياً أم عاماً (global).
يظهر في نافذة الخرج Output معلومات و رسائل عن المكتبات التي قام برنامجك بالارتباط معها أثناء التشغيل (dynamic link libraries) بالإضافة إلى معلومات أخرى تختلف بحسب نوع التطبيق الذي تكتبه. هذه النافذة هامة جداً و لكننا سنؤجل الحديث عنها لجزء آخر بإذن الله.
تستخدم النافذة Command Window لإصدار أوامر مباشرة إلى Visual Studio (مثلاً أوامر لا تظهر عادة في القوائم)، أو تستخدم لتنفيذ أي سطر code بسيط تكتبه فيها، يمكنك أن تغير قيم المتغيرات الحالية في برنامجك، إجراء تعابير شرطية (conditional expressions) ... الخ مما تفعله عادة بالـ code النظامي.
أما Breakpoints فتظهر لك قائمة بنقاط التوقف (breakpoints) التي أعددتها في code برنامجك. و هذا أيضاً موضوع سيتم تأجيله لوقت لاحق (يبدو أنني سأؤجل كل المقال إلى وقت لاحق!).
Call Stack نافذة تفيد في إجابة السؤال: كيف وصل برنامجي إلى هذه النقطة ؟ تأتي الإجابة عن طريق لائحة بأسماء الإجراءات التي تم استدعاء الإجراء الحالي من داخلها. تظهر القائمة بالترتيب نفسه في مكـدِّس النداء (Call Stack)، و يرد فيها أنواع و قيم المتغيرات التي تم تمريرها إلى هذه الإجراءات.
حسناً، و الآن بعد أن انتهينا من حفل التعارف هذا، سنعمل سوية على استخدام هذه المزايا في كشف الخطأ الموجود في هذا البرنامج.
تأكد من ظهور نافذة Autos. ستجد أنها تخبرك بقيمة cMyChar، و هي -إلى الآن- قيمة غير محددة لأن المتغير لم تتم تهيئته بعد بقيمة ما.
اضغط F10 لتنفذ الأمر Step Over و الذي سيقوم بتنفيذ السطر الذي نقف عنده، ليتخطاه إلى السطر التنفيذي التالي (أي عند printf).
إذا ألقيت نظرة على Autos الآن فإنك ستلاحظ بعض الاختلاف. أولاً، تغيرت قيمة cMyChar و ظهرت القيمة الجديدة w بدلاً من القيمة السابقة، ثانياً لاحظ أن لونها أحمر، و هذا يدل على أن قيمة هذا المتغير قد تغيرت للتو، أي مباشرة بعد تنفيذ السطر السابق، لاحقاً "ستبرد" هذه القيمة و تعود إلى اللون الأسود. الأمر الثاني الذي نلاحظه هو ظهور متغيرين جديدين في النافذة.
الآن اضغط F10 ثانيةً. إن السطر الذي نـُـفــِّـذ يقوم بإظهار جملة في نافذة البرنامج. إذا كانت نافذة برنامجك غير مغطاة، فإنك ستلاحظ ظهور الجملة مباشرة فيها.
في السطر التالي، نستدعي الإجراء scanf لقراءة مدخلات من المستخدم. نفذ السطر و ستجد أن برنامجك قد عاد إلى الواجهة و استعاد الـ focus، و هو الآن ينتظر منك أن تدخل قيمة ما ليتم الإجراء scanf. أدخل ما تريد ثم اضغط Enter.
استمر في مراقبة النافذة Autos و ستجد المزيد من المفاجآت.
استمر في التنفيذ سطراً سطراً حتى تصل إلى الشرط ( if (cHisChar = cMyChar) ). يجب أن تكون نافذة Autos مماثلة تقريباً لتلك الموضحة في الصورة التالية. لاحظ أن جميع قيم المتغيرات "باردة" (لونها أسود).
الآن نفذ السطر و راقب ماذا ستخبرنا به نافذة Autos.
ألم تلاحظ شيئاً غريباً ؟ لقد تغيرت قيمة cHisChar و أصبح لونها أحمر! و لكن المفروض أن هذه جملة شرطية و لا تقوم بأي عملية تغيير في قيم المتغيرات. إذن كيف تغيرت قيمة cHisChar ؟! (أعرف أنك تعرف و لكن تظاهر معي بأنك لا تعرف) نعم! هناك خطأ! لقد استخدمنا معامل الإسناد = بدلاً من معامل اختبار المساواة ==... و الآن يقف السهم الأصفر عند السطر الذي يخبر المستخدم أن إجابته صحيحة.. آها الآن كشفنا الخطأ... بإمكانك الآن إيقاف تنفيذ البرنامج مباشرة (اضغط Shift+F5) أو دع البرنامج يسير بشكل طبيعي إلى نهايته المحتومة (اضغط F5).
بعد إيقاف البرنامج، ستنتهي عملية الـ Debugging و تعود المياه إلى مجاريها كما كانت سابقاً. توجه مباشرة إلى السطر المصاب و عالجه بوضع علامة = إضافية، ثم تأكد من سلامة عمل البرنامج.
فيما بعد...
حسناً، أرجو أن يكون موضوع اليوم قد نجح في وضعك على أول الطريق. حقاً إن الموضوع واسع، و كما لاحظت، فقد أجلت الكثير من الحديث إلى الأجزاء الأخرى، و لكن ما باليد حيلة. على كلٍّ، الأمر بين يديك الآن، تستطيع التجريب و اللعب بكل المزايا المذكورة هنا و ستجد نفسك قد اكتسبت خبرات جديدة و اكتشفت مزايا أخرى بنفسك.
كما ذكرت في بداية الحديث، إن المزايا التي ذكرتها في هذا المقال تتواجد في كل بيئات التطوير التي تحترم نفسها، مثل Borland C++ Builder وَ Watcom وَ (طبعاً) Visual Studio منذ إصداراته الأولى. فإذا كنت تستخدم أحد هذه الأدوات، فيمكنك بالقليل من البحث، أن تجد جميع هذه المزايا و تستخدمها بنفس الأسلوب. بل إن الإختصارات (shortcuts) المذكورة هنا هي نفسها مستخدمة في Microsoft Visual Studio 6.0!
في الملف المرفق مع هذا المقال، ستجد نفس المشروع الوهمي الذي أنشأناه هنا، و هو مبني في Microsoft Visual Studio .NET، فإذا كنت تملك إصداراً أقدم، أو برنامجاً آخر للتطوير، فيمكنك الاستفادة من الملف main.cpp و الذي يحتوي على code البرنامج مكتوباً (كما قد لاحظت) وفق مقاييس ANSI من أجل ضمان عمله تحت جميع المترجمات.
أخيراً، أبقيك على وعد مني (بإذن الله) أن أقدم لك الجزء الثاني من هذا الموضوع في أقرب فرصة. كما أنني أدعو كل من له خبرة في هذا المجال، أن يشاركنا معلوماته عن طريق كتابة مقالات إضافية عن نفس الموضوع، لتـُـنشـَر كتكملة لنفس السلسلة.
إلى اللقاء!
لمزيد من المعلومات
المصطلحات المستخدمة
المصطلح
|
معناه |
logical error |
خطأ منطقي. أحياناً يسمى بالإنجليزية (semantic error)، و يحدث عندما يقوم التطبيق بتنفيذ عملية غير مقصودة من المبرمج. هذه الأخطاء لا يستطيع المترجم أو المفسر كشفها لأنها تتبع صيغة هجائية سليمة في أغلب الأحيان . |
debug |
عملية البحث عن و اكتشاف الأخطاء المنطقية الموجودة في تطبيق معين . |
development environment |
بيئة تطوير، و هي مجموعة أدوات متكاملة يستخدمها مطورو الحلول لبناء تطبيقات من شتى الإختصاصات. من الأدوات الهامة المطلوب تواجدها في أية بيئة تطوير محترمة، وجود Debugger . |
compiler warning |
تحذير من المترجم. تختلف التحذيرات عن الأخطاء (errors) بأنه يمكن تجاهلها و استكمال عملية البناء بنجاح. تعتبر التحذيرات بمثابة نصائح أو تنبيهات تختلف بالأهمية لتلفت نظرك إلى احتمال وجود أخطاء منطقية غير مقصودة في الـ code . |
variable initialization |
تهيئة المتغير. إسناد قيمة إلى المتغير قبل أن تتم أية عمليات أخرى عليه (خاصة تلك التي تقرأ قيمة المتغير) . |
Assignment Operator = |
معامل الإسناد. يقوم بإسناد (نسخ) قيمة متغير (في الطرف الأيمن) إلى متغير آخر (على الطرف الأيسر من المعامل) . |
Equality Operator == |
معامل المساواة. يقوم هذا المعامل بالتحقق من تساوي قيم معطياته تساوياً مطلقاً. في حال تساوي القيمتين (اليسرى و اليمنى)، فإن المعامل يعيد القيمة true، و إلا فإنه يعيد false . |
Call Stack |
مكدٍّس النداء. عندما يتم استدعاء إجراء من داخل إجراء آخر، فإن الإجراء الجديد يضاف إلى هذا المكدس، لكي يستطيع الإجراء الأب استكمال التنفيذ بصورة سليمة بعد عودة الإجراء الفرعي. لا يمكن لأي إجراء أب أن يستكمل التنفيذ دون عودة جميع أبنائه . |
Input Focus |
مَصَبّ اهتمام مدخلات المستخدم. تستقبل النافذة التي تملك الـ input focus ضغطات المستخدم على لوحة المفاتيح و تعتبر نفسها محور اهتمام المستخدم حالياً . |