تخطَّ إلى المحتوى

معالجة الأخطاء في C++‎: try و catch و throw

الدرس 10 من 11 · C++

المستخدم يكتب حروفاً مكان الرقم، والملف المطلوب غير موجود، ورقم العنصر خارج حدود القائمة. البرنامج الجيد لا ينهار أمام هذه المواقف، بل يتوقعها ويتعامل معها. في هذا الدرس ستتعلم نظام الاستثناءات (Exceptions) في C++‎: كيف ترمي الخطأ وكيف تلتقطه، وكيف تضمن اللغة تنظيف الموارد دون كلمة finally.

كل أمثلة الدرس تحتاج المكتبة <stdexcept> إضافة إلى <iostream> و <string>.

عندما تكتشف دالة مشكلة لا تستطيع حلها، ترمي كائناً يصف الخطأ. إذا لم يلتقطه أحد، يتوقف البرنامج فوراً برسالة مثل:

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:

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، ثم يكمل البرنامج طريقه.

الأمر مثل شبكة الأمان تحت لاعب السيرك: إذا سقط التقطته الشبكة واستمر العرض.

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;
}
}
الناتج
العدد: 42
abc ليس رقماً
99999999999 أكبر من أن يُخزَّن في int

C++‎ تجرّب أقسام catch بالترتيب وتنفّذ أول قسم يطابق النوع. وإذا لم تحتج كائن الخطأ فاكتب النوع وحده بلا اسم.

لأن كل الأنواع القياسية ترث من 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 أولاً لالتقط كل شيء ولم يصل التنفيذ لما بعده.

للأخطاء الخاصة ببرنامجك، أنشئ كلاساً يرث من 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 لتنظيف الموارد. 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: كلها تنظف نفسها مهما حدث.

  1. اكتب دالة int parseAge(const string& text) تستخدم stoi، وترمي out_of_range إذا كان العمر خارج المدى من 0 إلى 120.
  2. استدعها داخل try مع ثلاثة نصوص: رقم سليم، وحروف، ورقم كبير، والتقط كل نوع برسالة مناسبة.
  3. أضف قسماً أخيراً catch (const exception& e) يطبع e.what().
  4. تحدٍّ: أنشئ استثناءً خاصاً EmptyStackError، وكلاس Stack بسيطاً فوق vector<int> ترمي دالته pop هذا الاستثناء عندما يكون المكدس فارغاً.
  • الالتقاط بالقيمة: catch (exception e) ينسخ الكائن ويقصّ الجزء الخاص بالنوع الوارث. اكتب دائماً catch (const exception& e).
  • رمي مؤشر: throw new runtime_error("...") عادة منقولة من Java، وتسبب تسرب ذاكرة. ارمِ الكائن نفسه بدون new.
  • catch فارغ: ابتلاع الخطأ بصمت يخفي المشكلة. اطبع رسالة على الأقل.
  • انتظار استثناء من []: تجاوز حدود vector بـ [] سلوك غير معرَّف وليس استثناء. استخدم at() إذا أردت الفحص.

سؤال

ما أفضل طريقة لالتقاط استثناء في C++‎؟

سؤال

كيف تضمن C++‎ تنظيف الموارد عند حدوث استثناء، دون finally؟

  • throw ترمي كائن الخطأ، و try و catch يلتقطانه، و what() تُرجع وصفه.
  • التقط دائماً بمرجع ثابت، ورتّب الأقسام من الأخص إلى الأعم.
  • الأنواع القياسية ترث من exception: مثل invalid_argument و out_of_range و runtime_error.
  • أنشئ استثناءك الخاص بالوراثة من runtime_error.
  • لا توجد finally: المُدمِّرات تنظف الموارد تلقائياً بفكرة RAII.