سلسلة ++c للمحترفين الدرس 9: الحلقات التكرارية (Loops) في Cpp


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

تنفذ الحلقات التكرارية مجموعة من التعليمات إلى حين استيفاء شرط معين، وهناك ثلاثة أنواع من تلك الحلقات التكرارية في لغة C++: for و while و do…while.

حلقة for النطاقية (Range-Based For)

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

يمكن استخدام حلقات for للتكرار على عناصر نطاق تكراري (iterator-based range) دون الحاجة إلى استخدام الفهارس العددية أو الوصول بشكل مباشر إلى المكررات:

vector < float > v = {
    0.4 f,
    12.5 f,
    16.234 f
};
for (auto val: v) {
    std::cout << val << " ";
}
std::cout << std::endl;

ستكرّر الشيفرة السابقة تعليمة std::cout << val << " ";‎ على كل عناصر v، وستحصل val على قيمة العنصر الحالي. الشيفرة التالية:

for (for-range-declaration : for-range-initializer ) statement

ستكون مكافئة لما يلي:

{
    auto&& __range = for-range-initializer;
    auto __begin = begin-expr, __end = end-expr;
for (; __begin != __end; ++__begin) {
for-range-declaration = *__begin;
statement
    }
}

الإصدار ≥ C++‎ 17

من الممكن أن تكون end من نوع مختلف عن begin في C++17، انظر:

{
    auto&& __range = for-range-initializer;
    auto __begin = begin-expr;
    auto __end = end-expr;  
    for (; __begin != __end; ++__begin) {
        for-range-declaration = *__begin; 
        statement
    }
}

قُدِّم هذا التغيير في C++‎ لأجل التمهيد لدعم معيار النطاقات Ranges TS في الإصدار C++‎ 20، وعليه فإن الحلقة في هذه الحالة ستكون مكافئة لما يلي:

{
    auto &&__range = v;
    auto __begin = v.begin(), __end = v.end();
    for (; __begin != __end; ++__begin)
    {
        auto val = *__begin;
        std::cout << val << " ";
    }
}

لاحظ أن التعليمة auto val تصرح عن نوع القيمة التي ستكون نسخة من القيمة المخزنة داخل النطاق – سنهيئه استنساخًا أثناء تنفيذ البرنامج-، وإن كانت عملية نسخ القيم المخزنة في النطاق مكلفة فربما تود استخدام const auto &val.

كذلك لست مضطرًا لاستخدام auto ، بل تستطيع استخدام أي اسم نوع typename طالما أنه قابل للتحويل من نوع قيمة النطاق. واعلم أن حلقة for النطاقية غير مناسبة في حال احتجت للوصول إلى المُكرّر، أو ستحتاج مجهودًا كبيرًا.

إن أردت الإشارة للمكرِّر، فيمكنك ذلك بما يلي:

vector < float >v = {0.4f, 12.5f, 16.234f};
for(float &val: v)
{
   std::cout << val << " ";
}

كذلك يمكنك التكرار على مرجع const إذا كان لديك حاوية const:

const vector<float> v = {0.4f, 12.5f, 16.234f};
for(const float &val: v)
{
 std::cout << val << " ";
}

يمكنك استخدام مراجع إعادة التوجيه (forwarding references) في حال أعاد مُكرّر التسلسل كائنًا وكيلًا (proxy object) وكنت بحاجة إلى إجراء عمليات يمكن أن تغيّر قيمة الكائن. لكن بالمقابل، من المحتمل أن تربك من يقرأ شيفرتك إن استخدمت هذه الطريقة.

vector<bool> v(10);
for (auto &&val : v)
{
    val = true;
}

يمكن أن يكون نوع "النطاق" المقدم إلى حلقة for النطاقية أحد الخيارات التالية:

  • المصفوفات:
float arr[] = {0.4f, 12.5f, 16.234f};
for (auto val : arr)
{
    std::cout << val << " ";
}

لاحظ أنّ تخصيص المصفوفات الديناميكية لا يُحسب:

float *arr = new float[3]{0.4f, 12.5f, 16.234f};
for (auto val : arr) // خطأ تصريفي
{
    std::cout << val << " ";
}
  • أي نوع يحتوي على دوال begin()‎ و end()‎ تابعةٍ، يعيدُ مكرّرات إلى عناصر من ذلك النوع، ويمكنك استخدام حاويات المكتبات القياسية إضافة إلى استخدام الأنواع التي يحددها المستخدم (User-Defined)، انظر:
struct Rng {
    float arr[3];
    // المؤشرات ما هي إلا مكرّرات
    const float * begin() const {
        return &arr[0];
    }
    const float * end() const {
        return &arr[3];
    }
    float * begin() {
        return &arr[0];
    }
    float * end() {
        return &arr[3];
    }
};
int main() {
    Rng rng = {
        {
            0.4f, 12.5f, 16.234f
        }
    };
    for (auto val: rng) {
        std::cout << val << " ";
    }
}
  • أيّ نوع لديه دوال begin(type)‎ و end(type)‎ غير تابعة (non-member)، يمكن إيجاده من خلال البحث بالوسائط (Arguments) استنادًا للنوع type، هذا مفيد في إنشاء نوع نطاقٍ (range type) دون الحاجة إلى تعديل نوع الصنف نفسه، انظر:
namespace Mine {
    struct Rng {
        float arr[3];
    };
    // المؤشرات ما هي إلا مكررات
    const float * begin(const Rng & rng) {
        return &rng.arr[0];
    }
    const float * end(const Rng & rng) {
        return &rng.arr[3];
    }
    float * begin(Rng & rng) {
        return &rng.arr[0];
    }
    float * end(Rng & rng) {
        return &rng.arr[3];
    }
}
int main() {
    Mine::Rng rng = {
        {
            0.4f, 12.5f, 16.234f
        }
    };
    for (auto val: rng) {
        std::cout << val << " ";
    }
}

حلقة for التكرارية

تنفّذ الحلقة for التعليمات الموجودة في متن الحلقة loop body ما دامَ شَرط الحلقة condition صحيحًا، وتُنفَّذ initialization statement مرة واحدة قبل تنفيذ الحلقة التكرارية، ثم تُنفّذ التعليمة iteration execution بعد كل تكرار.

تٌعرَّف حلقة for كما يلي:

for (/*initialization statement*/; /*condition*/; /*iteration execution*/)
{
    // متن الحلقة
}

شرح الصيغة السابقة:

  • تُنفَّذ تعليمة initialization statement مرة واحدة فقط في بداية حلقة for، وتستطيع هنا أن تصرِّح عن عدة متغيرات من نفس النوع، مثل int i = 0, a = 2, b = 3. لا تكون تلك المتغيرات صالحة إلا في نطاق الحلقة، وأما المتغيرات التي لها نفس الاسم وعُرِّفّت قبل تنفيذ الحلقة فإنها تُخفى أثناء تنفيذ الحلقة.
  • تُقيَّم تعليمة condition قبل كل مرة يُنفَّذ فيها متن الحلقة (loop body) كي تُوقف الحلقة إن أعادت القيمة false.
  • تُنفَّذ تعليمة iteration execution بعد تنفيذ متن الحلقة وقبل تقييم الشرط التالي إلا إن أُوقِفت حلقة for في المتن بواسطة break أو goto أو return، أو في حالة رفع اعتراض (throwing an exception). تستطيع وضع عدة تعليمات في الجزء iteration execution مثل a++, b+=10, c=b+a.

تكافئ الشيفرةُ التالية حلقةَ for في حال كتابتها كحلقة while، حيث تنتقل الحلقة إلى جزء تنفيذ التكرار /*iteration execution*/ عند استخدام continue:

/*initialization*/
while ( /*condition*/ ) 
{
    // متن الحلقة 
    /*iteration execution*/
}

غالبًا ما تُستخدم الحلقة for لتنفيذ تعليمات برمجية معيّنة عددًا محدّدًا من المرات. على سبيل المثال:

for (int i = 0; i < 10; i++) {
    std::cout << i << std::endl;
}

أو:

for (int a = 0, b = 10, c = 20;
    (a + b + c < 100); c--, b++, a += c) {
    std::cout << a << " " << b << " " << c << std::endl;
}

