Menu

לולאת for ב-C: תחביר, דוגמאות וטעויות נפוצות

איך חוזרים על קוד עם לולאת for ב-C: הכותרת בת שלושת החלקים, ספירה למעלה ולמטה, מעבר על מערכים עם טריק ה-sizeof, לולאות מקוננות, לולאות אינסופיות, והבאגים של טעות באחד ומשתנים unsigned שתופסים את כולם.

בדף הזה יש עורכים שאפשר להריץ - לערוך, להריץ ולראות את הפלט מיד.

למה לולאת for

if ו-switch בוחרים ענף ומריצים אותו פעם אחת. תוכניות אמיתיות צריכות לעשות דברים שוב ושוב: להדפיס כל ציון, לסכום רשימת מספרים, לצייר עשר שורות של רשת, לנסות כל תו במחרוזת. לולאת for היא סוס העבודה של C לחזרה על קוד מספר ידוע של פעמים, עם מונה שאתם שולטים בו.

כל מה שלולאת for צריכה יושב בכותרת קומפקטית אחת, כך ש"כמה פעמים, ואיך" גלוי בשורה אחת.

הכותרת בת שלושת החלקים

לכותרת של for יש שלושה חלקים שמופרדים בנקודה ופסיק: מאתחל, תנאי ועדכון.

for (initializer; condition; update) {
    // body - runs while the condition is nonzero
}

הם רצים בסדר מסוים, והיכרות איתו מסבירה כל לולאת for שתקראו אי פעם:

  1. המאתחל רץ פעם אחת, לפני כל דבר אחר.
  2. התנאי נבדק. אם הוא אפס, הלולאה מסתיימת מיד.
  3. הגוף רץ.
  4. העדכון רץ.
  5. חזרה לשלב 2.

עקבו אחרי הלולאה שלמעלה עם הרשימה הזו: int i = 0 רץ פעם אחת. 0 < 5 מתקיים, אז הגוף מדפיס i = 0, ואז i++ הופך את i ל-1. התנאי נבדק שוב, וכן הלאה. כש-i מגיע ל-5 הבדיקה נכשלת, מדלגים על הגוף, ו-done מודפס. הגוף רץ בדיוק חמש פעמים, כש-i מקבל את הערכים 0 עד 4.

שתי השלכות נובעות מזה ישירות. התנאי נבדק לפני הסיבוב הראשון, כך שלולאה שהתנאי שלה מתחיל כשקר רצה אפס פעמים: for (int i = 10; i < 5; i++) אף פעם לא נכנסת לגוף. והעדכון רץ אחרי הגוף, כך ש-i עדיין מחזיק את הערך הישן לאורך כל הגוף.

הצהרה על המונה בתוך הכותרת (int i = 0) חוקית מאז C99 והיא ברירת המחדל הנכונה: i קיים אז רק בתוך הלולאה, כך שהוא לא יכול להתנגש בשום דבר שאחריה, וכל לולאה בפונקציה יכולה להשתמש שוב באותו שם בחופשיות.

ספירה למעלה, למטה ובקפיצות

העדכון לא מוגבל ל-i++. אפשר לספור למטה, לקפוץ בכל גודל או להכפיל בכל סיבוב:

התאימו את התנאי לעדכון. ספירה למעלה הולכת עם < או <=; ספירה למטה הולכת עם > או >=. טעות בזיווג הזה היא הדרך לכתוב לולאה שלעולם לא נגמרת.

שימו לב שהלולאה השנייה משתמשת ב-<= 10 כי 10 הוא ערך שרוצים לכלול, בעוד שהדוגמה הראשונה השתמשה ב-< 5 כי 5 לא. הבחירה הזו היא המקום שבו נולדים באגים של טעות באחד, אז עשו אותה במודע: i < n רצה n פעמים החל מ-0; i <= n רצה n + 1 פעמים.

מעבר על מערך

השימוש הנפוץ ביותר בלולאת ספירה הוא מעבר על מערך לפי אינדקס. המונה משמש גם כמיקום שקוראים ממנו.

שני דברים לקחת מהדוגמה הזו.

