C++ معالجة الأخطاء

مفهوم معالجة الأخطاء

معالجة الأخطاء ( Exceptions Handling ) يقصد منها كتابة الكود الذي قد يسبب أي مشكلة في البرنامج بطريقة تضمن أنه إذا حدث الخطأ المتوقع أو أي خطأ آخر فإن البرنامج لن يعلّق أو يتم إغلاقه بشكل فجائي.

ظهور خطأ في البرنامج بشكل مفاجئ هو أمر سيئ جداً لأنه يؤدي إلى نفور عدد كبير من المستخدمين و عدم رغبتهم في العودة إلى استخدام هذا البرنامج مجدداً.


أنواع الأخطاء

  • أخطاء لغوية ( Syntax Errors ) و يقصد بها أن تخالف مبادئ اللغة مثل أن تعرّف شيء بطريقة خاطئة أو تنسى وضع فاصلة منقوطة.
  • أخطاء تحدث أثناء تشغيل البرنامج يقال لها إستثناءات ( Exceptions ) مما يؤدي إلى تعليقه و إيقافه بشكل غير طبيعي.
  • أخطاء منطقية ( Logical Errors ) و يقصد منها أن الكود يعمل بدون أي مشاكل و لكن نتيجة تشغيل هذا الكود غير صحيحة.

إذاً, أي خطأ برمجي يحدث معك أثناء تشغيل البرنامج يقال له إستثناء ( Exception ) حتى إن كان إسم الخطأ يحتوي على كلمة Error.
بمعنى آخر, أي Error يظهر لك أثناء تشغيل البرنامج يعتبر Exception.

في هذا الدرس ستتعلم كيف تتجنب حدوث أخطاء في البرامج التي تكتبها, و فعلياً ستتعلم كيف تجهز البرنامج للتعامل مع الأخطاء التي قد تحدث أثناء تشغيله لجعل البرنامج شغال دائماً في نظر المستخدم و لا يظهر له أي أخطاء.



بعض الأسباب التي تسبب حدوث إستثناء

  • في حال كان البرنامج يتصل بالشبكة و فجأةً إنقطع الإتصال.
  • في حال كان البرنامج يحاول قراءة معلومات من ملف نصي, و كان هذا الملف غير موجود.
  • في حال كان البرنامج يحاول إنشاء أو حذف ملف و لكنه لا يملك صلاحية لفعل ذلك.


معلومة تقنية

في حال كان الكود الذي كتبته يحتوي على أخطاء لغوية ( Syntax Errors ) لا بد من أن تصلحها كلها حتى يستطيع المترجم تحويل الكود الذي كتبته لكود يفهمه الحاسوب و من ثم ينفذه لك. أي لا يمكنك حماية البرنامج من موجودة في الكود نفسه بل يمكنك حمايته من المشاكل التي قد تحدث وقت عمل هذه الكود.


في هذا الدرس ستتعلم كيف أن المكتبات الجاهزة التي قد تستخدمها مستقبلاً في مشاريعك يتم جعلها تظهر أخطاء إذا لم تستخدمها كما يجب.
معرفة هذا الأمر مهمة جداً لك أيضاً, لأنك ستتمكن من مشاركة الكود الذي تقوم بإعداده مع مبرمجين آخرين و إظهار أخطاء فيه إن لم يقوموا باستخدامه بشكل صحيح.

أمثلة على أنواع الأخطاء

في المثال التالي لم نضع فاصلة منقوطة في آخر الأمر cout مما سيؤدي لحدوث مشكلة عندما يحاول المترجم تشغيل البرنامج.
إذاً الكود التالي يحتوي على خطأ لغوي ( Syntax Error ).

المثال الأول

main.cpp
#include <iostream>

using namespace std;

int main()
{
    int x = 10;

    cout << x

    return 0;
}
		

سيظهر الخطأ التالي عند التشغيل و الذي يعني أن المترجم يتوقع أن تضع له فاصلة منقوطة في آخر السطر التاسع.

main.cpp|9|error: expected ';' before 'return'|


