C++/לולאות

מתוך ויקיספר, אוסף הספרים והמדריכים החופשי
< C++

כיצד נבצע את אותה הפעולה 10,000 פעמים? כיצד נבצע את אותה הפעולה מספר-לא-ידוע-מראש של פעמים? באמצעות התנאים התוכניות שלנו קיבלו יכולת להחליט על אופן פעולתן, אבל הן אף פעם לא חזרו לאותה שורה פעם שנייה. אם יכולנו להדפיס 10,000 פעמים את המחרוזת "Hello, world!\n" באמצעות שכפול פקודת ההדפסה 10,000 פעמים (דבר לא הגיוני בעליל), אז בכל אופן לא יכולנו לקלוט מהמשתמש מספר ולהדפיס את המחרוזת הנ"ל מספר זה של פעמים, לפי רצונו של המשתמש.

לולאות באות לסייע לנו בכתיבת קטעי קוד מהסוג הזה. לולאות הן "חזרות" על אותו קטע קוד כל עוד תנאי מסויים מתקיים. לקטע קוד זה קוראים גוף הלולאה. ב-C++‎ קיימים שלושה סוגים של לולאות: while‏, do while ו-for. כל לולאה שנרצה לכתוב ניתן לבטא באמצעות כל אחת משלושת האפשרויות הללו. ההבדל בינהן לא משמעותי, הרי כולן מבצעות את אותו קטע קוד כל עוד התנאי שכתבנו מתקיים, בכל זאת עבור כל מקרה פרטי נבחר את הלולאה המתאימה ביותר שתקצר את הקוד ותעשה אותו לברור ביותר.

לולאת while[עריכה]

while(expression)
    // code

קרי: "כל עוד ... בצע ...".

לולאת while תבצע את אותו קטע קוד כל עוד הביטוי שכתבנו (expression) מקבל את הערך true בהמרה לטיפוס בוליאני bool. כאשר התוכנית תגיע לתחילת הלולאה, היא תבדוק תחילה את התנאי. אם התנאי שקרי התוכנית לא תבצע את הקוד שבלולאה אף לא פעם אחת. אם התנאי אמיתי אז התוכנית תבצע את הבלוק שבא לאחר הלולאה, ותחזור בסופו לשורה הראשונה בה נבדק התנאי. ניתן לראות בלולאה זו את משפט ה-if שחוזר על עצמו. בזהה למשפטי התנאי, כדי לבצע יותר מפקודה אחת בגוף הלולאה, יש לתחום אותן בסוגריים מסולסלים.

להלן דוגמה שקולטת מהמשתמש מספרים ומחשבת את הסכום שלהם. הלולאה תפסק כאשר המשתמש יזין את המספר 0, ואז יודפס הסכום של המספרים. קטע זה מניח את תקינות הקלט.

int num, sum = 0;
cin >> num;

while(num)
{
    sum += num;
    cin >> num;
}

cout << "sum = " << sum << endl;

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

כאשר אנחנו כותבים while(num) המהדר ממיר את הערך של הביטוי שכתבנו לטיפוס בוליאני כדי לבדוק האם הוא אמיתי או שקרי. כנאמר בפרקים הקודמים, מספר שלם מומר ל-true כאשר הוא שונה מ-0 ול-false כאשר הוא שווה ל-0. אם לקחת את זה בחשבון ניתן לראות שהתנאי של הלולאה שכתבנו הוא בעצם גרסה מקוצרת של while(num != 0), או במילים: "כל עוד num שונה מ-0 בצע ...". כתוצאה קיבלנו תוכנית שתבצע את הקוד שבין הסוגריים המסולסלים אם ורק אם המספר הראשון שהקליד המשתמש שונה מ-0. אם המשתמש מקליד 0 כמספר ראשון, אזי התוכנית תדלג על הלולאה אל השורה האחרונה וישר תדפיס את הסכום, שאין פלא - הוא אכן אפס.

נברר מה יקרה אם המספר הראשון שנקלט שונה מ-0, למשל המשתמש יקליד 7. במקרה כזה תוצאת הביטוי שבתנאי הלולאה תהיה true, כי num שונה מאפס, והתוכנית תכנס לתוך הלולאה. הפקודה הראשונה שבגוף הלולאה תוסיף את הערך הנקלט השמור במשתנה num לתוך הצובר sum. בדוגמה שלנו, הצובר הזה יקבל את הערך 7. לאחר שעיבדנו את המספר הראשון שהמשתמש הקליד אין לנו יותר צורך בו, וכעת עלינו לעבור למספר הבא: cin >> num; (בשורה השנייה של גוף הלולאה) יקלוט את המספר הבא (נגיד 4). עתה התוכנית תקפוץ לתחילת הלולאה ושוב תבדוק את התנאי, כיוון ש-4 מומר ל-true התוכנית שוב תבצע את גוף הלולאה והצובר יוגדל לערך ‎7+4=11‎. כך התוכנית תחזור חלילה עד שמספר 0 יקלט. שימו לב שקטע קוד זה יעבוד גם עם מספרים שליליים, לכן עבור הקלט:

