سلسلة ++c للمحترفين الدرس 12: بنى التحكم (Flow Control) في Cpp


محمد الميداوي

case

تُدخِل كلمة case المفتاحية وسم حالة (case label) لتعليمة switch، وتنفّذ تعليمات معيّنة بناء على القيمة التي يساويها معامَلها. كذلك يجب أن يكون هذا المعامَل تعبيرًا ثابتًا ومطابقًا لشرط تعليمة switch. تنتقل تعليمة switch عند تنفيذها إلى وسم الحالة التي يساوي فيها المعامَلُ الشرطَ إن وُجِد. انظر المثال التالي:

char c = getchar();
bool confirmed;
switch (c) {
case 'y':
confirmed = true;
break;
case 'n':
confirmed = false;
break;
default:
std::cout << "invalid response!\n";
abort();
}

switch

وفقًا لتوصيف C++‎، فإن تعليمة switch تحول التحكم إلى تعليمة برمجية او أكثر وفقًا لقيمة الشرط، وتُتبَع كلمة ‎switch‎ المفتاحية بقوسين يحتويان شرطًا ثم كتلة برمجية، والتي قد تحتوي على عناوين ‎case‎، وعنوان ‎default‎ اختياري.

عند تنفيذ تعليمة switch سيُنقَل التحكّم إلى عنوان ‎case‎ ذي القيمة المطابقة لقيمة الشّرط -إن وُجدت-، أو إلى العنوان ‎default‎ إن وُجد أيضًا.

ويجب أن يكون الشرط تعبيرًا أو تصريحًا يحتوي على عدد صحيح، أو نوع عددي (enumeration type)، أو نوع صنف له دالة تحويل إلى عدد صحيح أو نوع عددي.

char c = getchar();
bool confirmed;
switch (c) {
case 'y':
    confirmed = true;
    break;
case 'n':
    confirmed = false;
    break;
default:
    std::cout << "invalid response!\n";
    abort();
}

catch

تُدخِل كلمة ‎catch‎ المفتاحية معالج اعتراضات ينتقل التحكّم إلى كتلته عند إطلاق اعتراض (exception) من النّوع الموافق. وتُعقَب ‎catch‎ بقوسين يحتويان تصريحًا عن اعتراض، وذلك يشبه في هيئته تصريحات المُعامِلات في الدّوال، إذ يجوز حذف اسم المعامِل ويُسمح بالمقطع ‎...‎، والذي يطابق أي نوع.

مُعالجُ الاعتراض لن يُعالجَ الاعتراض إلّا إن كان تصريحه موافقًا لنوع الاعتراض. لمزيد من التفاصيل، انظر إمساك الاعتراضات.

try {
    std::vector < int > v(N);
    // افعل شيئا ما
} catch (const std::bad_alloc & ) {
    std::cout << "failed to allocate memory for vector!" << std::endl;
} catch (const std::runtime_error & e) {
    std::cout << "runtime error: " << e.what() << std::endl;
} catch (...) {
    std::cout << "unexpected exception!" << std::endl;
    throw;
}

throw

  1. عندما تقع ‎throw‎ في تعبير ذي معامَل فإنّها تطلق اعتراضًا يكون نسخة من ذلك المعامَل:
void print_asterisks(int count) {
    if (count < 0) {
        throw std::invalid_argument("count cannot be negative!");
    }
    while (count--) {
        putchar('*');
    }
}
  1. عندما تقع ‎throw‎ في تعبير ليس له معامَل، فستعيد إطلاق الاعتراض الحالي، أما إن لم يكن ثمة اعتراض موجود فتُستدعى ‎std::terminate‎.
try {
    // افعل شيئا ما
} catch (const std::bad_alloc & ) {
    std::cerr << "out of memory" << std::endl;
} catch (...) {
    std::cerr << "unexpected exception" << std::endl;
    // رجاء أن يعرف المستدعي كيفية معالجة هذا الاعتراض.
    throw;
}
  1. عندما تقع ‎throw‎ في مُصرِّح دالة (function declarator)، فستقدّم توصيفًا للاعتراضات الديناميكية، والذي يسرد أنواع الاعتراضا التي يجوز للدّالة نشرها. انظر المثال التالي لدالة قد تنشر الاعتراض std::runtime_error مثلًا، لكن ليس std::logic_error:
void risky() throw(std::runtime_error);
// هذه الدالة لا يمكنها نشر أيّ اعتراض.
void safe() throw();

أُهملت توصيفات الاعتراض الديناميكية بدءًا من C++ 11.

أول استخدامَين للعبارة ‎throw‎ المذكورة أعلاه هما تعبيران (expressions) وليسا تعليمتيْن (statements)، لاحظ أن نوع الاعتراض الذي أُطلِق هو ‎void‎، هذا يجعل تداخلها ممكنًا في التعبيرات على النحو التالي:

unsigned int predecessor(unsigned int x) {
return (x > 0) ? (x - 1) : (throw std::invalid_argument("0 has no predecessor"));
}

default

تمثل default العنوان الذي سيقفز إليه البرنامج في تعليمة switch، إذا كانت قيمة الشرط لا تساوي أيًا من قيم عناوين case.

char c = getchar();
bool confirmed;
switch (c) {
case 'y':
    confirmed = true;
    break;
case 'n':
    confirmed = false;
    break;
default:
    std::cout << "invalid response!\n";
    abort();
}

الإصدار ≥ C++11

تُعرّف default مُنشِئًا افتراضيًا (default constructor) أو مُنشئ نسخ أو نقل، أو مُدمِّرًا (destructor)، أو عامل إسناد النّسخ (copy assignment operator)، أو عامل إسناد النقل (move assignment operator) ليكون هو السلوك الافتراضي. انظر المثال التالي حيث نريد أن نكون قادرين على محو الأصناف المشتقة عبر *Base، لكن نريد السلوك المعتاد لمدمر Base:

class Base {
    virtual~Base() =
        default;
};

try

تُتبَع الكلمة المفتاحية ‎try‎ بكتلة برمجية، أو قائمة تهيئة المُنشئ (constructor initializer list) ثمّ كتلة برمجية. وتُتبَع كتلة try بكتلة catch واحدة أو أكثر، وإذا انتشر اعتراض خارج كتلة try فإنّ كل كتل تعليمات catch الموجودة بعد كتلة try تستطيع معالجة الاعتراض إذا كانت الأنواع متطابقة. انظر المثال التالي حيث لن تمسك catch الاعتراض إن أُطلق بعد std::vector الأولى، لكنها ستمسكه إن أُطلق بعد الثانية التي داخل try:

std::vector < int > v(N);
try {
    std::vector < int > v(N); 
    // v افعل شيئا ما بـ
} catch (const std::bad_alloc & ) {
    // try من كتلة bad_alloc عالج اعتراض.
}

if

if هي تعليمة شَرْطية يجب أن تُتبَع بقوسين يضُمّان شرطًا يكون إمّا تعبيرًا أو تصريحًا، وإذا تحقّق ذلك الشّرط، فستُنفّذ العبارة الموجودة بعده. انظر:

int x;
std::cout << "Please enter a positive number." << std::endl;
std::cin >> x;
if (x <= 0) {
    std::cout << "You didn't enter a positive number!" << std::endl;
    abort();
}

else

قد تَتبع كلمة else المفتاحيةُ أولَ تعليمة فرعية لتعليمة if، وستُنفَّذ التعليمات الفرعية بعد ‎else‎ عند عدم تحقق شرط if، أي عندما لا تُنفّذ كتلة التعليمات الأولى. انظر:

int x;
std::cin >> x;
if (x % 2 == 0) {
    std::cout << "The number is even\n";
} else {
    std::cout << "The number is odd\n";
}

البُنى الشرطية: if و if..else

if و else

تُستخدم لنعرف ما إن كان التعبير المُعطى يعيد true أو false ومن ثم تتصرف وفقها، انظر:

if (condition) statement

يمكن أن يكون الشّرط أيّ تعبير C++‎ صالح يُعيد شيئًا يمكن التحقق من صحته أو خطئه، انظر المثال التالي حيث تُنفَّذ الشيفرة بين القوسين لتحقق الشرط (أي true)، بينما لا تُنفَّذ الشيفرة في السطر الثاني لعدم تحققه (أي false):