في المثال التالي قمنا بإنشاء برنامج يطبع للطالب ما إذا كان ناجحاً أو راسباً بناءاً على معدله النهائي.
من المفترض أنه يتم إعتبار الطالب راسب في حال كان معدله بين 0 و 9.9, و يتم إعتباره ناجح في حال كان معدله 10 و 20.
هنا تعمدنا وضع خطأ منطقي ( Logical Error ) حيث أننا عند طباعة نتيجة الطالب لم نتأكد ما إذا كان المعدل بين 0 كحد أدنى و 20 كحد أقصى.

المثال الثاني

main.cpp
#include <iostream>

using namespace std;

int main()
{
    float average = 25;

    if (average < 10)
    {
        cout << "The student failed the exam";
    }
    else if (average >= 10)
    {
        cout << "The student passed the exam";
    }

    return 0;
}

		

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

The student passed the exam

الكلمة throw

تستخدم هذه الكلمة لرمي إستثناء, أي لإظهار أنه يوجد مشكلة ما في البرنامج.


في المثال التالي قمنا بإنشاء دالة إسمها divide() عند استدعاءها نمرر لها رقمين فتقوم بإرجاع ناتج قسمة العدد الأول على العدد الثاني.
هنا في حال كان الرقم الثاني الذي تم تمريره للدالة (المقسوم عليه) يساوي صفر سيتم رمي إستثناء عبارة عن نص عادي مفاده بأنه في الرياضيات لا يمكن القسمة على صفر.

مثال

main.cpp
#include <iostream>

using namespace std;

// عند استدعاءها نمرر لها عددين فتقوم بإرجاع ناتج قسمة العدد الأول على العدد الثاني divide() هنا قمنا بتعريف دالة إسمها
double divide(double a, double b)
{
	// يساوي 0, سيتم رمي إستثناء b في حال كان العدد الثاني الذي سيتم تمريره للباراميتر
    if (b == 0)
    {
        throw "Math Error, you can't divide by 0";
    }

	// إذا لم يتم رمي إستثناء سيتم إرجاع ناتج القسمة
    return a / b;
}

// main() هنا قمنا بتعريف الدالة
int main()
{
	// و تمرير عددين لها و من ثم طباعة ناتج القسمة الذي سترجعه divide() هنا قمنا باستدعاء الدالة
    cout << divide(5, 2) << endl;
	
	// و تمرير 0 مكان الباراميتر الثاني مما سيؤدي لرمي إستثناء و توقف البرنامج بشكل كلي divide() هنا قمنا باستدعاء الدالة
    cout << divide(5, 0) << endl;
	
    return 0;
}
		

سنحصل على النتيجة التالية عند التشغيل.

لاحظ أنه عرض لك ناتج قسمة أول عددين و الذي هو 2.5 بنجاح.

و عندما حاول أن يعرض ناتج ثاني عددين أخبرك أن البرنامج توقف عن العمل بسبب أنه تم رمي إستثناء.

2.5
terminate called after throwing an instance of 'char const*'
		

بالإضافة لذلك فقد ظهرت نافذة منبثقة تخبرك بأن البرنامج توقف بشكل مفاجئ بسبب حدوث مشكلة فيه.


سبب توقف البرنامج عندما قامت الدالة برمي إستثناء هو أننا لم نقم باستدعاء الدالة devide() بشكل محمي رغم أننا نعرف أنها قد تسبب تعليق البرنامج, و نقصد بذلك أننا أي لم نقم باستخدام الجملتين try و catch كما يفترض أن نفعل.

إذا كنت تتساءل عن سبب عدم ظهور الجملة "Math Error, you can't divide by 0" عندما قامت الدالة برمي الإستثناء, فسبب ذلك أننا لم نقم بمعالجة الإستثناء الذي تم رميه كما يفترض و بالطبع ستتعلم كيف تفعل ذلك بعد قليل.

الجملتين try و catch

إلتقاط الإستثناء ( Exception Catching ) عبارة عن طريقة تسمح لك بحماية البرنامج من أي كود تشك بأنه قد يسبب أي خطأ و لتحقيق هذا الأمر نستخدم الجملتين try و catch.

بشكل عام, أي كود مشكوك فيه يجب وضعه بداخل حدود الجملة try.
أي مشكلة تحدث في الجملة catch يتم معالجتها في حدود الجملة try الخاصة بها كالتالي.

