معالجة الأخطاء في السي بلاس بلاس | C++ Exceptions

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

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

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


أنواع الأخطاء في C++

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

  • أخطاء تحدث أثناء تشغيل البرنامج يقال لها إستثناءات ( Exceptions ) مما يؤدي إلى تعليقه و إيقافه بشكل غير طبيعي.

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

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

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



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

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

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

  • في حال كان البرنامج يحاول إنشاء أو حذف ملف و لكنه لا يملك صلاحية لفعل ذلك.



معلومة تقنية 

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


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

أمثلة على أنواع الأخطاء في C++

في المثال التالي لم نضع فاصلة منقوطة في آخر الأمر 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 في C++

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


في المثال التالي قمنا بإنشاء دالة إسمها 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 في C++

إلتقاط الإستثناء ( 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* بالتحديد هو أننا لاحظنا في المثال السابق أن النص الذي يتم رميه كإستثناء, يكون نوعه كذلك.

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


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


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

في المثال التالي قمنا بتعريف دالة إسمها compareAges() عند استدعاءها نمرر لها عددين, العدد الأول عبارة عن عمر الإبن و الثاني عبارة عن عمر والدته.
الدالة ستقوم بمقارنة عمر الإبن مع عمر والدته و ترجع الفارق بينهما بشرط أن تكون الأعداد التي نمررها لها تعتبر أعداد مقبولة منطقياً, غير ذلك سترمي إستثناء.

مثال

main.cpp
      #include <iostream>

      using namespace std;

      // عند استدعاءها نمرر لها عددين فتقوم بإرجاع عدد يمثل الفارق بينهما compareAges() هنا قمنا بتعريف دالة إسمها
      int compareAges(int sonAge, int momAge)
      {
      // في حال كان عمر الإبن أكبر أو يساوي عمر الأم, سيتم رمي إستثناء رقمه 1
      if (sonAge >= momAge)
      throw 1;

      // في حال كان عمر الإبن أصغر أو يساوي صفر سيتم رمي إستثناء رقمه 2
      else if (sonAge <= 0)
      throw 2;

      // في حال كان عمر الأم أصغر أو يساوي صفر سيتم رمي إستثناء رقمه 3
      else if (momAge <= 0)
      throw 3;

      // في حال لم يكن الفرق بين عمر الأم و الإبن 12 سنة على الأقل سيتم رمي إستثناء رقمه 4
      else if (momAge - sonAge < 12)
      throw 4;

      // إذا لم يتم رمي إستثناء سيتم إرجاع فرق العمر
      return momAge - sonAge;
      }

      // main() هنا قمنا بتعريف الدالة
      int main()
      {
      // و تمرير عددين لها يمثلان عمر أمر و عمر إبنها لمعرفة ما إن كانت الأعمار المدخلة تعتبر مقبولة أم لا compareAges() هنا قمنا باستدعاء الدالة
      try
      {
      compareAges(26, 24);
      }
      // e الإستثناء الذي سيتم رميه سيكون عبارة عن نص (سلسلة من الأحرف) و هذه الأحرف سيتم تمريرها كقيمة للمتغير
      catch (int e)
      {
      switch(e)
      {
      case 1:
      cout << "Error: Son's age can't be less than his mom! \n";
      break;
      case 2:
      cout << "Error: Son's age can't be less than or equal zero \n";
      break;
      case 3:
      cout << "Error: Mom's age can't be less than or equal zero \n";
      break;
      case 4:
      cout << "Error: Mom's age should be 12 years bigger than son age \n";
      break;
      }
      }

      // هنا سيتم تنفيذ الأمر التالي بشكل عادي جداً لأن الإستثناء الذي حدث في السابق تم معالجته
      cout << "The program is still working properly :)";

      return 0;
      }
    

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

      Error: Son's age can't be less than his mom!
      The program is still working properly :)
    


  كيفية استخدام الرمز ... لحماية الكود من أي إستثناء

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

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

    try
    {
    // هنا نكتب الأوامر التي قد تسبب إستثناء
    }
    catch(...)
    {
    // سيقوم المترجم بالإنتقال لهنا try أي إستثناء يحدث في الجملة
    } 
  


في المثال التالي قمنا بتعريف دالة إسمها checkAge() عند استدعاءها نمرر لها عدد يمثل العمر, فتقوم بالتشييك عليه و إظهار أخطاء في حال كان العمر أصغر أو يساوي 0 أو كان أكبر من 130 أو أو كان أقل من 18.

مثال