if (true) { /* الشيفرة المراد تنفيذها */ }
if (false) { /* الشيفرة المراد تنفيذها */ }

قد يكون الشرط دالة أو متغيرًا أو مقارنة أو غير ذلك، انظر المثال التالي:

// تنفَّذ الشيفرة إن تحققت الدالة
if(istrue()) { } 

// تقييم الدالة بعد تمرير المتغير إليها
if(isTrue(var)) { } 

// a يساوي  b ستُنفّذ الشيفرة المقابلة إن كان
if(a == b) { } 

// قيمة بوليانية فستُقيّم بحسب قيمتها الفعلية a إن كانت
// وإن كانت عددا، فإنّ أي قيمة غير صفرية فستُعد صحيحة 
if(a) { } 

تستطيع التحقق من عدة تعبيرات بإحدى طريقتين: استخدام العمليات الثنائية‎:

if (a && b) { } // معًا a و b تتحقق في حال تحقق
scope here
if (a || b ) { } // تتحقق إن تحقق أي منهما.

استخدام if / ifelse / else: لإجراء تبديل بسيط، يمكنك استخدام إمّا if أو else:

if (a == "test") {
    // "test" تساوي a ستُنفّذ في حال كانت 
} else {
    // تنفذ في حال لم تنفّذ العبارة الأولى
}

وفي حالة الخيارات المتعددة:

if (a == 'a') {
    // 'a' حرفا يساوي a إن كانت
} else if (a == 'b') {
    //  'b' حرفا يساوي a إن كانت
} else if (a == 'c') {
    //  'c' حرفا يساوي a إن كانت
} else {
    // تُنفّذ إن لم يتحقق أي مما سبق
}

يُفضّل استخدام "switch " بدلاً من الشيفرة السابقة ما دُمنا نتحقّق من قيمة نفس المتغير.

goto

تنتقل goto إلى التعليمة المعنونة (labelled statement)، والتي ينبغي أن تكون موجودة في الدالة الحاليّة.

bool f(int arg) {
    bool result = false;
    hWidget widget = get_widget(arg);
    if (!g()) {
        // لا يمكن أن نستمر، لكن لا زال علينا أن ننظف.
        goto end;
    }
    // ...
    result = true;
    end:
        release_widget(widget);
    return result;
}

عبارات القفز: break و continue و goto و exit

التعليمة break

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

break;

على سبيل المثال: غالبًا ما نستخدم ‎break‎ في حالات ‎switch‎، كأن تتحقق حالة في switch فتُنفَّذ شيفرة الشرط. انظر:

switch (conditon) {
case 1:
    block1;
case 2:
    block2;
case 3:
    block3;
default:
    blockdefault;
}

في الشيفرة السابقة، إن تحققت الحالة الأولى case 1 فإن الكتلة block 1 تُنفَّذ، ثم تُنفَّذ كتل الحالتين الثانية والثالثة رغم أن الحالة الأولى فقط هي المتحققة، وهي ما أردنا تنفيذ الشيفرة المقابلة لها فقط. ولتجنب تنفيذ الشيفرات للحالات التي لم تحقق فإننا نستخدم break في نهاية كل كتلة، انظر:

switch (condition) {
case 1:
    block1;
    break;
case 2:
    block2;
    break;
case 3:
    block3;
    break;
default:
    blockdefault;
    break;
}

لن تُعالَج الآن إلا كتلة واحدة فقط، وسينتقل التحكم إلى خارج حلقة switch، كذلك يمكن استخدام break في الحلقات الشرطية وغير الشرطية الأخرى، مثل ‎if‎ و ‎while‎ و ‎for‎ وغيرها؛ انظر:

if (condition1) {
    ....
    if(condition2) {
            .......
            break;
        }
        ...
}

التعليمة continue

تجعل التعليمة continue البرنامجَ يتخطّى بقيّة تعليمات الحلقة في التكرار الحالي ممّا يؤدّي إلى الانتقال إلى التكرار التالي في الحلقة، وتكون صيغتها كالتالي:

continue;

انظر المثال التالي:

for (int i = 0; i < 10; i++) {
    if (i % 2 == 0)
        continue;
    cout << "\n @" << i;
}