try
{
    // Protected Code
    // هنا نكتب الأوامر التي قد تسبب إستثناء
}
catch(ExceptionType e)
{
    // Error Handling Code
    // برمي إستثناء try هنا نكتب أوامر تحدد للبرنامج ماذا يفعل إذا قامت الـ
} 
	

الكود الذي نضعه بداخل الجملة try يسمى Protected Code و هذا يعني أن البرنامج محمي من أي خطأ قد يحدث بسبب هذا الكود.
الكود الذي نضعه بداخل الجملة catch يسمى Error Handling Code و يقصد منها الكود الذي سيعالج الإستثناء الذي قد يتم إلتقاطه.


ملاحظة

عندما تستخدم الجملة try حتى لو لم تضع بداخلها أي كود, فأنت مجبر على وضع الجملة catch بعدها.
كما أنه بإمكانك وضع أكثر من جملة catch في حال كان الكود قد يسبب أكثر من خطأ.


في المثال التالي قمنا بإنشاء دالة إسمها devide() عند استدعاءها نمرر لها رقمين فتقوم بإرجاع ناتج قسمة العدد الأول على العدد الثاني.
هنا في حال كان الرقم الثاني الذي تم تمريره للدالة (المقسوم عليه) يساوي صفر سيتم رمي إستثناء عبارة عن نص عادي مفاده بأنه في الرياضيات لا يمكن القسمة على صفر.

بما أن الدالة devide() قد تسبب حدوث خطأ عندما يتم استدعاءها قمنا بوضعها بداخل try/catch

المثال الأول

main.cpp
#include <iostream>

using namespace std;

// عند استدعاءها نمرر لها عددين فتقوم بإرجاع ناتج قسمة العدد الأول على العدد الثاني divide() هنا قمنا بتعريف دالة إسمها
double divide(double a, double b)
{
	// يساوي 0, سيتم رمي إستثناء b في حال كان العدد الثاني الذي سيتم تمريره للباراميتر
    if (b == 0)
    {
        throw "Math Error, you can't divide by 0";
    }

	// إذا لم يتم رمي إستثناء سيتم إرجاع ناتج القسمة
    return a / b;
}

// main() هنا قمنا بتعريف الدالة
int main()
{
	// و تمرير 0 مكان الباراميتر الثاني مما سيؤدي لرمي إستثناء divide() هنا قمنا باستدعاء الدالة
	try
	{
		cout << divide(5, 0) << endl;
	}
	// e الإستثناء الذي سيتم رميه سيكون عبارة عن نص (سلسلة من الأحرف) و هذه الأحرف سيتم تمريرها كقيمة للمتغير
	catch (char const* e)
	{
		// e هنا قمنا بطباعة نص الإستثناء الذي تم رميه و تخزينه في المتغير
		cout << e << endl;
	}
	
	// هنا سيتم تنفيذ الأمر التالي بشكل عادي جداً لأن الإستثناء الذي حدث في السابق تم معالجته
	cout << "The program is still working properly :)";
	
    return 0;
}
		

سنحصل على النتيجة التالية عند التشغيل.

Math Error, you can't divide by 0
The program is still working properly :)
		

معلومة مهمة

سبب جعل نوع الباراميتر e يكون char const* بالتحديد هو أننا لاحظنا في المثال السابق أن النص الذي يتم رميه كإستثناء, يكون نوعه كذلك.

مفاهيم مهمة حول معالجة الأخطاء


المفهوم الأول

هنا وضعنا مثال حول كيفية تعريف دالة تفعل throw لأكثر من رقم نوعهم int بالإضافة إلى كيفية إستدعاءها.

شاهد المثال »



المفهوم الثاني

هنا وضعنا مثال حول كيفية استخدام الرمز ... لحماية الكود من أي إستثناء قد يحدث حتى لو لم نكن نعرف نوع الإستثناء الذي قد يتم رميه.

شاهد المثال »



المفهوم الثالث

هنا وضعنا مثال حول كيفية وضع أكثر من catch في حال كان الكود قد يسبب إستثناءات من أكثر من نوع.

شاهد المثال »



المفهوم الرابع

هنا وضعنا مثال حول كيفية تعريف نوع إستثناء جديد و استخدامه.

شاهد المثال »

الدورات

أدوات مساعدة

الأقسام

دورات
مقالات أسئلة مشاريع كتب