main.cpp
      #include <iostream>

      using namespace std;

      // عند استدعاءها نمرر لها عدد يمثل العمر checkAge() هنا قمنا بتعريف دالة إسمها
      void checkAge(int age)
      {
      // في حال كان العمر الذي تم تمريره لها أصغر أو يساوي 0 سترمي إستثناء
      if (age <= 0)
      throw "Error: Entered age can't be less or equal zero!";

      // في حال كان العمر الذي تم تمريره لها أكبر من 130 سترمي إستثناء
      if (age > 130)
      throw "Error: Entered age is impossible!";

      // في حال كان العمر الذي تم تمريره لها أصغر من 18 سترمي إستثناء
      if (age < 18)
      throw "Error: You are not allowed!";

      // إذا لم يتم رمي أي إستثناء سيتم تنفيذ أمر الطباعة التالي و الذي يعني أن العمر مقبول
      cout << "Age confirmed!";
      }


      // main() هنا قمنا بتعريف الدالة
      int main()
      {
      // و تمرير القيمة 15 لها checkAge() هنا قمنا باستدعاء الدالة 
      try
      {
      checkAge(15);
      }
      // بالتقاطه و طباعة الجملة الموضوعة فيه catch() ستقوم الدالة try أي إستثناء يتم رميه في الجملة
      catch (...)
      {
      cout << "Oops.. Something is not right!";
      }

      return 0;
      }
    

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

      Oops.. Something is not right!
    


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

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

في المثال التالي قمنا بتعريف دالة إسمها checkWord() عند استدعاءها نمرر لها نص لتتحقق منه و التأكد ما إن كان يحتوي على كلمة واحدة أم لا.
الدالة سترمي إستثناء قيمته العدد 0 في حال تم تمرير نص فارغ لها, و سترمي إستثناء قيمته الكلمة 'Space' في حال تم تمرير أكثر من كلمة لها.

مثال

main.cpp
      #include <iostream>

      using namespace std;

      // عند استدعاءها نمرر لها نص checkWord() هنا قمنا بتعريف دالة إسمها
      checkWord(string s)
      {
      // في حال كان النص الذي تم تمريره لا يحتوي على أي حرف سيتم رمي إستثناء عبارة عن عدد قيمته 0
      if (s.empty())
      throw 0;

      // "Space" في حال كان النص الذي تم تمريره يحتوي على مسافة فارغة سيتم رمي إستثناء عبارة عن نص قيمته
      if (s.find(" ") != string::npos)
      throw "Space";
      }

      // main() هنا قمنا بتعريف الدالة
      int main()
      {
      // هنا قمنا بتعريف النص الذي سنقوم بالتشييك على قيمته
      string s = "Hello Word!";

      // s للتشييك على قيمة المتغير checkWord() هنا قمنا باستدعاء الدالة
      try
      {
      checkWord(s);
      }
      // في حال تم رمي إستثناء قيمته العدد 0, سيتم معالجته هنا
      catch(int e)
      {
      cout << "Error: string length is empty! \n";
      }
      // سيتم معالجته هنا ,Space في حال تم رمي إستثناء قيمته الكلمة
      catch(char const* e)
      {
      cout << "Error: string contain a whitespace! \n";
      }
      // في حال تم رمي إستثناء من أي نوع آخر, سيتم معالجته هنا
      catch(...)
      {
      cout << "Error: something is not right! \n";
      }

      cout << "The program is still working properly :)";

      return 0;
      }
    

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

      Error: string contain a whitespace!
      The program is still working properly :)
    


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

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

الإستثناءات الإفتراضية في C++

قبل شرح كيفية تعريف إستثناء جديد, يجب أن تعرف كيف تم بناء الإستثناءات الجاهزة في اللغة.
بشكل عام std::exception هو الكلاس الأساسي لأي إستثناء يتم تعريفه لذلك يجب على أي كلاس يمثل إستثناء أن يرث منه.
بعد أن يرث منه, يجب أن يفعل Override لدالة إسمها what() ليحدد فيها قيمة الإستثناء الذي سيتم رميه.

الصورة التالية تظهر لك الإستثناءات الجاهزة في C++ و كيف أنها ترث من الكلاس std::exception.


مثال حول كيفية تعريف إستثناء جديد في C++

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

مثال

main.cpp
      #include <iostream>
      #include <exception>

      using namespace std;

      // لأننا نريده أن يمثل إستثناء exception يرث من الكلاس MyException هنا قمنا بتعريف كلاس إسمه
      class MyException : public exception
      {
      public:
      // لجعلها ترمي الإستثناء الذي نريده عندما نقوم باستدعائها what() هنا قمنا بتعريف الدالة
      const char* what() const throw ()
      {
      return "My Exception is thrown! \n";
      }
      };

      // main() هنا قمنا بتعريف الدالة
      int main()
      {
      // الموجودة فيه what() حتى نتمكن من استدعاء الدالة myExcep إسمه MyException هنا قمنا بإنشاء كائن من الكلاس
      MyException myExcep;

      // الموجودة فيه بشكل تلقائي what() و الذي بدوره سيقوم باستدعاء الدالة myExcep هنا قمنا برمي إستثناء نوعه
      try
      {
      throw myExcep;
      }
      // الموجودة فيه what() سيتم إستدعاء الدالة MyException هنا قلنا أنه في حال كان الإستثناء الذي تم رميه نوعه
      catch(MyException& e)
      {
      cout << e.what();
      }
      // و هنا نقصد من أي نوع كان exception هنا قلنا أنه في حال كان الإستثناء الذي تم رميه نوعه
      catch(exception& e)
      {
      // هنا يمكنك كتابة ماذا نريد أن نفعل في حال حدوث أي إستثناء آخر
      }

      cout << "The program is still working properly :)";
      }
    

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

      My Exception is thrown!
      The program is still working properly :)