الذي ينتج الخرج:

@1
@3
@5
@7
@9

في المثال السابق، كلمّا تحقّق الشرط ‎i%2==‎0‎ فستُنفّذ العبارة ‎continue‎، مما يجعل المُصرّف يتخطّى كل الشيفرة المتبقيّة (طباعة @ و i)، ويعودَ لتنفيذ عبارة الزّيادة/الإنقاص (increment/decrement) في الحلقة.

Fbuep.jpg

التعليمة goto

تسمح goto بإجراء قفزة إلى نقطة أخرى في البرنامج. يجب عليك الحذر عند استخدام هذه الميزة لأنّ تنفيذها يتجاهل كلّ قيود التداخل (nesting).

وتُحدَّد النقطة التي سينتقل إليها البرنامج بواسطة عنوان (label) يُمرَّر بعدها كوسيط لتعليمة goto، ويتألّف اسم ذلك العنوان من مُعرِّف صالح متبوع بنقطتين : على النحو التالي:

goto label;
..
.
label: statement;

ملاحظة: يُوصى بتجنّب استخدام عبارة goto لأنها تصعّب تتبّع سير البرنامج ومن ثم فهمه وتعديله.

Li8ZR.jpg

مثال:

int num = 1;
STEP:
    do {

        if (num % 2 == 0) {
            num = num + 1;
            goto STEP;
        }

        cout << "value of num : " << num << endl;
        num = num + 1;
    } while (num < 10);

الناتج:

pre widget

value of num : 1
value of num : 3
value of num : 5
value of num : 7
value of num : 9

تنقل goto تنفيذَ التحكم، عند تحقق شرط ‎num%2==‎0‎، إلى بداية حلقة do-while

دالة exit

‎exit‎ هي دالة مُعرَّفة في مكتبة cstdlib، والغرض منها هو إنهاء البرنامج الذي يتم تنفيذه برمز خروج محدّد يكون على الصورة التالية:

void exit (int exit code);

رمزا الخروج القياسيَّين EXIT_SUCCESS و EXIT_FAILURE مُعرَّفان في مكتبة ‎cstdlib‎.

return

تعيد تعليمة return التحكمَ من الدالة إلى مُستدعيها، وإذا كان للتعليمة مُعامَل فسيُحوَّل إلى نوع القيمة المعادة الخاص بالدالة، ثم تُعاد القيمة المُحوّلة إلى المستدعي.

int f() {
    return 42;
}
int x = f(); // x = 42
int g() {
    return 3.14;
}
int y = g(); // x = 3

إذا لم يكن للتعليمة ‎return‎ أيّ معامَل فينبغي أن يكون نوع القيمة المُعادة للدالة هو ‎void‎. كذلك يمكن للدوال التي نوع قيمتها المعادة فارغ (‎void‎) أن تعيد تعبيرًا إن كان ذلك التعبير من النوع ‎void‎.

void f(int x) {
    if (x < 0) return;
    std::cout << sqrt(x);
}
int g() {
    return 42;
}
void h() {
    return f(); // ثم العودة f استدعاء
    return g(); // غير صالحة
}

تستدعي main دالة ‎std::exit‎ ضمنيًا مع القيمة التي تعيدها، وتعاد القيمة عندها إلى بيئة التنفيذ، غير أن الإعادة من main يدمر المتغيرات المحلية التلقائية في حين أن استدعاء ‎std::exit‎ مباشرة لا يفعل ذلك.

int main(int argc, char ** argv) {
    if (argc < 2) {
        std::cout << "Missing argument\n";
        return EXIT_FAILURE; // exit(EXIT_FAILURE); يكافئ
    }
}

هذا الدرس جزء من سلسلة دروس عن C++‎.

ترجمة -بتصرّف- للفصل Chapter 15: Flow Control من كتاب C++ Notes for Professionals





تفاعل الأعضاء


لا توجد أيّة تعليقات بعد



يجب أن تكون عضوًا لدينا لتتمكّن من التعليق

انشاء حساب جديد

يستغرق التسجيل بضع ثوان فقط


سجّل حسابًا جديدًا

تسجيل الدخول

تملك حسابا مسجّلا بالفعل؟


سجّل دخولك الآن