هذا مثال يوضّح إخفاء المتغيرات المُصرّح عنها قبل الحلقة، حيث نصرح في السطر الثاني عن المتغير i الذي ستتغير قيمته بين 0 و 9 أثناء تنفيذ الحلقة، ثم نستطيع يعود إلى قيمته الأصلية 99 بعد انتهاء تنفيذها.

int i = 99; //i = 99
for (int i = 0; i < 10; i++) {    // i التصريح عن متغير جديد
}

ولكن إن كنت تريد استخدام المتغيرات المُصرّحة سلفًا وعدم إخفائها، فاحذف التصريح. انظر المثال التالي إذ نستخدم متغير i المصرَّح عنه من قبل، والذي تتغير قيمته بين 0 و 9 أثناء تنفيذ الحلقة، لكن هذه المرة ستكون قيمته بعد تنفيذها 10.

int i = 99; //i = 99
for (i = 0; i < 10; i++) { // i سنستخدم المتغير المُعرّف مسبقا
}

ملاحظات:

  • يمكن لتعليمات التهيئة (initialization) والزيادة (increment) إجراء عمليات لا تتعلق بتعليمة شرط الحلقة، بل قد تكون فارغة تمامًا إن شئت ذلك، لكن من الأفضل أن نجري عمليات لها علاقة مباشرة بالحلقة لجعل الشيفرة أسهل في القراءة والفهم.
  • لا يُرى المتغير الذي صُرِّح عنه في تعليمة التهيئة إلا داخل نطاق حلقة for، ويُحذف بمجرد إنهائها.
  • لا تنس أنّ المتغير الذي تم التصريح عنها في initialization statement يمكن أثناء الحلقة، وكذلك المتغير المُحدّد في الشرط condition.

انظر المثال التالي لحلقة تَعُدُّ من 0 إلى 10:

for (int counter = 0; counter <= 10; ++counter) {
    std::cout << counter << '\n';
}
// counter لا يمكن الوصول هنا إلى

شرح الشيفرة السابقة:

  • التعليمة int counter = 0 تهيّئ المتغير counter عند القيمة 0. لا يمكن استخدام هذا المتغير إلا داخل حلقة for.
  • التعليمة counter <= 10 هي شرط بولياني يتحقق ممّا إذا كان counter أصغر من أو يساوي 10. إذا كان الشرط صحيحًا (true) فستُنفّذ الحلقة. وإن كان false فستُنهى.
  • counter++‎ هي عملية زيادة (increment) تزيد قيمة العدَّاد بـ 1 قبل التحقق من الشرط التالي.

إن تركت جميع التعليمات فارغة، فستحصل على حلقة لا نهائية (infinite loop):

for (;;)
    std::cout << "Never ending!\n";

حلقة while اللانهائية التي تكافئ حلقة for السابقة هي:

while (true)
    std::cout << "Never ending!\n";

لكن على أي حال، يمكن إيقاف الحلقات اللانهائية باستخدام تعليمات break أو goto أو return، أو عبر رفع اعتراض (throwing an exception).

انظر المثال التالي الذي يوضح التكرار على عناصر مجموعة من المكتبة القياسية STL (مثل vector) دون استخدام الترويسة :

std::vector < std::string > names = {
    "Albert Einstein",
    "Stephen Hawking",
    "Michael Ellis"
};
for (std::vector < std::string > ::iterator it = names.begin(); it != names.end(); ++it) {
    std::cout << * it << std::endl;
}

حلقة While

تكرِّر حلقة while تنفيذ تعليمات معيّنة طالما كان شرطها صحيحًا، تُستخدم هذه الحلقة إن لم تكن تعرف عدد مرات تنفيذ جزء من الشيفرة بشكل مسبق. فمثلأ، لطباعة جميع الأعداد من 0 إلى 9، يمكن استخدام الشيفرة التالية:

int i = 0;
while (i < 10) {
    std::cout << i << " ";
    ++i; // عداد الزيادة
}
std::cout << std::endl;