7 4 -20 51 0

יהיה הפלט "sum = 42‎".

לולאת do while[עריכה]

לולאת do while היא לולאת while שהתנאי הנבדק הוא בסוף הלולאה ולא בתחילתה:

do
    // code
while(expression);

בניגוד ללולאת while, גוף לולאה זו יתבצע לפחות פעם אחת. התוכנית תכנס לתוך הלולאה לפני שתבדוק את התנאי. התנאי יקבע האם התוכנית תחזור על הקוד שבלולאה בפעם השנייה, השלישית וכו'... כאשר תכתבו לולאת while שלפניה בא קוד שדומה לקוד שכתבתם בתוף גוף הלולאה, תשקלו לשנות את הלולאה ל-do while. לדוגמה, נוכל לשנות את הקוד שכתבנו למעלה ללולאת do while:

int num, sum = 0;

do {
    cin >> num;
    sum += num;
} while(num);
 
cout << "sum = " << sum << endl;

בדוגמה פרטית הזו לא הייתה לנו בעייה לעשות שינוי זה. הסיבה לכך שהפקודה הנוספת שהייתה לנו בתוך גוף הלולאה, היא sum += num;. כאשר המספר שנקלט הוא אפס, הוא לא משפיע על הסכום ולכן נוכל לבדוק את תנאי העצירה אחרי שכבר התווסף לסכום. במקרה שלנו, גם אם תנאי העצירה שונה, נוכל לשנות את סדר הפקודות כדי שהקוד יתבצע באופן תקין.

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

לולאת for[עריכה]

ניגש לבעיה שהצגנו בתחילת פרק זה. כיצד נדפיס 10000 פעמים את המחרוזת "Hello, world!\n"? נוכל לעשות את זה באמצעות לולאת while:

int i = 0; // אתחול
while(i < 10000) // תנאי
{
    cout << "Hello, world!\n";
    i++; // קידום מונה הלולאה
}

בתוכנית זו הצהרנו על משתנה בשם i ואיתחלנו אותו ל-0, כי ברגע ההתחלתי הדפסנו "אפס" פעמים. כל פעם שהתוכנית תדפיס את המחרוזת "Hello, world!/n" היא תגדיל את המונה (המשתנה i) ב-1, בצורה כזאת בכל רגע נתון המונה יכיל את מספר השורות שכבר הודפסו. הלולאה תיפסק כאשר מספר זה יגיע ל-10000, לכן היא תתבצעה 10000 פעמים עבור ערכים של i החל מ-0 ועד ל-9999 כולל (סך הכל 10000 פעמים). בפעם האחרונה כאשר i יקבל את הערך הסופי שלו 10000 הלולאה תיפסק.

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

נתבונן בלולאה for. לולאה זו מאפשרת לנו לפרק את הקוד לאותם 4 החלקים שדיברנו עליהם אך לארגן בצורה טיפה שונה:

for(initialization; condition; iteration)
    // code

בשורה הראשונה נמצאים שלושת החלקים האחראים על בקרת ביצוע הלולאה ואילו גוף הלולאה בא אחר-כך. ננתח את השורה הראשונה:

  • אתחול (initialization) – חלק זה הוא הוראה בפני עצמו. הוא הראשון שיתבצע כאשר התוכנית תגיע לשורת הלולאה והתוכנית לא תחזור עליו כשתחזור על ביצוע גוף הלולאה. נוכל להצהיר בחלק זה על משתנים מאותו טיפוס ולאתחלם לערכים כלשהם. טווח ההכרה של משתנים שיוצהרו כאן יהיה עד לסיום הלולאה, משמע הדבר שלא נוכל להשתמש בהם בהמשך הקוד מחוץ ללולאה. אם בכל זאת נרצה להשתמש באותו משתנה גם בתוך הלולאה וגם מחוץ לה, נצהיר עליו לפני הלולאה. ניתן לרשום במקום זה ביטוי כלשהו, למשל שיאתחל משתנה כזה שהוצהר לפני הלולאה. במקרה ונשאיר חלק זה ריק, התוכנית תתעלם ממנו.
  • תנאי (condition) – תפקידו כתפקיד התנאי בלולאת while רגילה. זהו ביטוי שיבוצע אחרי האתחול או הקידום ויושווה לערך true. אם בתחילת הלולאה ביטוי זה שקרי, אזי הלולאה לא תתבצע אף לא פעם אחת. אם נשאיר ביטוי זה ריק, התוכנית תתייחס אליו כלאמיתי, כאילו שנכתוב בו true. כתוצאה הלולאה לא תעצר מעצמה, נצטרך להשתמש בהוראת break, בהוראת return, בהוראת goto, באופרטור throw, בפונקציית exit או בפונקציית terminate על מנת להפסיק את הלולאה. במקרה הגרוע התוכנית תתקע. כמו כן לעיתים נכתבות תוכניות שלא אמורות לצאת מלולאה כזו כלל, במקרה כזה הלולאה היא אינסופית.
  • איטרציה (iteration) – כל פעם שהתוכנית תגיע לסוף גוף הלולאה ולפני שתבדוק את התנאי כדי להחליט האם להמשיך בביצוע הלולאה או לא, היא תבצע את הביטוי שנרשם פה. בדרך כלל נרשום פה הגדלה או הקטנה של מונה הלולאה, מעבר לחוליה הבאה ברשימה מקושרת וכד'... הערך הסופי של הביטוי הזה לא נבדק על ידי התוכנית ואין לו שום חשיבות. ביטוי הנרשם פה יתבצע פעם אחד פחות מהביטוי שבתנאי. כמו גם את שני החלקים הקודמים נוכל להשאיר גם את החלק הזה ריק.

