یکی از ابزارهای پر استفاده و مهم هر زبان برنامهنویسی حلقههای تکرار هستند. وجود چنین ابزاری به برنامهنویس این امکان را میدهد که ساختارهای نیازمند به تکرار مجموعه دستورات (مانند جستجو، گزارشگیری، محاسبات، دریافت اطلاعات از کاربر یا فایل) را پیادهسازی کند.
هر زبانی عموما شامل چندین نوع حلقه تکرار است که هر کدام به نحوی به برنامهنویس در نوشتن کدهای مختصر و با مفهوم کمک میکنند. در این فرصت با انواع حلقههای تکرار در زبان برنامهنویسی ++C آشنا میشویم.
حلقه تکرار while
[برگرد بالا]
این نوع حلقه سادهترین نوع حلقه تکرار در این زبان برنامهنویسی است. فرم کلی حلقه while به این صورت است:
while(شرط اجرای حلقه){
دستورات داخل حلقه
}
عبارتهای داخل حلقه تا زمانی که شرط اجرای حلقه صحیح باشد اجرا خواهند شد. به عنوان مثال:
int n = 1;
while(n <= 10){
cout << n << endl;
n++;
}
در این قطعه کد، ابتدا متغیر n با عدد یک مقداردهی میشود. سپس شرط n ≤ 10 بررسی میشود که صحیح است. پس اجرای قطعه کد با دستورات داخل حلقه ادامه پیدا میکند. در این حلقه مقدار n چاپ شده و یک واحد به آن اضافه میشود. سپس کنترل برنامه به ابتدای حلقه باز میگردد. اگر شرط حلقه همچنان صحیح باشد، عبارتهای داخل آن مجددا اجرا خواهند شد. در نتیجه قطعه کد فوق اعداد یک تا ده را به ترتیب در سطرهای جداگانه خروجی چاپ خواهد کرد.
تذکر: شرط اجرای حلقه قبل از ورود به آن نیز بررسی میشود. اگر این شرط از همان ابتدا نادرست باشد، دستورات داخل حلقه هیچگاه اجرا نخواهند شد.
int n = 11;
while(n <= 10){
cout << n << endl;
n++;
}
این قطعه کد خروجی ندارد. چرا که شرط اجرای حلقه هنگام ورود به آن نیز نادرست است.
حلقه تکرار do-while
[برگرد بالا]
فرم کلی این حلقه به صورت زیر است:
do{
دستورات داخل حلقه
}while(شرط اجرای حلقه);
تنها تفاوت این حلقه با حلقه while در این است که شرط اجرای حلقه do-while در انتهای آن بررسی میشود. به عنوان مثال:
int n = 1;
do{
cout << n << endl;
n++;
}while(n <= 10);
در این قطعه کد نیز همانند قطعه کد قبلی اعداد یک تا ده در خروجی چاپ میشوند.
تفاوت این دو حلقه در قطعه کد زیر - که برای حلقه while هم نوشته شده بود - آشکار میشود:
int n = 11;
do{
cout << n << endl;
n++;
}while(n <= 10);
همانگونه که عنوان شد، در حلقه while شرط اجرای دستورات داخل حلقه در ابتدای آن بررسی میشود. اما در حلقه do-while این شرط در انتهای آن قرار دارد. در نتیجه دستورات داخل حلقه قبل از رسیدن به شرط حلقه یک بار اجرا میشوند. یعنی قطعه کد فوق عدد 11 را چاپ کرده و سپس با توجه به اینکه شرط n ≤ 10 نادرست است، کنترل برنامه به داخل حلقه باز نمیگردد.
به طور خلاصه میتوان گفت: تفاوت حلقه do-while با حلقه while در این است که دستورات داخل حلقه do-while حداقل یک بار اجرا میشوند. بیشتر کاربردهای چنین حلقهای هم به خاطر همین خاصیت آن است.
حلقه تکرار for
[برگرد بالا]
سادهترین نوع تعریف حلقه for به این ترتیب است:
for(نمو ; شرط اجرای حلقه ; مقداردهی اولیه){
دستورات داخل حلقه
}
به مثال زیر توجه کنید:
int n;
for(n = 1 ; n <= 10 ; n++){
cout << n << endl;
}
این حلقه نیز اعداد یک تا ده را در خروجی چاپ میکند. اما چگونه؟
با اجرای خط اول، متغیر n تعریف میشود. سپس بخش «مقداردهی اولیه» اجرا شده و مقدار n برابر عدد یک میشود. پس از آن «شرط اجرای حلقه» بررسی میشود. این شرط همانند شروط حلقههای قبلی عمل کرده و در صورت نادرست بودن کنترل برنامه از حلقه خارج میشود. اما اگر شرط صحیح باشد کنترل برنامه وارد حلقه شده و دستورات داخل آن اجرا میشوند. در اجرای بعدی بخش «مقداردهی اولیه» اجرا نمیشود. اما قبل از بررسی «شرط اجرای حلقه»، عملیات بخش «نمو» اجرا میشوند. همانگونه که شرح داده شد، این عملیات در اجرای اول و زمان ورود به حلقه اجرا نمیشوند. پس از نمو، شرط اجرای حلقه بررسی شده و به همین ترتیب اجرای برنامه ادامه پیدا میکند.
توجه داشته باشید که لزومی ندارد نمو همواره افزایش یک واحدی باشد:
for(n = 1 ; n <= 10 ; n += 1)
for(n = 1 ; n <= 10 ; n += 2)
for(n = 10 ; n >= 1 ; n -= 1)
for(n = 10 ; n >= 1 ; n--)
for(n = 1 ; n <= 100 ; n *= 2)
for(n = 100 ; n >= 1 ; n /= 10)
تمامی این عبارتها صحیح هستند.
چنین ساختاری در اکثر زبانهای برنامهنویسی وجود دارد. بزرگترین ویژگی این روش، کنترل شمارشی حلقهها است. اکثر کاربردهای این حلقه به حالتی باز میگردد که قرار است مجموعه دستوراتی به تعداد معینی انجام شوند. به عنوان مثال تابع زیر مجموع عناصر یک آرایه از اعداد صحیح را محاسبه میکند:
int sum(int arr[], int size){
int i, sum = 0;
for(i = 0 ; i < size ; i++){
sum += arr[i];
}
return sum;
}
همانطور که میدانید، اندیس آرایهها در زبان ++C از صفر شروع میشوند. پس اگر تعداد عناصر آن size باشد، اندیسها از صفر تا size - 1 خواهند بود. حلقه for فوق نیز با شروع از عدد صفر و افزایش یک واحد در هر اجرا، مجموع عناصر اندیسهای صفر تا size - 1 را محاسبه میکند.
نکته: تعریف ارائه شده برای حلقه for میتواند با استفاده از حلقه while به صورت زیر شبیهسازی شود:
مقداردهی اولیه
while(شرط اجرای حلقه){
دستورات داخل حلقه
نمو
}
این شبیهسازی دو حسن دارد. اول اینکه عملکرد حلقه for و ترتیب اجرای بخشهای سهگانه آن بهتر مشخص میشود. و دوم، ما را متوجه نکته مهمی میکند: لزومی ندارد بخشهای سهگانه حلقه for از عبارتهای محاسباتی تشکیل شده باشند. هر کدام از بخشهای مقداردهی اولیه و نمو میتوانند شامل هر دستور متعارفی از این زبان باشند. شرط اجرای حلقه هم میتواند هرگونه شرطی (نه لزوما محاسباتی) باشد. در ضمن توجه داشته باشید که وارد کردن اطلاعات هر کدام از این بخشها اختیاری است. به مثال ساده زیر توجه کنید:
int n = 0;
for(; n < 0 || n > 20 ;){
cout << "Enter a number between 0 and 20: ";
cin >> n;
}
چنین حلقهای بدون مقداردهی اولیه و نمو است. در صورتی که کاربر عددی بیرون از بازه صفر و بیست وارد کند، شرط ادامه حلقه صحیح بوده و برنامه مجددا با چاپ پیامی منتظر ورود اطلاعات خواهد ماند. البته حلقههایی نظیر این حلقه بیشتر با while یا do-while پیادهسازی میشوند.
نکته: حلقه for به صورت زیر نیز استفاده میشود:
for(int n = 1 ; n <= 10 ; n++){
دستورات داخل حلقه
}
در این حالت متغیر n تنها در داخل بلوک حلقه تعریف شده و امکان استفاده از آن در ادامه کد وجود ندارد. به عبارت دیگر، قطعه کد فوق معادل با قطعه کد زیر نیست:
int n = 1;
while(n <= 10){
دستورات داخل حلقه
n++;
}
حلقه for محدودهای (Ranged-Based)
[برگرد بالا]
یکی از کاربردهای مهم حلقه for پیمایش مجموعهای از دادهها (مثل آرایه یا vector) از ابتدا تا انتها به منظور انجام عملیات مختلف است:
int array[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
for(int i = 0 ; i < 10 ; i++){
cout << array[i] << endl;
}
استاندارد c++11 شکل جدیدی از حلقه for برای چنین کاربردی پیشنهاد میدهد:
for(int x : array){
cout << x << endl;
}
بر اساس این تعریف در هر تکرار یک عضو آرایه array در متغیر x قرار گرفته و دستورات داخل حلقه اجرا میشود. این ساختار برای هر نوع مجموعهای از اشیاء که قابلیت تشخیص ابتدا و انتهای آن وجود داشته و عملگرهایی برای پیمایش آنها تعریف شده باشد (مثل آرایه، vector، list و ...) قابل استفاده است.
تذکر: زمانی که یک آرایه پارامتر ارسالی به تابع باشد، آدرس اولین خانه آرایه در تابع دریافت شده و امکان تشخیص طول تعریف شده اصلی وجود ندارد. بنابراین قطعه کد زیر اجرا نمیشود:
void func(int arr[]){
for(int x : arr)
{
cout << x << endl;
}
}
تذکر: زمانی که متغیر به صورت
for(
int x : array) تعریف میشود، در هر تکرار یک کپی از مقدار عنصر در x قرار میگیرد. در نتیجه تغییر x اثری در آرایه ندارد. به عنوان مثال، قطعه کد زیر تغییری در عناصر array ایجاد نمیکند:
for(int x : array){
cin >> x;
}
در این حالت متغیر x باید به صورت متغیر مرجع تعریف شود:
for(int &x : array){
cin >> x;
}
از قطعه کدی مانند for(const className &x : array) نیز زمانی استفاده میشود که هدف تغییر دادن عناصر مجموعه array نبوده، اما کپی هر شی از نوع className در متغیر x از لحاظ زمانی یا مصرف حافظه به صرف نیست. در چنین شرایطی استفاده از متغیر مرجع (برای اجتناب از کپی) با کلمه کلیدی const (برای جلوگیری از تغییر) توصیه میشود.
ماکروی for_each
[برگرد بالا]
ماکروی for_each از مجموعه توابع کتابخانه استاندارد ++C است که تعریف آن در فایل سرآیند algorithm قرار دارد. این ماکرو نیز همانند حلقه for محدودهای عمل میکند. با این تفاوت که امکان تعیین ابتدا و انتهای بازه مورد نظر وجود دارد:
void print(int n){
cout << n << endl;
}
int main(){
std::vector<int > arr = {1, 2, 3, 4, 5};
for_each(arr.begin() + 1, arr.end() - 1, print);
}
خروجی این برنامه اعداد 2 تا 4 بوده و عملکرد ماکروی for_each معادل حلقه for زیر است:
for(std::vector<int>::iterator it = arr.begin() + 1 ; it != arr.end() - 1 ; it++){
print(*it);
}
دستور break
[برگرد بالا]
این دستور برای خروج از داخل حلقه استفاده میشود. به عبارت دیگر، زمانی که کنترل برنامه در داخل حلقه به این دستور برسد، از آن حلقه خارج شده و به اولین خط بعد از دستورات حلقه منتقل میشود. مثال سادهای از کاربرد این دستور را میتوان در عملیات جستجوی خطی یافت.
تابع زیر در آرایه n عنصری arr (با عناصر متمایز) به دنبال خانهای با محتوای x میگردد و شماره اندیس این خانه را به عنوان نتیجه باز میگرداند:
int search(int[] arr, int n, int x){
int i, result = -1;
for(i = 0 ; i < n ; i++){
if(arr[i] == x){
result = i;
}
}
return result;
}
روش کار تابع ساده است: با استفاده از حلقه تکرار for اندیسهای صفر تا n - 1 برای یافتن عنصر مورد نظر پیمایش میشوند. اگر چنین عنصری یافت شود شماره اندیس خانه مربوطه در result قرار میگیرد. اگر یافت نشود، مقدار result همان منفی یک باقی میماند که در تعریف آن مقداردهی شده است. چنین عددی هرگز نمیتواند شماره اندیس یک خانه آرایه در زبان ++C باشد. بنابراین روش خوبی برای نشان دادن عدم موفقیت عملیات جستجو است.
این تابع جستجو از لحاظ الگوریتمی ایرادی ندارد. اما میتوان در پیادهسازی آن کمی دقیقتر بود. فرض کنید n برابر صد هزار بوده و عدد x در خانه دوم آرایه قرار داشته باشد. پس در تکرار دوم حلقه، عنصر مورد نظر یافت میشود. اما کنترل برنامه تا پایان یافتن بررسی تمامی صد هزار عنصر در داخل حلقه خواهد ماند. این بررسی کاملا بیهوده بوده و موجب اتلاف وقت خواهد شد. این مشکل را میتوان با دستور break حل کرد:
int search(int[] arr, int n, int x){
int i, result = -1;
for(i = 0 ; i < n ; i++){
if(arr[i] == x){
result = i;
break;
}
}
return result;
}
تفاوت این تابع با تابع قبلی تنها در دستور break است. با استفاده از این دستور، هرگاه شرط arr[i] == x صحیح باشد، پس از تخصیص مقدار i به result، دستور break اجرا میشود. اجرای این دستور موجب میشود که کنترل برنامه از حلقه خارج شده و به اولین خط بعد از آن منتقل شود. بنابراین اگر عنصر مورد نظر ما در خانه شماره دو قرار داشته باشد، دستورات داخل حلقه تنها سه بار (شمارهها از صفر شروع میشوند) تکرار میشوند.
دستور continue
[برگرد بالا]
این دستور برای ادامه کار حلقه از تکرار بعدی آن استفاده میشود. به عبارت دیگر، زمانی که کنترل برنامه در داخل حلقه به دستور continue میرسد، از تمامی دستورات بعدی حلقه تا انتهای آن صرف نظر شده و به شروع تکرار بعدی حلقه میرسد. اگر حلقه مورد نظر حلقه while یا do-while باشد، شرط ادامه حلقه بررسی میشود؛ اما اگر حلقه for باشد، ابتدا بخش نمو اجرا شده و سپس شرط حلقه بررسی میشود. به مثال ساده زیر توجه کنید:
int i, s = 0, p = 1;
for(i = 1 ; i <= 10 ; i++){
if(i % 2 == 0){
continue;
}
s += i;
p *= i;
}
در این حلقه اگر i عدد زوجی باشد، دستور continue اجرا شده و کنترل اجرای برنامه به ابتدای تکرار بعدی میرود. در نتیجه به مقدار i یک واحد افزوده شده و سپس شرط ادامه حلقه بررسی میشود.
همانطور که از تعریف این دستور پیدا است، کاربرد آن زمانی است که قصد داریم در شرایط خاصی قسمتی از دستورات انتهایی حلقه اجرا نشود.
حلقههایی با شرط همواره صحیح
[برگرد بالا]
تمام حلقههای تکرار برای ادامه کار خود شرطی را بررسی میکنند. این شرط میتواند یک عبارت مقایسهای ساده، ترکیب عبارات محاسباتی یا هر ساختار دیگری باشد. اگر این شرط به گونهای باشد که همیشه صحیح باشد، حلقههایی با شرط همواره صحیح به وجود میآیند. در این حلقهها شرط ادامه حلقه همواره صحیح است. پس در حالت عادی حلقه هرگز خاتمه پیدا نمیکند! چنین حلقههایی اگر به درستی کنترل نشوند به یک حلقه بدون توقف تبدیل میشوند که در اصطلاح به آنها حلقه بینهایت گفته میشود. حلقههای بینهایت گاهی در اثر اشتباهات منطقی در پیادهسازی الگوریتم بروز میدهند و باعث شکست اجرای صحیج برنامه میشوند. اما اگر در داخل حلقه و در جای مناسب از دستور break استفاده شود، چنین حلقههایی نیز پایانپذیر خواهند بود:
int n;
while(1){ // while(true)
cout << "Enter a positive number:";
cin >> n;
if(n > 0){
break;
}
}
شرط اجرای این حلقه عدد یک است. در زبان ++C هر عدد غیر صفر (صحیح یا اعشاری) معنی درست و عدد صفر معنی نادرست میدهد. پس حلقه فوق یک حلقه با شرط صحیح است. در داخل حلقه از کاربر عدد مثبتی درخواست میشود. اگر کاربر صفر یا یک عدد منفی وارد کند شرط n > 0 صحیح نبوده و کنترل برنامه مجددا به ابتدای حلقه منتقل خواهد شد، تا عدد دیگری از کاربر دریافت کند. اما اگر شرط n > 0 صحیح باشد، دستور break اجرا شده و کنترل برنامه از حلقه خارج خواهد شد. یعنی تکرار حلقه تا زمانی ادامه پیدا میکند که کاربر عدد مثبتی وارد نکرده است. به محض اینکه اولین عدد مثبت وارد شد، کنترل برنامه نیز از حلقه خارج میشود. البته در این مثال خاص میتوان حلقه فوق را به صورت زیر نیز پیادهسازی کرد:
int n;
do{
cout << "Enter a positive number:";
cin >> n;
}while(n <= 0);
اگر برای حلقه for شرطی را قائل نشویم، به مفهوم شرط همواره درست است:
int n;
for (;;){
cout << "Enter a positive number:";
cin >> n;
if(n > 0){
break;
}
}
توجه: علامتهای سمیکالن (;) همیشه باید نوشته شوند.
حلقههای تو در تو
[برگرد بالا]
در زبان برنامهنویسی ++C امکان استفاده از حلقههای تکرار تو در تو نیز وجود دارد. قطعه کد زیر جدول ضرب اعداد 4 تا 9 را در خروجی چاپ میکند:
int i, j;
for(i = 4 ; i < 10 ; i++){
for(j = 4 ; j < 10 ; j++){
cout << i * j << " ";
}
cout << endl;
}
حلقه داخلی وظیفه چاپ اعداد هر سطر را دارد. پس از اتمام این حلقه، دستوری اجرا میشود که مکاننما را به ابتدای سطر بعدی میبرد. پس در هر سطر یک ردیف از جدول ضرب چاپ میشود.
در پایان به دو نکته توجه داشته باشید:
1- در این ساختارها اگر مجموعه دستورات داخل حلقه تنها شامل یک دستور باشد، نیازی به استفاده از آکولاد برای مشخص کردن بلوک مجموعه دستورات داخل حلقه وجود ندارد.
2- عبارتهای دستوری break ،while ،do ،for و continue جزو کلمات کلیدی زبان برنامهنویسی ++C هستند.