טריק ה-sizeof. sizeof(scores) הוא הגודל של כל המערך בבתים, ו-sizeof(scores[0]) הוא הגודל של איבר אחד, כך שהמנה שלהם היא מספר האיברים. זה מתעדכן אוטומטית כשמוסיפים איבר, משהו ש-5 קבוע בקוד לא עושה. המלכודת, והיא גדולה, היא שזה עובד רק במקום שבו המערך עצמו נמצא בטווח. העבירו את המערך לפונקציה והוא הופך למצביע, כך ש-sizeof שם נותן את הגודל של מצביע במקום. בתוך פונקציה, תמיד קבלו את האורך כפרמטר נפרד.

התנאי הוא i < n, אף פעם לא i <= n. למערך של חמישה איברים יש אינדקסים חוקיים 0 עד 4. קריאת scores[5] היא התנהגות לא מוגדרת: היא עלולה להדפיס זבל, לקרוס, או להיראות תקינה בזמן שהיא משחיתה בשקט משהו אחר. C לא בודקת גבולות בכלל, אז זה לגמרי באחריותכם.

מעבר על מערך מהסוף להתחלה הוא אותו רעיון בכיוון ההפוך:

מתחילים ב-n - 1 (האינדקס החוקי האחרון), עוצרים ב-0 כולל, ולכן >= ולא >. השאירו את i כ-int עם סימן כאן; הסעיף הבא מסביר למה.

לולאות מקוננות

שימו for אחת בתוך אחרת כדי לעבוד עם רשתות, טבלאות או כל זוג של פריטים. הלולאה הפנימית רצה עד הסוף עבור כל צעד יחיד של הלולאה החיצונית.

הלולאה החיצונית קובעת row; הלולאה הפנימית עוברת על כל col עבור השורה הזו; ה-printf("\n") שאחרי הלולאה הפנימית מסיים את השורה. העבירו את השורה החדשה הזו לתוך הלולאה הפנימית וכל הטבלה הופכת לעמודה אחת, וזה ניסוי שימושי, כי הוא הופך את מבנה הקינון למוחשי.

תנו למונים שמות שונים. row/col או i/j בסדר; שימוש חוזר ב-i לשניהם מסתיר את המונה החיצוני ומייצר תוצאות מבלבלות. ושימו לב לעלות: לולאה של n בתוך לולאה של n מריצה את הגוף n * n פעמים, כך שזוג לולאות של 1,000 איטרציות הן מיליון סיבובים.

הנה לולאה מקוננת שעושה עבודה אמיתית: משולש של כוכביות, שבו הגבול של הלולאה הפנימית תלוי במונה החיצוני:

break ו-continue

שתי מילות מפתח משנות את הזרימה באמצע הלולאה. break יוצאת מהלולאה מיד; continue מדלגת על שאר הסיבוב הנוכחי וקופצת לעדכון.

הלולאה הראשונה עוצרת ברגע שהיא מוצאת את 7 ואף פעם לא בודקת את 93 הערכים הנותרים. השנייה משתמשת ב-continue כדי לדלג על ההדפסה של מספרים זוגיים; ה-i++ שבכותרת עדיין רץ, כך שהלולאה ממשיכה להתקדם. יש הרבה יותר על שתיהן בעמוד על break ו-continue, כולל איך יוצאים מלולאה מקוננת, משהו ש-break יחידה לא יכולה לעשות.

לולאות אינסופיות וחלקים ריקים

כל שלושת החלקים של הכותרת אופציונליים. השמיטו את התנאי והוא נחשב אמת לתמיד:

for (;;) {
    /* runs forever - exit with break or return */
}

for (;;) היא הלולאה האינסופית המקובלת ב-C, וסימני הנקודה ופסיק עדיין נדרשים. זו לא טעות כל עוד משהו בפנים יכול לעשות break, return או exit: לולאות אירועים ולולאות של "תמשיך לשאול עד שהקלט תקין" נכתבות בדיוק כך.

אפשר להשמיט גם חלקים אחרים. אם המונה כבר קיים, דלגו על המאתחל; אם הגוף מקדם אותו, דלגו על העדכון:

int i = 0;
for (; i < n; ) {
    /* ... */
    i += step;
}

זה חוקי אבל נקרא גרוע יותר מלולאת ה-while שזה בעצם. העדיפו for כשהמונה, הגבול שלו והצעד שלו שייכים יחד לכותרת, ולולאת while כשלא.

אופרטור הפסיק בכותרת

המאתחל והעדכון יכולים כל אחד להכיל כמה ביטויים שמופרדים בפסיקים, וכך מריצים שני מונים בבת אחת:

int i = 0, j = len - 1 מצהיר על שני המונים, ו-i++, j-- מקדם את שניהם. זה אופרטור הפסיק, שמחשב את האופרנדים שלו לפי הסדר. השתמשו בו למונים שבאמת מקבילים; דחיסה של עבודה לא קשורה לכותרת רק מסתירה אותה.

מלכודות נפוצות

קומץ מלכודות אחראי לרוב הבאגים של לולאות for ב-C.

טעות באחד. i <= n על מערך שמתחיל מאפס קורא איבר אחד מעבר לסוף. השתמשו ב-i < n עבור "n פעמים החל מ-0".

נקודה ופסיק אחרי הכותרת. זה מתקמפל וכמעט אף פעם אינו מה שהתכוונתם:

/* BUG: the loop body is the empty statement; the printf runs once */
for (int i = 0; i < 5; i++);
{
    printf("%d\n", i);
}

ה-; הופך לגוף, כך שהלולאה מסתובבת חמש פעמים בלי לעשות כלום והסוגריים המסולסלים שמתחת רצים פעם אחת כבלוק רגיל. gcc -Wall מזהיר על זה.

מונים unsigned שסופרים למטה. זו לולאה אינסופית:

/* BUG: an unsigned value is never < 0 */
for (size_t i = n - 1; i >= 0; i--) {
    process(a[i]);
}

i >= 0 תמיד אמת לטיפוס בלי סימן. כש-i מגיע ל-0, i-- מתגלגל למספר חיובי ענק והלולאה ממשיכה לתוך זיכרון שאינו שלה. השתמשו ב-int עם סימן לספירה למטה, או כתבו את התנאי כ-for (size_t i = n; i-- > 0; ), שמקטין ובודק בצעד אחד ועוצר נכון באפס.

שינוי המונה בשני מקומות. שינוי של i בתוך הגוף וגם בכותרת הופך את מספר האיטרציות לבלתי צפוי. בחרו מקום אחד.

מונים של נקודה צפה. for (double x = 0.0; x != 1.0; x += 0.1) עלולה לעולם לא להסתיים, כי ל-0.1 אין ייצוג בינארי מדויק והסכום עובר את 1.0 בלי לפגוע בו. עברו בלולאה עם ספירה שלמה וחשבו את הערך בפנים:

for (int i = 0; i <= 10; i++) {
    double x = i / 10.0;
    /* ... */
}

שינוי אורך המערך תוך כדי מעבר עליו. התנאי i < n קורא את n מחדש בכל סיבוב, כך שאם הגוף מקטין את האוסף, התאימו את האינדקס במכוון במקום לתת לשניהם להתרחק זה מזה.

שאלות נפוצות

איך כותבים לולאת for ב-C?

שמים בכותרת שלושה חלקים שמופרדים בנקודה ופסיק, מאתחל, תנאי ועדכון: for (int i = 0; i < 5; i++) { printf("%d\n", i); }. זה מריץ את הגוף חמש פעמים כש-i מקבל את הערכים 0 עד 4, ועוצר ברגע שהתנאי שקרי.

איך עוברים על מערך בלולאה ב-C?

סופרים אינדקס מ-0 ועד האורך (לא כולל): for (int i = 0; i < n; i++) { sum += a[i]; }. עבור מערך שהוצהר באותה פונקציה אפשר לחשב את האורך עם sizeof(a) / sizeof(a[0]); ברגע שהמערך הועבר לפונקציה הטריק הזה כבר לא עובד, אז העבירו את האורך כפרמטר.

למה לולאת ה-for שלי ב-C רצה פעם אחת יותר מדי?

זה הבאג של טעות באחד (off-by-one). עם מערך בגודל n שמתחיל מאפס, האינדקסים החוקיים הם 0 עד n - 1, כך ש-i <= n מריץ איטרציה נוספת וקורא מעבר לסוף, וזו התנהגות לא מוגדרת. ברירת המחדל הבטוחה היא i < n.

אפשר להצהיר על משתנה הלולאה בתוך לולאת for ב-C?

כן, מאז C99: for (int i = 0; i < n; i++). המשתנה קיים אז רק בתוך הלולאה, וזה מה שרוצים. קמפלו עם gcc -std=c99 או גרסה מאוחרת יותר (GCC ו-clang מודרניים משתמשים ב-C17 כברירת מחדל, כך שזה פשוט עובד); הסגנון הישן של C89 הצהיר על i לפני הלולאה.

איור של שפות התכנות ב-Coddy

ללמוד תכנות עם Coddy

להתחיל