כעת נשתמש בלולאת for כדי להדפיס 10000 פעמים את המחרוזת "Hello, world!\n":

for(int i = 0; i < 10000; i++)
    cout << "Hello, world!\n";

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

int count;
cin >> count;
for(int i = 0; i < count; i++)
    cout << "Hello, world!\n";

ניתן לקונן לולאות, כלומר לרשום לולאה אחת בתוך גוף הלולאה השנייה. שמות מקובלים למוני הלולאה הם i‏, j‏, k, כאשר משתמשים ב-i ללולאה החיצונית ביותר, ב-j למונה הלולאה הפנימית יותר וב-k ללולאה הפנימית ביותר. אם יצא לכם להשתמש ב-4 או יותר לולאות מקוננות, שקלו לפרק את הקוד לפונקציות או להשתמש ברקורסיה (למדו בהמשך). להלן דוגמה של לולאה מקוננת:

int height, width;
cin >> height >> width;

for(int i = 0; i < height; i++)
{
    for(int j = 0; j < width; j++)
        cout << '*';
    cout << endl;
}

הלולאה הפנימית, זו שנמצאת בתוך בלוק של הלולאה הראשונה, מדפיסה שורה של כוכביות, בהתאם לערך שבמשתנה width (רוחב). לאחר השורה הזו התוכנית יורדת שורה. כיוון שהקוד שמדפיס שורה ומוריד את הסמן לשורה חדשה נמצא בתוך גוף הלולאה החיצונית, הוא יתבצע מספר פעמים, בהתאם לתנאי העצירה של הלולאה החיצונית. הלולאה החיצונית תתבצעה מספר פעמים בהתאם למשתנה height (גובה) ולכן התוכנית תדפיס מספר שורות של כוכביות השווה ל-height. כתוצאה יופיע בפלט מלבן של כוכביות שרוחבו הוא width וגובהו הוא height שהם הערכים שנקלטו מהמשתמש. מנוסחת שטח המלבן נוכל לחשב שהמלבן יכלול height*width כוכביות בסך הכל, ומכאן נוכל להסיק שגוף הלולאה הפנימית התבצע בדיוק מספר זה של פעמים.

הוראת goto[עריכה]

שימו לב:

יש להמנע משימוש בהוראה זו. לרוב ניתן למצוא תחליף עדיף במקומה(לרוב אף אינה עובדת!!!).

את כל הלולאות שנלמדו למעלה ניתן לבטא באמצעות קפיצות וקפיצות מותנות. הוראת goto מאפשרת לגרום לתוכנית לקפוץ (לדלג) לשורה מסויימת. את השורה שאליה נרצה לגרום לתוכנית לעבור נסמן באמצעות תגית ואחרי המילה השמורה goto נכתוב את שם התגית שאליה אנחנו רוצים לעבור:

    cout << "Good ";
    goto printMorning;

    cout << "evening.\n";

printMorning:
    cout << "morning.\n";

בדוגמה הזו כאשר התוכנית תגיע לשורה goto printMorning היא תדלג לשורה שאחרי printMorning. כתוצאה השורה "Good evening.\n" אף פעם לא תודפס, במקומה תופיע השורה "Good morning.\n".