تُطبع الأعداد من 0 إلى 9 مع نهاية السطر الأخير. لاحظ أن دمج التعليمتيْن الأوليين صار ممكنًا منذ الإصدار C++ 17، انظر:

while (int i = 0; i < 10)
//... بقية الشيفرة هي نفسها

يمكن استخدام الشيفرة التالية لإنشاء حلقة لا نهائية، وتستطيع إيقاف الحلقة عبر تعليمة break:

while (true) {
    // أدخل هنا أي شيء تريد فعله بلا نهاية
}

هناك صورة آخرى لحلقات while، وهي do...while. وهي موضوع الفقرة التالية.

حلقة do-while

الحلقتان التكراريتان do-while و while متشابهتان، إلا أن الأولى تتحقق من الشرط في نهاية كل تكرار، وليس في بدايته، وعليه فإنّ الحلقة ستُنفّذ مرة واحدة على الأقل. ستطبع الشيفرة التالية العدد 0 إذ سيكون تقييمُ الشرط false في نهاية التكرار الأول:

int i = 0;
do {
    std::cout << i;
    ++i; // عداد الزيادة
}
while (i < 0);
std::cout << std::endl; // يطبع صفرًا 

تنبيه: لا تنس الفاصلة المنقوطة في نهاية while(condition);‎، فهي إلزامية في بنية do-while. على النقيض من الحلقة do-while، فإن الشيفرة التالية لن تطبع شيئًا لأنّ شرط الحلقة لم يتحقق في بداية التكرار الأول:

int i = 0;
while (i < 0) {
    std::cout << i;
    ++i; // عداد الزيادة
}
std::cout << std::endl; // لن يُطبع أيّ شيء

تنبيه: يمكن إنهاء الحلقة while حتى لو لم يصبح الشرط خاطئًا باستخدام أي من التعليمات الآتية: break أو goto أو return.

int i = 0;
do {
    std::cout << i;
    ++i; // عداد الزيادة
    if (i > 5) {
        break;
    }
}
while (true);
std::cout << std::endl; // يطبع الأعداد من صفر إلى خمسة

تُستخدم الحلقة do-while أحيانًا لكتابة وحدات الماكرو (macros) التي تتطلّب نطاقًا خاصًّا بها (في هذه الحالة، يتمّ حذف الفاصلة المنقوطة الزائدة من تعريف الماكرو، ويُطلب من المستخدم توفيرها):

#define BAD_MACRO(x) f1(x); f2(x); f3(x);

// f1 الشرط لا يحمي هنا إلا استدعاء
if (cond) BAD_MACRO(var);

#define GOOD_MACRO(x) do { f1(x); f2(x); f3(x); } while(0)

// كل الاستدعاءات محميّة هنا
if (cond) GOOD_MACRO(var);

تعليمات التحكم في الحلقات: break و continue

تُستخدم عبارتَا التحكم break و continue لتغيير مسار التنفيذ من تسلسله المعتاد، فتُدمَّر جميع الكائنات الآلية (automatic objects) التي أنشئت داخل نطاق ما بمجرد ترك تنفيذٍ لذلك النطاق. وتنهي التعليمة break الحلقة فورًا دون النظر لأي عوامل أخرى.

for (int i = 0; i < 10; i++) {
    if (i == 4)
        break; // إنهاء الحلقة فورا
    std::cout << i << '\n';
}

تكون النتيجة ما يلي:

1
2
3

