دستورات try و catch
میتوان خطاها را با استفاده از دستور try…catch اداره کرد. بدین صورت که کدی را که احتمال میدهید ایجاد خطا کند در داخل بلوک try قرار میدهید. بلوک catch هم شامل کدهایی است که وقتی اجرا میشوند که برنامه با خطا مواجه شود. تعریف سادهی این دو بلوک به این صورت است که بلوک try سعی میکند که دستورات را اجرا کند و اگر در بین دستورات خطایی وجود داشته باشد برنامه دستورات مربوط به بخش catch را اجرا میکند. در کل ساختار دستور try catch به صورت زیر است :
try { //some code }
کدهای داخل بلوک catch زمانی اجرا میشوند که یک خطایی در بدنه try به وجود بیاید. ساختار این بلوک به صورت زیر میباشد:
catch(Exception-type) { //some code }
از عبارت throw برای به وجود آوردن یک استثنا استفاده میشود تا به وسیله آن خطاهای برنامه را مدیریت کنیم. ساختار استفاده از عبارت throw به صورت زیر میباشد:
throw exception;
برنامه زیر نحوه استفاده از دستور try…catch را نمایش میدهد :
1: #include <iostream> 2: using namespace std; 3: 4: int main() 5: { 6: int x = 5; 7: int y = 0; 8: 9: try 10: { 11: if (y == 0) 12: throw y; 13: else 14: x / y; 15: } 16: catch (int y) 17: { 18: cout << "An attempt to divide by 0 was detected."; 19: } 20: }
An attempt to divide by 0 was detected.
در کد بالا ما قصد داریم خطاهای احتمالی را که در عملیات تقسیم ممکن است به وجود آید را اداره کنیم. در ریاضیات عمل تقسیم عدد بر صفر ممکن نیست. در خطوط 6و 7 دو متغیر تعریف کردهایم که مقدار یکی از آنها 5 و دیگری 0 است. چون ممکن است که عمل تقسیم بر صفر اتفاق افتد پس در قسمت try در خط 11 با استفاده از دستور if چک میکنیم که اگر مقدار متغیر y برابر 0 باشد، استثناء ایجاد (خط 12) در غیر اینصورت، عملیات تقسیم انجام شود. از آنجاییکه مقدار متغیر y عدد 0 است، یک استثناء رخ میدهد. در نتیجه برنامه ار قسمت try به قسمت catch رفته و در این بلوک، یک پیغام دلخواه و قابل فهم برای نمایش به کاربر مینویسیم (خط 18). اگر فکر میکنید که در بلوک try ممکن است با چندین خطا مواجه شوید میتوانید از چندین بلوک catch استفاده نمایید :
#include <iostream> using namespace std; int main() { int x, y; cout << "Enter x and y values: "; cin >> x >> y; try { if (y == 0) throw y; else if (y < 0) throw "y cannot be negative"; else cout << "Result of x/y = " << (x / y); } catch (int y) { cout << "y cannot be zero"; } catch (const char* message) { cout << message; } }
Enter x and y values: 10 0 y cannot be zero Enter x and y values: 10 -5 y cannot be negative
در برنامه بالا ما میخواهیم عدد صحیح x را بر y تقسیم کنیم. همانطور که می دانید تقسیم بر صفر باعث بروز خطا میشود. در این مثال ما x را 10 و y را ۰ در نظر گرفتیم. زمانی که در اولین شرط به دلیل اینکه b مقدار صفر را دارد یک خطا از نوع int را throw میکند. از آنجایی که اولین بلوک catch از نوع int میباشد این خطا را همین بلوک مدیریت میکند.
در یک مثال دیگر در برنامه بالا ما مقدار x را 10 و مقدار y را 5- در نظر گرفتیم. در اینجا بدنه شرط دوم که منفی بودن y را بررسی میکند اجرا میشود. در داخل بدنه این شرط، ما یک رشته را throw کردیم. از آن جایی که اولین بلوک catch از نوع int میباشد و نمیتواند یک رشته را بپذیرد بنابراین کنترل را به بلوک بعدی منتقل میکند و چون بلوک catch دوم یک آرایه از کاراکترها را میپذیرد بنابراین میتواند این خطا را مدیریت کند.
حال فرض کنید شما میخواهید تمام خطاهای احتمالی که ممکن است در داخل بلوک try اتفاق می افتند را فهمیده و اداره کنید این کار چگونه امکانپذیر است؟ به راحتی میتوان از یک بلوک catch عمومی برای مدیریت هر نوع خطایی استفاده کند. ساختار بلوک catch عمومی به صورت زیر میباشد:
catch(...) { //some code }
بهتر است زمانی که از بلوکهای catch چند تایی استفاده میکنیم، یک بلوک catch عمومی نیز قرار دهیم تا اگر خطایی به غیر از خطاهایی که ما تشخیص دادیم به وجود آمد برنامه دچار مشکل نشود. برنامه زیر نحوه استفاده از یک بلوک catch عمومی را نشان میدهد:
#include <iostream> using namespace std; int main() { int x, y; cout << "Enter x and y values: "; cin >> x >> y; try { if (y == 0) throw y; else if (y < 0) throw "y cannot be negative"; else cout << "Result of x/y = " << (x / y); } catch (int y) { cout << "y cannot be zero"; } catch (...) { cout << "Unkown exception in program"; } }
Enter a and b values: 10 -5 Unkown exception in program
با استفاده از این روش دیگر لازم نیست نگران اتفاق خطاهای احتمالی باشید چون بلوک catch برای هرگونه خطایی که در داخل بلوک try تشخیص داده شود پیغام مناسبی نشان میدهد.
راهاندازی مجدد استثناء
در C++ اگر یک تابع یا یک بلوک try-catch داخلی (تو در تو) نخواهد خطایی را مدیریت کند، مدیریت آن را به بلوک try-catch بالایی پاس میدهد. ساختار Re-throw کردن یا راهاندازی مجدد استثناء به صورت زیر میباشد:
throw;
برنامه زیر نحوه re-throw کردن یک استثنا و مدیریت آن در بلوک try بالاتر را نشان میدهد:
#include <iostream> using namespace std; int main() { int x, y; cout << "Enter x and y values: "; cin >> x >> y; try { try { if (y == 0) throw y; else if (y < 0) throw "y cannot be negative"; else cout << "Result of x/y = " << (x / y); } catch (int y) { cout << "y cannot be zero"; } catch (...) { throw; } } catch (const char* message) { cout << message; } }
Enter x and y values : 10 -2 y cannot be negative
در برنامه بالا مشاهده میکنید که خطای به وجود آمده در بلوک try داخلی توسط بلوک catch عمومی داخلی گرفته شد و این بلوک آن را با استفاده از re-throw به بلوک try-catch بالایی پاس داد.
throw کردن استثناها در تعریف توابع
در هنگام اعلان یک تابع میتوانیم استثناهایی که ممکن است اتفاق بیافتد را throw کنیم. ساختار کلی آن به صورت زیر میباشد:
return-type function-name(params-list) throw(type1, type2, ...) { // some code ... }
برنامه زیر نحوه throw کردن یک استثنا در هنگام تعریف یک تابع را نشان میدهد:
#include <iostream> using namespace std; void sum() throw(int) { int x, y; cout << "Enter x and y values: "; cin >> x >> y; if (x == 0 || y == 0) throw 1; else cout << "Sum is: " << (x + y); } int main() { try { sum(); } catch (int) { cout << "x or y cannot be zero"; } }
Enter x and b values: 5 0 x or y cannot be zero
در برنامه بالا تابع sum میتواند یک استثنا از نوع int را throw کند. بنابراین زمانی که در جایی این تابع فراخوانی شود، باید یک بلوک catch از نوع int برای مدیریت این استثنا وجود داشته باشد.
throw کردن استثناهایی از نوع کلاس
در بخشهای قبل ما از انواع داده اولیه نظیر int ،float ،char و … برای throw کردن یک استثناء استفاده کردیم. اما میتوانیم یک کلاس ایجاد کنیم و یک استثنا از نوع آن کلاس را throw کنیم. استفاده از کلاسهای خالی به خصوص در مدیریت استثناها کاربرد زیادی دارند. برنامه زیر نشان میدهد که چگونه میتوانیم یک استثنا از نوع کلاس را throw کنیم:
#include <iostream> using namespace std; class ZeroError {}; void sum() { int x, y; cout << "Enter x and y values: "; cin >> x >> y; if (x == 0 || y == 0) throw ZeroError(); else cout << "Sum is: " << (x + y); } int main() { try { sum(); } catch (ZeroError e) { cout << "x or y cannot be zero"; } }
Enter x and y values: 0 8 x or y cannot be zero
در برنامه بالا ZeroError یک کلاس خالی است که برای مدریت استثنا ساخته شده است.
مدیریت خطا و وراثت
همانطور که در بخشهای قبلی نیز بررسی کردیم، زمانی که چند بلوک catch داریم، ترتیب اجرا به این صورت است که ابتدا اولین بلوک catch بررسی میکند که آیا نوع خطایی که throw شده را میتواند بپذیرد یا خیر؟ اگر بتواند، آن را مدیریت میکند و در غیر ابنصورت کنترل به بلوک بعدی منتقل میشود. در مباحث وراثتی زمانی که ما دو بلوک catch داریم که یکی از نوع کلاس base یا پدر و دیگری از نوع کلاس Derived یا فرزند باشد، بهتر است بلوک catch ای که از نوع کلاس پدر است را بعد از بلوک catch ای که از نوع کلاس فرزند است قرار دهیم. به مثال زیر توجه کنید:
#include <iostream> using namespace std; class Base { }; class Derived : public Base { }; int main() { try { throw Derived(); } catch (Base b) { cout << "Base object caught"; } catch (Derived d) { cout << "Derived object caught"; } }
Base object caught
در برنامه بالا ما یک کلاس به نام base داریم که همان کلاس والد یا پدر است و یک کلاس به نام Derived داریم که همان کلاس فرزند است. در متد main ما یک خطا از نوع کلاس Derived را throw کردیم. همانطور که گفته شد ابتدا بلوک catch اولی بررسی میکند که آیا میتواند خطا را مدیریت کند یا خیر. از آنجایی که با توجه به مباحث وراثتی میتوانیم یک نمونه از کلاس فرزند را در یک نمونه از کلاس پدر قرار دهیم. بنابراین بلوک catch اول، خطا را میگیرد. در صورتی که ما یک بلوک catch از نوع کلاس Derived یا فرزند نیز داریم و هدف این بود که بلوک catch دوم خطا را مدیریت کند. برای جلوگیری از این نوع مشکلات بهتر است بلوک catch کلاس والد را بعد از بلوک catch کلاس فرزند قرار دهیم. به برنامه زیر توجه کنید:
#include <iostream> using namespace std; class Base { }; class Derived : public Base { }; int main() { try { throw Derived(); } catch (Derived d) { cout << "Derived object caught"; } catch (Base b) { cout << "Base object caught"; } }
Derived object caught
استثناها در متدهای سازنده (constructor) و مخرب (destructor)
ممکن است که خطایی در یک متد سازنده یا مخرب اتفاق بیافتد. اگر یک استثنا در متد سازنده به وجود بیاید، میتواند باعث نشت حافظه شود. فرض کنید ما دو متغیر در متد سازنده تعریف کردیم، اگر به هر دلیلی در تخصیص حافظه به یکی از آنها خطایی به وجود بیاید باعث بروز مشکل در حافظه میشود و ممکن است باعث متوقف شدن ناگهانی برنامه شود. همچنین این مشکل در متد مخرب نیز وجود دارد. بنابراین بهتر است یک مدیریت خطای مناسب را داخل متدهای سازنده و مخرب انجام دهیم تا از بروز مشکلات بعدی جلوگیری کنیم. برنامه زیر نحوه مدیریت یک استثنا در متد سازنده و مخرب را نشان میدهد:
#include <iostream> using namespace std; class Divide { private: int *x; int *y; public: Divide() { x = new int(); y = new int(); cout << "Enter two numbers: "; cin >> *x >> *y; try { if (*y == 0) { throw *x; } } catch (int) { delete x; delete y; cout << "Second number cannot be zero!" << endl; throw; } } ~Divide() { try { delete x; delete y; } catch (...) { cout << "Error while deallocating memory" << endl; } } float division() { return (float)*x / *y; } }; int main() { try { Divide d; float res = d.division(); cout << "Result of division is: " << res; } catch (...) { cout << "Unkown exception!" << endl; } }
Enter two numbers: 5 0 Second number cannot be zero! Unkown exception!