تنفذ الحلقات التكرارية مجموعة من التعليمات إلى حين استيفاء شرط معين، وهناك ثلاثة أنواع من تلك الحلقات التكرارية في لغة 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
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.