فصل سوم
حلقههای تکرار
دستور while
یکی از کارهای رایج برنامهنویسی استفاده از حلقههای تکرار است. کاربرد حلقههای تکرار وقتی است که میخواهیم مجموعهای از دستورات به تعداد دلخواه تکرار شود. با توجه به اینکه بسیاری از محاسبات نیازمند تکرار دستورات خاصی هستند، استفاده از حلقههای تکرار در برنامهنویسی متداول است.
دستور while یکی از دستوراتی است که ++C برای طراحی حلقههای تکرار ارائه میدهد. شکل استفاده از این دستور در مثال زیر نشان داده شده است:
خروجی:
قالب دستور while به این صورت است که یک شرط پس از آن نوشته میشود و در خط یا خطوط بعدی تعدادی دستور که میخواهیم بر اساس شرط مذکور تکرار شوند، قرار میگیرند. مفهوم آن نیز بسیار شبیه زبان محاوره است: تا وقتیکه شرط برقرار است، دستورات موردنظر را اجرا کن.
در مثال فوق تا وقتیکه x کوچکتر از 5 باشد، آن را چاپ کرده و یک واحد به آن اضافه میکند. درواقع هر بار فرایند زیر تکرار میشود:
-
شرط را ارزیابی کن.
-
اگر مقدار شرط درست است (True)، تکتک دستورات بدنه حلقه را اجرا کن و مجدداً به مرحله 1 برگرد.
-
در غیر این صورت اجرای برنامه را از اولین دستور پس از حلقه ادامه بده.
بدنه حلقه شامل تمام دستوراتی است که در خطهای بعدی دستور while نوشتهشدهاند و دارای تورفتگی هستند.
حلقههای while از سه رکن اصلی تشکیل میشوند:
-
مقداردهی اولیه متغیر حلقه: منظور از متغیر یا متغیرهای حلقه، متغیر(هایی) است که در شرط حلقه مقدار آنها کنترل میشود و در بدنه نیز مقدار آنها تغییر میکند. در مثال فوق x متغیر حلقه محسوب میشود. باید پیش از حلقه مقدار متغیر حلقه مشخص شود. این مقدار معمولاً تعیینکننده این است که حلقه چند بار اجرا خواهد شد.
-
کاهش یا افزایش مقدار متغیر حلقه: مقدار متغیر حلقه باید در بدنه حلقه تغییر کند بهنحویکه درنهایت به نقطهای برسد که شرط حلقه نادرست شود و حلقه خاتمه پیدا کند. در شرایطی که تغییر متغیر فراموش شود و یا تغییرات بهگونهای باشد که هیچگاه شرط حلقه نادرست نشود، اجرای حلقه هرگز خاتمه پیدا نمیکند و اصطلاحاً حلقه بینهایت یا حلقه بیپایان تشکیل میشود.
-
شرط حلقه: شرط حلقه مشخص میکند که اجرای حلقه تا چه زمانی ادامه پیدا کند و تحت چه شرایطی خاتمه پیدا کند.
هر سه بخش فوق باید متناسب باهم و بهدرستی تعریف شده باشند. در غیر این صورت برنامه عملکرد درستی نخواهد داشت. به دو مثال زیر توجه کنید:
حلقه فوق مقدار متغیر حلقه را از 10 شروع میکند و تا وقتی بزرگتر از 0 باشد آن را چاپ کرده و یک واحد از آن کم میکند. به عبارتی اعداد 10 تا 1 را چاپ میکند.
حال چنانچه این مثال را بهصورت زیر تغییر دهیم، متغیر حلقه از 1 تا 10 تغییر خواهد کرد و اعداد 1 تا 10 را چاپ خواهد کرد.
به تفاوت بین ارکان سهگانه فوق در دو مثال فوق دقت کنید. لزوماً باید سه بخش باهم متناسب باشند. بهعنوانمثال چنانچه این مثال به شکل زیر تغییر کند، هدف موردنظر محقق نشده و عملاً حلقه بینهایت خواهیم داشت.
مثال: برنامهای بنویسید که عدد n را دریافت کند و n! را چاپ کند.
مثال: برنامهای بنویسید که نمره بیست دانشجو را دریافت کند و کمترین نمره، بیشترین نمره، مجموع نمرات و میانگین نمرات را محاسبه نماید.
مثال: برنامهای بنویسید که اعداد صحیح x و y را از کاربر بگیرد و x به توان y را محاسبه نماید.
مثال: برنامهای بنویسید که مقدار تابع سینوس x را با استفاده از بسط زیر محاسبه کند:
ازآنجاییکه در برنامهنویسی محاسبه بینهایت جمله سمت راست معادله امکانپذیر نیست، عملاً بهجای سینوس x، تقریبی از آن محاسبه میشود. فرض میکنیم که میخواهیم 25 جمله اول از این بسط را محاسبه کنیم. برای این منظور از حلقه while استفاده میکنیم:
متغیر sign برای محاسبه علامت هر یک از جملات این بسط به کار میرود که یکدرمیان مثبت و منفی خواهد شد. هر بار یک جمله محاسبه شده و مقدار آن در متغیر term قرار میگیرد. مجموع این مقادیر هم در متغیر s قرار داده میشود.
تمرین: برنامهای بنویسید که جدولی از تبدیل واحد فارنهایت به سلسیوس را محاسبه و چاپ کند. ستون اول شامل اعداد 0، 10، 20،...، 100 و ستون دوم مقدار متناظر به سلسیوس باشد.
مثال: برنامهای بنویسید که مقدار عدد پی را از روی بسط زیر با دقت 0.0001 محاسبه نماید.
حل: برای محاسبه سریها، میتوان از حلقههای تکرار استفاده نمود. کافی است در یک حلقه تکرار، هر بار یک جمله از این سری محاسبه شود و با جملات قبلی جمع شود. برای اینکه بتوانیم علامت جمله را بهصورت یکدرمیان مثبت و منفی کنیم، از توانهای -1 استفاده میکنیم. با توجه به اینکه 〖-1〗^n برحسب زوج یا فرد بودن n یکدرمیان منفی و مثبت میشود، میتواند در تولید علامت برای مواردی مشابه این مسئله مورداستفاده قرار گیرد.
نکته مهم دیگر شرط پایان حلقه است. در این مثال دقت محاسباتی تعیینشده میتواند در تنظیم شرط پایان حلقه به کار گرفته شود. ازآنجاییکه هرقدر جملات بیشتری از این سری محاسبه شود، دقت محاسباتی افزایش پیدا میکند و خطای محاسباتی کاهش مییابد، و از طرفی بهتدریج با افزایش جملات، مقدار محاسبهشده به جواب موردنظر همگرا میشود، لذا میتوان محاسبه را تا جایی ادامه داد که جمله جدید یا به عبارتی اختلاف بین دو مقدار محاسبهشده جدید و قدیم، از حد خطای مورد انتظار کمتر باشد. به برنامه زیر توجه کنید:
مثال: تابع پیوسته و همواره مثبت f به ما داده شده است. اطلاع دقیقی از شکل و جزئیات این تابع در دسترس نیست. میخواهیم انتگرال این تابع را در فاصله a تا b محاسبه کنیم.
حل: پیدا کردن تابع انتگرال در مورد بسیاری از توابع کار پیچیده است اما محاسبه آن به کمک سیستم بهراحتی امکانپذیر است. برای این منظور کافی است بازه موردنظر را به گامهای کوچکی تقسیم کنیم و با محاسبه سطح زیر منحنی مقدار انتگرال را حساب کنیم. برای هر یک از بازههای کوچک انتخابشده، میتوان با تقریب فرض کرد که قطعه انتخابی یک مستطیل است و با کمک فرمول محاسبه مستطیل مساحت آن قطعه را محاسبه نمود. بدیهی است هرچقدر طول گامها کوچکتر شود، دقت محاسبه افزایش خواهد یافت. در این مثال فرض میکنیم انتگرال تابع exp(x) را پیدا میکنیم که میتوان آن را با توابع دلخواه جایگزین کرد.
ترکیب حلقه با دستورات شرطی
حلقههای تودرتو
مثال: خروجی برنامه زیر چیست؟
خروجی این برنامه 6 ستاره است.
مثال: خروجی برنامه زیر چیست؟
پاسخ: این برنامه 15 ستاره چاپ خواهد کرد. برنامه را طوری تغییر دهید که علاوه بر ستاره، مقدار x و y را نیز چاپ نماید تا با تغییر متغیرها و نحوه عملکرد حلقه، بهتر آشنا شوید.
مثال: خروجی برنامه زیر چیست؟
پاسخ: این برنامه 16 ستاره چاپ خواهد کرد. برنامه را طوری تغییر دهید که علاوه بر ستاره، مقدار x و y را نیز چاپ نماید تا با تغییر متغیرها و نحوه عملکرد حلقه، بهتر آشنا شوید.
مثال: خروجی برنامه زیر چیست؟
پاسخ: در حلقه اول، مقدار متغیر حلقه تغییر نمیکند و این حلقه، یک حلقه بیپایان است و هیچگاه نوبت به اجرای حلقه دوم نمیرسد.
مثالهای تکمیلی while
مثال: تابع فرضی f(x) را در نظر بگیرید. این تابع پیوسته بوده و میدانیم در فاصله بین a تا b دارای ریشه است. اطلاع بیشتری از شکل تابع در اختیار ما نیست. مثلاً ممکن است تابع هر شکل دلخواهی داشته باشد. از طرف دیگر میدانیم علامت تابع در a و b متفاوت است. مطلوب است پیدا کردن ریشه تابع در فاصله مذکور.
حل: معمولاً برای پیدا کردن ریشه معادلات ساده، فرمولهای محاسباتی ارائه شده است اما پیدا کردن فرمول برای توابع پیچیده کار آسانی نبوده و در بسیاری از موارد امکانپذیر نیست. یکی از راهکارهای مؤثر در حل چنین مسائلی استفاده از تکنیکهای محاسبات عددی است که معمولاً دانشجویان رشتههای مهندسی بهطور کامل با گذراندن درسی به همین نام با این تکنیکها آشنا میشوند. در اینجا روش تنصیف که یک تکنیک ساده محاسبات عددی است را جهت آشنا شدن با نحوه کاربرد حلقه while معرفی میکنیم.
روش تنصیف به این صورت عمل میکند:
۱ - بازه [a,b] را طوری انتخاب میکنیم که f(a). f(b)<0
۲ - بازه را نصف میکنیم، c = (a+b) / 2
۳ –بسته به اینکه نقطه وسط، با کدامیک از نقاط a و b هم علامت باشد بهصورت زیر تصمیمگیری میشود:
الف : f(a) f(c)<0 یک ریشه در بازه [a,c] وجود دارد.
ب : f(c) f(b)<0 یک ریشه در بازه [b,c] وجود دارد.
ج : f(c) = 0 در این صورت x=c ریشه معادله است.
با ادامه دادن مراحل فوق و تقسیم مکرر بازهای که شامل جواب است، بهتدریج اندازه این بازه کوچکتر شده و به جواب نزدیکتر میشویم. هرقدر تعداد دفعات تکرار مراحل فوق بیشتر شود، دقت افزایش مییابد و جواب دقیقتری محاسبه میشود. بنابراین لازم است مراحل فوق در قالب یک حلقه اجرا شوند. برای تعیین شرط خاتمه حلقه میتوانیم از فاصله نقاط a و b کمک بگیریم. وقتی این فاصله از حد مشخصی که همان دقت قابل قول مسئله است، کمتر شد، میتوانیم حلقه را خاتمه دهیم. بدیهی است چنانچه در این شرایط نقطه c را بهعنوان جواب اعلام کنیم، مطمئن هستیم که خطای جواب از خطای قابلقبول کوچکتر است.
حلقههای کنترلشده با یک واقعه
انواع متعددی از حلقههای کنترلشده با یک واقعه موجود میباشند:
حلقههای کنترلشده با یک مقدار قراردادی: گاهی حلقه برای خواندن و پردازش تعداد نامشخصی از داده بکار میرود که تعداد آن از پیش مشخص نیست. هر دفعه که بدنه حلقه اجرا میشود، داده جدیدی خوانده و مورد پردازش واقع میشود. اغلب یک داده خاص قراردادی برای تشخیص اینکه داده دیگری وجود ندارد به کار گرفته میشود. این داده قراردادی باید چیزی باشد که در لیست عادی ورودیها وجود نداشته باشد. برای مثال اگر برنامهای نمرات را بهعنوان ورودی میخواند، میتوانیم 1- را بهعنوان داده قراردادی نشانه اتمام دادهها در نظر بگیریم:
اولین داده قبل از ورود به حلقه خوانده میشود و اگر 1- نباشد پردازش میشود. در پایان حلقه داده جدیدی خوانده میشود و کنترل به ابتدای حلقه باز میگردد. اگر داده جدید 1- نباشد نظیر اولی پردازش میشود. وقتی با داده ورودی 1- مواجه شویم ، شرط حلقه نادرست میشود و برنامه از حلقه خارج میشود (بدون پردازش این داده).
به دو نکته در کد فوق توجه کنید: اول آنکه قبل از ورود به حلقه باید متغیرx مقداردهی شده باشد. دوم آنکه گرفتن مقدار جدید و تغییر در حلقه در پایان حلقه انجام گرفته است. اگر این کار در ابتدای حلقه انجام شود سبب میشود که مقدار اولیه بدون پردازش از میان برود.
در اغلب مسائل مقدار معیار بهخودیخود به ما دیکته میشود. برای مثال اگر در مسئله مقدار صفر مجاز نباشد صفر معیار است. گاهی اوقات ترکیبی از مقادیر، مجاز نیست، مثلاً در مورد تاریخ، ترکیب ماه اسفند و روز سی و یکم چنین وضعیتی دارد. بعضی وقتها بازهای از مقادیر (مثلاً مقادیر منفی) میتواند بهصورت قراردادی برای پایان حلقه در نظر گرفته شود. هنگام پردازش خطوطی از دادههایی از نوع کاراکتر که در آن کار پردازش خط به خط انجام میشود، کاراکتر خط جدید '\n' برای اتمام پردازش یک خط از اطلاعات مورداستفاده قرار میگیرد. کد ذیل کاراکترهای یک خط را میخواند.
(توجه کنید که در اینجا برای ورود کاراکترها از تابع get استفاده کردیم و نه از عملگر >> یادآوری میشود که عملگر >> از کاراکترهای فضای سفید whitespace شامل کاراکتر خالی blank و خط جدید صرفنظر میکند. در این مثال میخواهیم هر کاراکتری حتی blank و بخصوص کاراکتر خط جدید خوانده شوند.)
هنگام خواندن داده از دستگاههای ورودی استاندارد مثل صفحهکلید (با جریان داده cin ) میتوان از کاراکتر خاصی برای اتمام ورودی استفاده کرد. در سیستمعامل یونیکس با تایپ Ctrl/D (گرفتن همزمان کلید Ctrl و D ) و در ویندوز با Ctrl/Z میتوان پایان ورود اطلاعات را اعلام کرد.
حلقههای کنترلشده با یک نشانه یا علامت (flag): نشانه متغیری است منطقی که برای کنترل جریان منطقی برنامه میتواند به کار گرفته شود. میتوان یک متغیر منطقی را بهعنوان نشانه قبل از شروع حلقه با TRUE مقداردهی اولیه کرد، سپس وقتی بخواهیم حلقه را متوقف کنیم، مقدار نشانه را به FALSE تغییر مقدار میدهیم. یعنی میتوانیم با نشانه وقوع واقعهای که پردازش را کنترل میکند ثبت کنیم. برای مثال برنامه ذیل اعدادی را میخواند و با هم جمع میکند تا اینکه مقدار ورودی منفی شود. (متغیر nonNegative منطقی و نشانه است. دیگر متغیرها صحیح هستند.)
لازم نیست نشانه حتماً با TRUE مقداردهی شود. صورت دیگر این برنامه که نشانه با FALSE مقداردهی شده است چنین است:
مثالهای بیشتر
یکی از اهداف عمده حلقهها نگهداری تعداد دفعاتی است که حلقه اجرا میشود. برای مثال قطعه برنامه زیر کاراکترهای ورودی را میخواند و آنها را میشمارد و تا به کاراکتر نقطه برسد. (inChar از نوع char و count از نوع int است). در این مثال حلقه کنترلشده با یک شمارنده نیست زیرا از متغیر شمارنده برای کنترل حلقه استفاده نکردهایم.
حلقه تا زمانی که نقطه خوانده میشود ادامه دارد. بعد از اتمام حلقه متغیر count از تعداد کاراکترهای خوانده شده یکی کمتر را دربر دارد. یعنی تعداد کاراکترهای خواندهشده را تا رسیدن به نقطه و نه خود آن را میشمارد. توجه کنید اگر اولین کاراکتر خواندهشده نقطه باشد به بدنه حلقه وارد نخواهیم شد و مقدار count صفر باقی خواهد ماند.
متغیر شمارنده در این مثال شمارنده تکرار خوانده میشود زیرا مقدار آن برابر با تعداد تکرارهای حلقه است.
شمارنده تکرار (Iteration Counter) شمارندهای که با هر تکرار یکی اضافه میشود.
به مثال دیگری توجه کنید. میخواهیم اولین 10 عدد فرد در یک مجموعه اعداد را با هم جمع کنیم. ابتدا باید هر عدد را که میخوانیم امتحان کنیم و ببینیم فرد است یا خیر. برای این کار از عملگر % استفاده میکنیم. اگر number%2 برابر 1 باشد number فرد است و در غیر این صورت زوج است. اگر عدد ورودی زوج باشد کاری انجام نمیدهیم اگر فرد باشد شمارنده را یکی افزایش داده و عدد فرد خواندهشده را با sum جمع میکنیم. از یک نشانه برای کنترل حلقه استفاده کردهایم. در کد زیر همه متغیرها از نوع int هستند، بهجز متغیر lessThanTen که منطقی است.
شمارنده در این مثال یک شمارنده وقایع است که ابتدا با صفر مقداردهی اولیه شده است و با وقوع واقعه خاصی افزایش مییابد. بنابراین ربطی بین این شمارنده و تعداد دفعات تکرار حلقه وجود ندارد.
شمارنده واقعه (Event Counter) متغیری که وقوع واقعهای را میشمارد
جدول
در برنامهنویسی بهویژه در رشتههای مهندسی یکی از کاربردهای حلقههای تکرار تولید جداول اطلاعاتی است. ارائه اطلاعات در قالب جدول در مسائل مختلف کاربرد دارد. بهعنوانمثال فرض کنید میخواهیم جدول لگاریتمی تولید کنیم. تکه برنامه زیر جدول لگاریتمی را در دو ستون محاسبه و چاپ میکند. ستون اول شامل لیستی از اعداد 1 تا 10 است و ستون دوم لگاریتم هر عدد را نشان میدهد:
کاراکتر \t یا tab باعث میشود ستون دوم بهصورت منظم و کاملاً زیر هم و با فاصله مناسب چاپ شود. خروجی این برنامه بهصورت زیر است:
جداول دوبعدی
حال فرض کنید میخواهیم اطلاعات را در قالب یک جدول دوبعدی چاپ کنیم، به این صورت که هر یک از اقلام این جدول به مقدار سطر و ستون خاصی وابسته باشد. مثلاً میخواهیم جدول مسافت بین شهرها را چاپ کنیم و یا جدول ضرب را به کاربر نمایش دهیم. بهعنوانمثال فرض کنید میخواهیم برنامهای بنویسیم که جدول ضرب را برای ما چاپ کند. در چنین مواردی باید از حلقههای تودرتو استفاده کنیم:
حلقه داخلی وظیفه چاپ یک سطر از جدول ضرب را به عهده دارد. حلقه بیرونی هم باعث میشود سطرهای مختلف جدول ضرب چاپ شود. خروجی این برنامه به شکل زیر خواهد بود:
چنانچه شرط پایان حلقه داخلی را اصلاح کنیم میتوانیم به جداولی بهصورت پایین قطری یا بالا قطری و جداولی که در نمایش مسافت بین شهرها مرسوم است، دست پیدا کنیم. به مثال زیر توجه کنید:
همانگونه که مشاهده میشود شرط پایان حلقه داخلی تغییر کرده است و بهجای اعداد کوچکتر یا مساوی 6، اعداد کوچکتر یا مساوی i را شامل میشود. خروجی این برنامه به شکل زیر تغییر خواهد کرد:
دستور For
دستور For برای سادهتر نوشته شدن حلقههای کنترل شده با شمارنده طراحی شده است. دستور زیر اعداد صحیح از 1 تا n را چاپ میکند:
دستور For فوق به این معنی است که «متغیر کنترل حلقه count را به 1 مقداردهی اولیه کن. تا زمانی که count از n کوچکتر یا مساوی آن است، دستور خروجی را اجرا کن و یکی به count اضافه کن. حلقه را بعد از آنکه count به n+1 رسید متوقف کن.»
در C++ دستور For صورت فشرده دستور While است. در حقیقت، کمپایلر دستور For را به حلقه While معادل آن به صورت زیر ترجمه میکند.
ساختار دستوری دستور For چنین است:
Expression1 شرط While است: InitStatement یکی از موارد ذیل میتواند باشد: یک دستور خالی یعنی فقط یک ویرگول نقطه «; »، یک تعریف که به «; » ختم شده است، یا یک عبارت باز هم مختوم به «; » بنابراین همواره قبل از Expression1 ویرگول نقطه «; » قرار دارد.
اغلب InitStatement به عنوان متغیر کنترل حلقه مقداردهی میشود و Expression2 متغیر کنترل حلقه را کاهش یا افزایش میدهد. دو مثال ذیل تعداد تکرار یکسانی دارند (50 بار).
نظیر حلقه while، حلقه for نیز میتوانند به صورت تو در تو بکار روند. برای مثال ساختار for تو در تو ذیل:
خروجی زیر را چاپ میکند:
به عنوان مثال دیگر ساختار For تودرتو زیر:
مثلث زیر را چاپ میکند:
اگرچه دستور For عمدتاً برای حلقههای کنترل شده با شمارنده بکار میرود، C++ این امکان را در اختیار میگذارد که هر حلقهای با دستور For پیادهسازی شود. برای بکارگیری هوشمندانه دستور For به نکات ذیل توجه کنید:
1 . در ساختار دستوری، InitStatement میتواند یک دستور خالی (null) باشد و Expression2 نیز اختیاری است. اگر Expression2 حذف شود، دستور While پیادهسازی میشود:
مشابه دستور ذیل است.
2 . طبق ساختار دستوری Expression2 اختیاری است. اگر آن را حذف کنیم (به این معنی است که Expression1 ، TRUE است) TRUE فرض میشود. حلقه:
با حلقه ذیل معادل است:
هر دو حلقه بیانتهای فوق "Hi" را بینهایت بار چاپ میکنند.
3 . دستور مقداردهی، InitStatement میتواند شامل تعریف نیز باشد:
دقت مختصری در اینجا نیاز است. متغیر i برای بدنه حلقه محلی است و دامنه آن فقط تا انتهای دستور For گسترش مییابد. لذا نوشتن کد ذیل خطائی از جانب کمپایلر دربر ندارد.