معالجة الأخطاء في C++: try و catch و throw
المستخدم يكتب حروفاً مكان الرقم، والملف المطلوب غير موجود، ورقم العنصر خارج حدود القائمة. البرنامج الجيد لا ينهار أمام هذه المواقف، بل يتوقعها ويتعامل معها. في هذا الدرس ستتعلم نظام الاستثناءات (Exceptions) في C++: كيف ترمي الخطأ وكيف تلتقطه، وكيف تضمن اللغة تنظيف الموارد دون كلمة finally.
كل أمثلة الدرس تحتاج المكتبة <stdexcept> إضافة إلى <iostream> و <string>.
ما هو الاستثناء؟
Section titled “ما هو الاستثناء؟”عندما تكتشف دالة مشكلة لا تستطيع حلها، ترمي كائناً يصف الخطأ. إذا لم يلتقطه أحد، يتوقف البرنامج فوراً برسالة مثل:
terminate called after throwing an instance of 'std::out_of_range' what(): vector::_M_range_check: __n (which is 5) >= this->size() (which is 3)في الرسالة نوع الخطأ (out_of_range) ووصفه بعد what().
try و catch
Section titled “try و catch”ضع الكود الخطِر داخل try، وخطة الطوارئ داخل catch:
vector<int> scores = {85, 92, 78};try { cout << scores.at(1) << endl; cout << scores.at(5) << endl; cout << "لن يُطبع هذا السطر" << endl;} catch (const out_of_range& e) { cout << "موضع غير موجود" << endl;}cout << "البرنامج مستمر" << endl;92موضع غير موجودالبرنامج مستمرعند حدوث الخطأ يقفز التنفيذ فوراً إلى catch وتُهمل بقية أسطر try، ثم يكمل البرنامج طريقه.
الأمر مثل شبكة الأمان تحت لاعب السيرك: إذا سقط التقطته الشبكة واستمر العرض.
throw: ارمِ استثناءك
Section titled “throw: ارمِ استثناءك”double divide(double a, double b) { if (b == 0) { throw invalid_argument("لا يمكن القسمة على صفر"); } return a / b;}try { cout << divide(10, 4) << endl; cout << divide(5, 0) << endl; cout << divide(9, 3) << endl;} catch (const invalid_argument& e) { cout << "خطأ: " << e.what() << endl;}2.5خطأ: لا يمكن القسمة على صفرthrowتوقف الدالة فوراً وترسل الكائن إلى أقربcatchمناسب في سلسلة الاستدعاءات.e.what(): تُرجع رسالة الخطأ التي كتبتها عند الرمي.const ...&: نلتقط الاستثناء بمرجع ثابت دائماً، لتفادي نسخه وتقطيعه.
أنواع المكتبة القياسية
Section titled “أنواع المكتبة القياسية”كل الاستثناءات القياسية ترث من الكلاس exception، وهذه أشهرها:
| النوع | يُستخدم عندما |
|---|---|
invalid_argument |
معامل قيمته غير مقبولة |
out_of_range |
رقم خارج المدى المسموح |
runtime_error |
خطأ عام يظهر وقت التشغيل |
bad_alloc |
فشل حجز الذاكرة |
والدالة stoi التي عرفتها في درس النصوص ترمي نوعين منها:
vector<string> inputs = {"42", "abc", "99999999999"};for (const string& input : inputs) { try { int number = stoi(input); cout << "العدد: " << number << endl; } catch (const invalid_argument&) { cout << input << " ليس رقماً" << endl; } catch (const out_of_range&) { cout << input << " أكبر من أن يُخزَّن في int" << endl; }}العدد: 42abc ليس رقماً99999999999 أكبر من أن يُخزَّن في intC++ تجرّب أقسام catch بالترتيب وتنفّذ أول قسم يطابق النوع. وإذا لم تحتج كائن الخطأ فاكتب النوع وحده بلا اسم.
التقاط الأعم
Section titled “التقاط الأعم”لأن كل الأنواع القياسية ترث من exception، يلتقطها جميعاً قسم واحد. والصيغة catch (...) تلتقط أي شيء على الإطلاق:
try { throw runtime_error("انقطع الاتصال");} catch (const out_of_range& e) { cout << "خارج المدى" << endl;} catch (const exception& e) { cout << "خطأ عام: " << e.what() << endl;} catch (...) { cout << "خطأ مجهول" << endl;}خطأ عام: انقطع الاتصالرتّب الأقسام من الأخص إلى الأعم. لو وضعت exception أولاً لالتقط كل شيء ولم يصل التنفيذ لما بعده.
استثناء خاص بك
Section titled “استثناء خاص بك”للأخطاء الخاصة ببرنامجك، أنشئ كلاساً يرث من runtime_error:
class InsufficientFunds : public runtime_error {public: double missing;
InsufficientFunds(double m) : runtime_error("الرصيد لا يكفي"), missing(m) {}};
class Account { double balance = 100;
public: void withdraw(double amount) { if (amount > balance) { throw InsufficientFunds(amount - balance); } balance -= amount; cout << "تم السحب، الرصيد الآن " << balance << endl; }};Account account;try { account.withdraw(30); account.withdraw(200);} catch (const InsufficientFunds& e) { cout << e.what() << "، ينقصك " << e.missing << endl;}تم السحب، الرصيد الآن 70الرصيد لا يكفي، ينقصك 130الوراثة من runtime_error تمنحك what() جاهزة، وتجعل catch (const exception&) يلتقط استثناءك أيضاً.
أين finally؟ الجواب: RAII
Section titled “أين finally؟ الجواب: RAII”لغات أخرى فيها finally لتنظيف الموارد. C++ تعتمد فكرة أذكى: عند الخروج من أي نطاق، سواء بشكل طبيعي أو بسبب استثناء، تُستدعى مُدمِّرات كل الكائنات المحلية تلقائياً:
class Door { string name;
public: Door(string n) : name(n) { cout << "فتح " << name << endl; } ~Door() { cout << "إغلاق " << name << endl; }};
void enter() { Door door("الباب"); throw runtime_error("حريق!");}try { enter();} catch (const exception& e) { cout << "التُقط: " << e.what() << endl;}فتح البابإغلاق البابالتُقط: حريق!أُغلق الباب قبل الوصول إلى catch، مع أن الدالة لم تكمل عملها. هذه الفكرة اسمها RAII: المورد يُحجز في المُنشئ ويُحرَّر في المُدمِّر. وبها تعمل vector و string و unique_ptr وملفات fstream: كلها تنظف نفسها مهما حدث.
جرّب بنفسك
Section titled “جرّب بنفسك”- اكتب دالة
int parseAge(const string& text)تستخدمstoi، وترميout_of_rangeإذا كان العمر خارج المدى من 0 إلى 120. - استدعها داخل
tryمع ثلاثة نصوص: رقم سليم، وحروف، ورقم كبير، والتقط كل نوع برسالة مناسبة. - أضف قسماً أخيراً
catch (const exception& e)يطبعe.what(). - تحدٍّ: أنشئ استثناءً خاصاً
EmptyStackError، وكلاسStackبسيطاً فوقvector<int>ترمي دالتهpopهذا الاستثناء عندما يكون المكدس فارغاً.
أخطاء شائعة
Section titled “أخطاء شائعة”- الالتقاط بالقيمة:
catch (exception e)ينسخ الكائن ويقصّ الجزء الخاص بالنوع الوارث. اكتب دائماًcatch (const exception& e). - رمي مؤشر:
throw new runtime_error("...")عادة منقولة من Java، وتسبب تسرب ذاكرة. ارمِ الكائن نفسه بدونnew. - catch فارغ: ابتلاع الخطأ بصمت يخفي المشكلة. اطبع رسالة على الأقل.
- انتظار استثناء من
[]: تجاوز حدودvectorبـ[]سلوك غير معرَّف وليس استثناء. استخدمat()إذا أردت الفحص.
اختبر نفسك
Section titled “اختبر نفسك”سؤال
ما أفضل طريقة لالتقاط استثناء في C++؟
الالتقاط بمرجع ثابت يتفادى النسخ وتقطيع الكائن، ويحافظ على نوعه الحقيقي.
سؤال
كيف تضمن C++ تنظيف الموارد عند حدوث استثناء، دون finally؟
عند الخروج من النطاق لأي سبب تُستدعى المُدمِّرات، فتحرر الكائنات مواردها بنفسها.
الخلاصة
Section titled “الخلاصة”throwترمي كائن الخطأ، وtryوcatchيلتقطانه، وwhat()تُرجع وصفه.- التقط دائماً بمرجع ثابت، ورتّب الأقسام من الأخص إلى الأعم.
- الأنواع القياسية ترث من
exception: مثلinvalid_argumentوout_of_rangeوruntime_error. - أنشئ استثناءك الخاص بالوراثة من
runtime_error. - لا توجد
finally: المُدمِّرات تنظف الموارد تلقائياً بفكرة RAII.