أما التّعليمة continue لا توقف الحلقة على الفور، بل تتخطّى بقيّة التعليمات الموجودة في متن الحلقة وتذهب إلى بداية الحلقة (بما في ذلك تعليمة التحقّق من الشّرط). انظر المثال التالي حيث تقيَّم (if (i % 2 == 0 إلى true إن كان العدد زوجيًا، وتذهب continue فورًا إلى بداية الحلقة، لكن لا يُنتَقَل إلى التعليمة التالية إن لم تُنفَّذ.

for (int i = 0; i < 6; i++) {
    if (i % 2 == 0)
        continue;
    std::cout << i << " is an odd number\n";
}

تكون النتيجة ما يلي:

1 is an odd number
3 is an odd number
5 is an odd number

لا تُستخدم break و continue إلا نادرًا، ذلك أنه يصعب معهما قراءة الشيفرة وفهمها، وتُستخدم أساليب أخرى أبسط بدلًا منهما.فمثلًا يمكن إعادة كتابة الحلقة for الأولى التي تستخدم break على النّحو التالي:

for (int i = 0; i < 4; i++) {
    std::cout << i << '\n';
}

وبالمثل، يمكن إعادة كتابة المثال الثّاني الذي يحتوي continue كالتالي:

for (int i = 0; i < 6; i++) {
    if (i % 2 != 0) {
std::cout << i << " is an odd number\n";
    }
}

التصريح عن المتغيرات في العبارات الشَّرطية

يسمح بالتصريح عن كائن في شرط حلقات for أو while، وسيُدرَج ذلك الكائن في النّطاق حتى نهاية الحلقة، وسيكون متاحًا خلال كل تكرارات الحلقة:

for (int i = 0; i < 5; ++i) {
    do_something(i);
}
// لم يعد في النطاق i
for (auto& a : some_container) {
    a.do_something();
}
// لم يعد في النطاق a
while(std::shared_ptr<Object> p = get_object()) {
    p-> do_something();
}
// لم يعد في النطاق p

لا يمكنك فعل الشيء نفسه مع حلقة do...while؛ إذ عليك التصريح عن المتغير قبل الحلقة، ثمّ وضع المتغير والحلقة داخل نطاق محلّي (local scope) إن أردت أن يُحذَف المتغير بعد انتهاء الحلقة:

// هذه الشّيفرة لن تُصرّف
do {
    s = do_something();
} while (short s > 0);
// جيّد
short s;
do {
    s = do_something();
} while (s > 0);

وذلك لأنّ متن الحلقة do...while يقيَّم قبل الوصول إلى الجزء (while)، وعليه فإن التصاريح الموضوعة في ذلك الجزء لن تكون مرئيّة أثناء التّكرار الأول للحلقة.

تكرار حلقة for على نطاق فرعي

تستطيع التكرار على جزء فرعي من حاوية أو نطاق ما باستخدام الحلقات النطاقية (range-base loops)، وذلك من خلال إنشاء كائن وكيل (proxy object).

template < class Iterator, class Sentinel=Iterator >
    struct range_t {
        Iterator b;
        Sentinel e;
        Iterator begin() const {
            return b;
        }
        Sentinel end() const {
            return e;
        }
        bool empty() const {
            return begin() == end();
        }
        range_t without_front(std::size_t count = 1) const {
           if (std::is_same< std::random_access_iterator_tag, typename std::iterator_traits<Iterator>::iterator_category >{} ) {
                count = (std::min)(std::size_t(std::distance(b, e)), count);
            }
            return {
                std::next(b, count),
                e
            };
        }
        range_t without_back(std::size_t count = 1) const {
            if (std::is_same< std::random_access_iterator_tag, typename std::iterator_traits<Iterator>::iterator_category >{} ) {
                count = (std::min)(std::size_t(std::distance(b, e)), count);
            }
            return {
                b,
                std::prev(e, count)
            };
        }
    };
template < class Iterator, class Sentinel >
    range_t<Iterator, Sentinel> range( Iterator b, Sentinal e ) {
        return {
            b,
            e
        };
    }
template < class Iterable >
    auto range(Iterable & r) {
        using std::begin;
        using std::end;
        return range(begin(r), end(r));
    }
template < class C >
    auto except_first(C & c) {
        auto r = range(c);
        if (r.empty()) return r;
        return r.without_front();
    }

نستطيع الآن فعل ما يلي:

std::vector < int > v = {1, 2, 3, 4};

for (auto i: except_first(v))
    std::cout << i << '\n';

يكون الناتج:

2
3
4

يجب أن تتذكّر أنّ الكائنات الوسيطة (intermediate objects) المُنشأة في الجزء for(:range_expression)‎ من حلقة for ستُحذف عند بدء تنفيذ الحلقة.

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

ترجمة -بتصرّف- للفصل Chapter 11: Loops‎ من الكتاب C++ Notes for Professionals





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


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



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

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

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


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

تسجيل الدخول

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


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