کدلپر - مرجع جامع آموزش برنامه‌نویسی

All Right Reserved © 2025 Codoloper

background codoloper

حلقه ها - While و For

خانه

ما پیش‌تر با حلقه‌های while و for آشنا شده‌ایم.

در حالت

 
while (expression)
    statement

عبارت (expression) ارزیابی می‌شود. اگر مقدار آن ناصفر باشد، دستور (statement) اجرا می‌شود و سپس عبارت دوباره ارزیابی می‌شود.
این چرخه تا زمانی ادامه دارد که مقدار عبارت صفر شود، و در آن لحظه اجرای برنامه از بعد از دستور ادامه پیدا می‌کند.

دستور for به شکل زیر:

 
for (expr1; expr2; expr3)
    statement

معادل است با:

 
expr1;
while (expr2) {
    statement
    expr3;
}

به‌جز در مورد رفتار continue که تو درس 7 همین قسمت (Break و Continue) توضیح داده شده است.

از نظر دستوری، هر سه بخش یک حلقه for عبارت‌هایی هستند. به طور معمول، expr1 و expr3 دستورات انتساب (assignment) یا فراخوانی تابع هستند و expr2 یک عبارت مقایسه‌ای (relational expression) است.
هر یک از سه بخش می‌توانند حذف شوند، اما علامت‌های سمی‌کالن (;) باید باقی بمانند.
اگر expr1 یا expr3 حذف شوند، فقط از گسترش کد کنار گذاشته می‌شوند.
اگر بخش بررسی (expr2) وجود نداشته باشد، همیشه درست در نظر گرفته می‌شود، بنابراین:

 
for (;;) {
    ...
}

یک حلقه‌ی «بی‌نهایت» است که معمولاً با دستورهایی مثل break یا return متوقف می‌شود.

انتخاب بین while یا for معمولاً به سلیقه‌ی شخصی بستگی دارد.
مثلاً در حالت زیر:

 
while ((c = getchar()) == ' ' || c == '\n' || c == '\t')
    ; /* پرش از کاراکترهای فاصله‌دار */

چون مقداردهی اولیه یا مجدد وجود ندارد، while طبیعی‌تر است.

اما وقتی مقداردهی اولیه و افزایش ساده‌ای وجود دارد، for مناسب‌تر است، چون کنترل حلقه در یک‌جا و در بالای حلقه نگه داشته می‌شود.
مثلاً:

 
for (i = 0; i < n; i++)
    ...

این روش استاندارد C برای پردازش n عنصر اول یک آرایه است — مشابه حلقه DO در Fortran یا for در Pascal.
با این حال، این تشابه کامل نیست چون متغیر شاخص (index variable) یعنی i بعد از اتمام حلقه مقدار خود را حفظ می‌کند.

از آنجا که اجزای حلقه for عبارت‌های دلخواه هستند، حلقه‌های for محدود به پیشرفت‌های عددی نیستند.
با این حال، استفاده از آن برای محاسبات نامرتبط در بخش مقداردهی اولیه و افزایش، سبک برنامه‌نویسی ضعیفی است.

به عنوان مثالی بزرگ‌تر، در زیر نسخه‌ی دیگری از تابع atoi را می‌بینید که رشته‌ای را به عدد صحیح تبدیل می‌کند.
این نسخه کمی کلی‌تر از نسخه‌ی فصل ۲ است؛ چون فضای خالی اولیه و علامت‌های + یا - اختیاری را نیز مدیریت می‌کند.
(در فصل ۴، تابع atof همین تبدیل را برای اعداد اعشاری انجام می‌دهد.)

ساختار برنامه شکل ورودی را بازتاب می‌دهد:

  1. پرش از فاصله‌های خالی (در صورت وجود)

  2. گرفتن علامت (در صورت وجود)

  3. گرفتن بخش عددی و تبدیل آن

هر مرحله کار خودش را انجام می‌دهد و وضعیت را برای مرحله‌ی بعد آماده می‌گذارد.
کل فرایند با رسیدن به اولین کاراکتری که نمی‌تواند بخشی از عدد باشد، متوقف می‌شود.

 
#include <ctype.h>
/* atoi: convert s to integer; version 2 */
int atoi(char s[])
{
    int i, n, sign;
    for (i = 0; isspace(s[i]); i++) /* پرش از فاصله‌ها */
        ;
    sign = (s[i] == '-') ? -1 : 1;
    if (s[i] == '+' || s[i] == '-') /* پرش از علامت */
        i++;
    for (n = 0; isdigit(s[i]); i++)
        n = 10 * n + (s[i] - '0');
    return sign * n;
}

کتابخانه استاندارد تابع کامل‌تری به نام strtol برای تبدیل رشته‌ها به اعداد صحیح بلند (long) فراهم کرده است؛ به بخش ۵ پیوست B مراجعه کنید.

