معالجة الاستثناءات في OOP
جزء من قسم Object Oriented Programming في رحلة C++ على Coddy — الدرس 81 من 104.
توفر معالجة الاستثناءات في لغة C++ طريقة منظمة لاكتشاف الأخطاء التي تحدث أثناء التشغيل والاستجابة لها. في البرمجة كائنية التوجه، تتكامل الاستثناءات بشكل طبيعي مع التسلسلات الهرمية للفئات، مما يسمح لك بإنشاء أنواع أخطاء ذات مغزى تحمل سياقاً حول ما حدث من خطأ.
تستخدم الآلية الأساسية ثلاث كلمات رئيسية: try و throw و catch. توضع الأكواد التي قد تفشل داخل كتلة try، ويتم الإشارة إلى الأخطاء باستخدام throw، وتقوم كتل catch بمعالجة أنواع استثناءات محددة:
#include <iostream>
#include <stdexcept>
class BankAccount {
double balance;
public:
BankAccount(double b) : balance(b) {}
void withdraw(double amount) {
if (amount > balance) {
throw std::runtime_error("Insufficient funds");
}
balance -= amount;
}
double getBalance() const { return balance; }
};
int main() {
BankAccount account(100.0);
try {
account.withdraw(150.0);
} catch (const std::runtime_error& e) {
std::cout << "Error: " << e.what() << "\n";
}
std::cout << "Balance: " << account.getBalance() << "\n";
}توفر مكتبة C++ القياسية تسلسلاً هرمياً لفئات الاستثناءات التي تنحدر من std::exception. وتشمل الفئات المشتقة الشائعة std::runtime_error لمشاكل وقت التشغيل و std::logic_error للأخطاء البرمجية. توفر جميع الاستثناءات القياسية طريقة what() تعيد رسالة خطأ.
عندما يتم طرح استثناء (exception)، يتم فك المكدس (stack unwinds) - حيث يتم استدعاء المدمرات (destructors) لجميع الكائنات المحلية بينما يبحث البرنامج عن كتلة catch مطابقة. يضمن ذلك إجراء عملية تنظيف صحيحة حتى عند حدوث أخطاء، وهذا هو السبب في أن RAII ومعالجة الاستثناءات يعملان معاً بشكل جيد للغاية.
التحدي
سهللنقم ببناء آلة حاسبة للقسمة الآمنة توضح معالجة الاستثناءات (exception handling) في سياق البرمجة كائنية التوجه. ستقوم بإنشاء فئة Calculator تنفذ عمليات القسمة وتطرح استثناءات ذات مغزى عند حدوث خطأ ما، ثم تعالج تلك الاستثناءات بسلاسة في برنامجك الرئيسي.
ستقوم بتنظيم الكود الخاص بك عبر ثلاثة ملفات:
Calculator.h: حدد فئةCalculatorالتي تؤدي العمليات الحسابية مع معالجة صحيحة للأخطاء.يجب أن تحتوي فئة
Calculatorعلى طريقةdivide(double numerator, double denominator)تعيد نتيجة القسمة. ومع ذلك، إذا كان المقام صفراً، فيجب أن تطرحstd::runtime_errorمع الرسالةDivision by zero. إذا كان البسط أو المقام سالباً، فيجب أن تطرحstd::invalid_argumentمع الرسالةNegative values not allowed.قم أيضاً بتضمين طريقة
safeSqrt(double value)تعيد الجذر التربيعي للقيمة. إذا كانت القيمة سالبة، اطرحstd::runtime_errorمع الرسالةCannot compute square root of negative number. ستحتاج إلى تضمين<cmath>من أجل دالةsqrt.Calculator.cpp: قم بتنفيذ الطرق المعلن عنها في ملف الترويسة (header file). تأكد من تضمين الترويسات اللازمة (<stdexcept>و<cmath>) والتحقق من الشروط قبل إجراء العمليات.main.cpp: اقرأ ثلاثة مدخلات (كل منها في سطر منفصل):- نوع العملية: إما
divideأوsqrt - الرقم الأول (double)
- الرقم الثاني (double) — يُستخدم فقط للقسمة، ويتم تجاهله في حالة sqrt
قم بإنشاء كائن
Calculatorونفذ العملية المطلوبة داخل كتلة try. إذا نجحت العملية، اطبعResult: [value]. إذا تم التقاط استثناء، اطبعError: [message]حيث تأتي الرسالة من طريقةwhat()الخاصة بالاستثناء.بعد كتلة try-catch (بغض النظر عما إذا كان قد حدث استثناء أم لا)، اطبع
Calculation completeلإثبات أن البرنامج يستمر في التنفيذ بعد معالجة الاستثناءات.- نوع العملية: إما
على سبيل المثال، مع المدخلات divide و 10 و 2:
Result: 5
Calculation completeمع المدخلات divide و 10 و 0:
Error: Division by zero
Calculation completeمع المدخلات divide و -5 و 2:
Error: Negative values not allowed
Calculation completeمع المدخلات sqrt و 16 و 0:
Result: 4
Calculation completeمع المدخلات sqrt و -9 و 0:
Error: Cannot compute square root of negative number
Calculation completeلاحظ كيف يتعامل البرنامج بسلاسة مع الأخطاء عن طريق التقاط الاستثناءات وعرض رسائل ذات مغزى، ثم يواصل التنفيذ. يحافظ هذا النمط على نظافة معالجة الأخطاء ويجعل منطقك الرئيسي يركز على المسار الناجح (happy path).
ورقة مرجعية
تستخدم معالجة الاستثناءات (Exception handling) في لغة C++ ثلاث كلمات مفتاحية: try، و throw، و catch. توضع الأكواد التي قد تفشل داخل كتلة try، ويتم الإشارة إلى الأخطاء باستخدام throw، بينما تعالج كتل catch أنواعاً محددة من الاستثناءات:
try {
// Code that might throw an exception
account.withdraw(150.0);
} catch (const std::runtime_error& e) {
std::cout << "Error: " << e.what() << "\n";
}رمي الاستثناءات (Throwing exceptions) من دوال الفئات (class methods):
void withdraw(double amount) {
if (amount > balance) {
throw std::runtime_error("Insufficient funds");
}
balance -= amount;
}توفر مكتبة C++ القياسية فئات للاستثناءات تنحدر من std::exception. وتشمل الأنواع الشائعة ما يلي:
std::runtime_error- للمشكلات التي تحدث أثناء وقت التشغيلstd::logic_error- للأخطاء البرمجية المنطقيةstd::invalid_argument- للوسائط غير الصالحة
توفر جميع الاستثناءات القياسية دالة what() التي تعيد رسالة الخطأ.
عند رمي استثناء، يتم فك المكدس (stack unwinds) - حيث يتم استدعاء المدمرات (destructors) لجميع الكائنات المحلية أثناء بحث البرنامج عن كتلة catch مطابقة، مما يضمن التنظيف (cleanup) السليم للموارد.
قم بتضمين <stdexcept> لاستخدام فئات الاستثناءات القياسية.
جرّب بنفسك
#include <iostream>
#include <string>
#include "Calculator.h"
using namespace std;
int main() {
// قراءة المدخلات
string operation;
double num1, num2;
cin >> operation;
cin >> num1;
cin >> num2;
// إنشاء كائن Calculator
Calculator calc;
// TODO: استخدم كتلة try-catch لتنفيذ العملية
// - إذا كانت العملية "divide"، استدعِ calc.divide(num1, num2)
// - إذا كانت العملية "sqrt"، استدعِ calc.safeSqrt(num1)
// - عند النجاح، اطبع "Result: " متبوعة بالنتيجة
// - التقط الاستثناءات واطبع "Error: " متبوعة برسالة الاستثناء (استخدم what())
// - بعد try-catch، اطبع دائماً "Calculation complete"
return 0;
}
يتضمن هذا الدرس اختبارًا قصيرًا. ابدأ الدرس للإجابة عليه وتتبّع تقدمك.
جميع دروس Object Oriented Programming
1أساسيات الـ OOP
الملفات الخارجيةبناء وتجميع (Compilation) لغة C++ملفات الـ Header وملفات الـ Sourceالـ Namespaces والـ Scopeمقدمة في الـ OOP في C++الـ Classes مقابل الـ Objectsالمؤشر 'this'الـ Methods (الدوال الأعضاء)الـ Attributes (بيانات الأعضاء)أساسيات الـ Ctors والـ Dtorsمراجعة - آلة حاسبة بسيطة4خصائص الـ Class
أعضاء الـ Instance مقابل الـ Staticدوال الـ Getters والـ Settersدوال الأعضاء الثابتة (Const)الكلمة المفتاحية Mutableالدوال والمتغيرات الساكنة (Static)الدوال والفئات الصديقة (Friend)مراجعة - مدير الحساب البنكي7الوراثة
الوراثة الأساسيةمستويات الوصول في الوراثةترتيب استدعاء الـ Ctor والـ Dtorإعادة تعريف الدوال (Method Overriding)الدوال الافتراضية والـ VTableالوراثة المتعددةالوراثة الافتراضيةمراجعة - هيكلية الموظفين10نظرة عامة على STL
نظرة عامة وفلسفة STLحاويات STLالـ Iteratorsخوارزميات STLالـ Functors وتعبيرات Lambdaمراجعة - تكرار الكلمات13أنماط التصميم - الجزء الأول
مقدمة في أنماط التصميمنمط Singletonنمط Factory و Abstract Factoryنمط Builderنمط Observerنمط Strategy2إدارة الذاكرة
ذاكرة Stack مقابل Heapالمؤشرات والمراجعالذاكرة الديناميكية (new/delete)المؤشرات الذكية في C++RAII في C++مراجعة - مدير المصفوفات الديناميكية5التغليف (Encapsulation)
محددات الوصول في C++محددات الوصول بشكل متعمقإخفاء المعلوماتStruct مقابل Classالأصناف المتداخلة والداخليةمراجعة - نظام سجلات الطلاب8تعدد الأشكال (Polymorphism)
تعدد الأشكال: وقت التجميع مقابل وقت التشغيلالتحميل الزائد للدوال (Function Overloading)مراجعة الدوال الافتراضية (Virtual Functions)الدوال الافتراضية البحتة (Pure Virtual Functions)الأصناف المجردة (Abstract Classes)تصميم الواجهات (Interface) في C++التحويل الديناميكي (Dynamic Casting) و RTTIملخص - حاسبة الأشكال11مفاهيم OOP المتقدمة
التركيب مقابل الوراثةالـ Mixins عبر CRTPنمط Pimplمحو النوع (Type Erasure)أصناف Enum والأنواع القويةمعالجة الاستثناءات في OOPتسلسلات الاستثناءات المخصصة14أنماط التصميم - الجزء الثاني
نمط الأمرنمط المحولنمط المزيننمط قالب الطريقةنمط الحالةنمط التركيبRAII كنمط3المنشئات والموادم
المنشئ الافتراضيالمنشئ ذو المعاملاتمنشئ النسخمنشئ النقلقوائم تهيئة المنشئالمنشئات المفوضةتعمق في الموادمقاعدة الثلاثة / الخمسة / الصفرمراجعة - فئة String6تحميل العوامل (Operator Overloading)
مقدمة في تحميل العواملتحميل العوامل الحسابيةتحميل عوامل المقارنةعوامل Streamتحميل عامل التعيينتحميل العوامل [] و ()عوامل تحويل النوعمراجعة - Matrix Class9القوالب
قوالب الدوالقوالب الأصنافتخصيص القوالبالقوالب المتغيرةأساسيات SFINAE و Type Traitsمراجعة - الحاويات العامة