הוראה זו מאפשרת לעבור בתוך גבולות אותה פונקציה (למד בהמשך על פונקציות) מכל שורה שהיא לכל שורה אחרת. המגבלה היחידה היא האיסור לדלג על איתחול המשתנים. כדוגמה נכתוב את הדוגמה עם לולאת ה-do while שלמעלה באמצעות הוראת ה-goto:

    int num, sum = 0;
 
loopBegin:
    cin >> num;
    sum += num;
    if(num)
        goto loopBegin;
 
    cout << "sum = " << sum << endl;

הוראה זו מסרבלת את הקוד והופכת אותו לפחות ברור ואף לעיתים הוא הופך לגמרי לבלתי קריא. הסיבה לקיום הוראה זו בשפת C++‎ היא בעיקר היסטורית. כמו כן היא משמשת תוכנות אוטומטיות לכתיבת קוד או לביצוע אופטימיזציות. יש להמנע משימוש בהוראה זו בקוד שנכתב על ידי בן-אדם. מלבד מקרים נדירים למדי ניתן למצוא תחליף להוראה זו.

כעת נתבונן בשתי הוראות שהן תחליף להוראת goto במקרים מסויימים.

הוראת continue[עריכה]

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

for(int i = 0; i < 10; i++)
{
    double n;
    cin >> n;

    if(n >= 0)
    {
        /* ... */
    }
}

אך דבר זה עלול לגרום לקינון משפטי תנאי רבים אחד בתוך השני. למקרה כזה נוכל להשתמש בהוראה מיוחדת, הוראת continue. נוכל לכתוב הוראה זו בכל אחת מהלולאות (while, do while, for). כאשר התוכנית תגיע להוראת continue היא תדלג לסוף גוף הלולאה ותבדוק שוב את תנאי הלולאה, כאילו שנבצע קפיצת goto לשורה שלפני הסוגר של סוף בלוק הלולאה. במקרה ומדובר בלולאת for התוכנית תבצע גם את ביטוי האיטרציה. להלן הלולאה שלמעלה הכתובה באמצעות הוראת continue:

for(int i = 0; i < 10; i++)
{
    double n;
    cin >> n;

    if(n < 0)
        continue;

    /* ... */
}

בניגוד להוראת ה-goto, הוראת ה-continue ברורה יותר והשימוש בה מקובל.

הוראת break[עריכה]

לפעמים נרצה לסיים את ביצוע הלולאה לפני שהתנאי שלה יהפוך לשקרי. ניתן לעשות זאת באמצעות צירוף משתנה בוליאני נוסף done שתפקידו לשמור 'שקר' כל עוד ברצוננו להמשיך בביצוע רגיל של הלולאה ושיקבל 'אמת' כאשר נרצה לסיים את הלולאה. באמצעות הוראת continue נוכל לגרום לתוכנית לבדוק את המשתנה done ולסיים את הלולאה בכל שורה מתוך גוף הלולאה שנרצה:

bool done = false;
for(int i = 0; i < 10 && !done; i++)
{
    double n;
    cin >> n;

    if(n < 0)
    {
        done = true;
        continue;
    }

    /* ... */
}

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

for(int i = 0; i < 10; i++)
{
    double n;
    cin >> n;

    if(n < 0)
        break;

    /* ... */
}

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

בהוראת ה-break, כמו גם בהוראת ה-continue, נוכל להשתמש בכל שלושת הלולאות: while‏, do while ו-for. כמו כן שימוש נפוץ בהוראה זו הוא בלולאות אינסופיות. לעיתים נוח יותר לכתוב לולאה אינסופית (לולאת for ללא תנאי) ולצאת ממנה משורות שונות בגופה על ידי שימוש ב-break.

שימו לב שכאשר יש מספר לולאות מקוננות אז הוראת ה-break יוצאת מהלולאה הפנימית ביותר. בדוגמה הבאה יודפס 10 פעמים ערכו של i:

for(int i = 0; i < 10; i++)
{
    for(int j = 0; j < 10; j++)
        break;
    cout << "i = " << i << endl;
}

כדי לצאת מתוך מספר לולאות מקוננות ללא הוספת משתני עזר ניתן להשתמש בהוראת goto:

for(int i = 0; i < 10; i++)
{
    for(int j = 0; j < 10; j++)
        goto endOfLoops;
    cout << "i = " << i << endl;
}

endOfLoops:

במקרה זה ערכו של i יודפס אך ורק פעם אחת. אך כלל הזהב לגבי השימוש ב-goto עדיין תקף. יש להימנע משימוש ב-goto, ואכן יש פתרון יותר טוב. הדרך האליגנטית ביותר היא להכניס את הלולאות המקוננות לתוך פונקציה, אז נוכל לצאת מהן באמצעות הוראת return (למדו בהמשך).


הפרק הקודם:
תנאים
לולאות
תרגילים
הפרק הבא:
מערכים