مزیت نگه‌داشتن کنترل حلقه در یک‌جا زمانی که چند حلقه تودرتو داریم حتی آشکارتر می‌شود.
تابع زیر یک مرتب‌سازی شِلی (Shell sort) برای مرتب کردن آرایه‌ای از اعداد صحیح است.

ایده‌ی اصلی این الگوریتم (ابداع D. L. Shell در سال ۱۹۵۹) این است که در مراحل اولیه، عناصر با فاصله زیاد با هم مقایسه می‌شوند — نه عناصر مجاور مثل روش‌های ساده‌تر.
این باعث می‌شود بخش بزرگی از بی‌نظمی به‌سرعت از بین برود، بنابراین مراحل بعدی کار کمتری دارند.
فاصله بین عناصر مقایسه‌شده به‌تدریج کاهش می‌یابد تا به ۱ برسد، جایی که الگوریتم عملاً به روش مقایسه‌ی مجاور تبدیل می‌شود.

 
/* shellsort: sort v[0]...v[n-1] into increasing order */
void shellsort(int v[], int n)
{
    int gap, i, j, temp;
    for (gap = n/2; gap > 0; gap /= 2)
        for (i = gap; i < n; i++)
            for (j = i - gap; j >= 0 && v[j] > v[j + gap]; j -= gap) {
                temp = v[j];
                v[j] = v[j + gap];
                v[j + gap] = temp;
            }
}

در این کد سه حلقه‌ی تو در تو وجود دارد:

  • بیرونی‌ترین حلقه فاصله بین عناصر مقایسه‌شده را کنترل می‌کند و در هر بار اجرای کامل، آن را نصف می‌کند تا صفر شود.

  • حلقه‌ی میانی بین عناصر حرکت می‌کند.

  • حلقه‌ی درونی هر جفت عنصر با فاصله‌ی مشخص را مقایسه کرده و در صورت نادرست بودن ترتیب، آن‌ها را جابه‌جا می‌کند.

از آن‌جایی که در نهایت فاصله به ۱ کاهش می‌یابد، تمام عناصر به درستی مرتب می‌شوند.
توجه کنید که انعطاف‌پذیری دستور for باعث شده حلقه‌ی بیرونی نیز در همان قالب بگنجد، حتی اگر دنباله‌اش حسابی نباشد.

آخرین عملگر C که باید یاد بگیریم، کاما (,) است که معمولاً در دستور for به کار می‌رود.
یک جفت عبارت جداشده با کاما از چپ به راست ارزیابی می‌شود، و نوع و مقدار عبارت نهایی برابر با نوع و مقدار عبارت سمت راست است.
بنابراین در دستور for می‌توان چند عبارت را در بخش‌های مختلف قرار داد؛ مثلاً برای پردازش دو شاخص به طور هم‌زمان.

این در تابع reverse(s) نشان داده شده که رشته‌ای را در همان‌جا معکوس می‌کند:

 
#include <string.h>
/* reverse: reverse string s in place */
void reverse(char s[])
{
    int c, i, j;
    for (i = 0, j = strlen(s) - 1; i < j; i++, j--) {
        c = s[i];
        s[i] = s[j];
        s[j] = c;
    }
}

کاماهایی که برای جدا کردن آرگومان‌های تابع یا متغیرها در اعلان‌ها استفاده می‌شوند، عملگر کاما نیستند و ترتیب ارزیابی چپ به راست را تضمین نمی‌کنند.

عملگر کاما باید با احتیاط استفاده شود. مناسب‌ترین کاربرد آن در ساختارهایی است که ارتباط نزدیکی با هم دارند — مانند حلقه‌ی for در تابع reverse — یا در ماکروها (macros) که چند مرحله باید در یک عبارت واحد انجام شوند.
همچنین در تبادل عناصر در تابع reverse نیز می‌توان از آن استفاده کرد، چون می‌توان این عملیات را به عنوان یک عمل واحد در نظر گرفت:

 
for (i = 0, j = strlen(s) - 1; i < j; i++, j--)
    c = s[i], s[i] = s[j], s[j] = c;

تمرین 3-3:
تابعی بنویسید به نام expand(s1, s2) که یادداشت‌های کوتاه‌شده مانند a-z را در رشته‌ی s1 به فهرست کامل abc...xyz در s2 گسترش دهد.
تابع باید حروف کوچک و بزرگ و اعداد را نیز پشتیبانی کند و مواردی مانند a-b-c، a-z0-9 و -a-z را مدیریت کند.
ترتیب دهید که اگر - در ابتدا یا انتهای رشته بود، به‌صورت معمولی (نه محدوده) در نظر گرفته شود.