[0]zero-index clytie0x/zero-index

4.1 while และ for

ถ้าโจทย์ให้พิมพ์เลข 1 ถึง 5 เขียน printf ห้าบรรทัดก็เสร็จ แต่ถ้าให้พิมพ์ 1 ถึง N โดย N มาจากข้อมูลเข้า และอาจใหญ่ถึงหนึ่งล้าน เขียนทีละบรรทัดไม่ได้แล้ว เราต้องสั่งให้คอมพิวเตอร์ทำคำสั่งชุดเดิมซ้ำหลายรอบ เรียกว่า วนซ้ำ (loop) หน้านี้รู้จักคำสั่งวนซ้ำทั้งสามแบบของภาษา C และวิธีเลือกใช้

while ทำซ้ำตราบที่เงื่อนไขยังจริง

while (เงื่อนไข) {
    คำสั่งที่ต้องการทำซ้ำ
}

while แปลว่า "ตราบใดที่" ก่อนเริ่มแต่ละรอบ โปรแกรมจะตรวจเงื่อนไขในวงเล็บแบบเดียวกับ if ในบทที่ 3 ถ้าจริงก็ทำคำสั่งในปีกกาหนึ่งรอบ แล้วกลับขึ้นไปตรวจใหม่ ถ้าเท็จก็ข้ามปีกกาไปทำบรรทัดถัดไปเลย คำสั่งที่อยู่ในปีกกาเรียกว่า ตัวลูป (loop body)

#include <stdio.h>

int main()
{
    int i = 1;
    while (i <= 5) {
        printf("%d\n", i);
        i++;
    }
    printf("done\n");
    return 0;
}
1
2
3
4
5
done

ไล่ทีละรอบด้วยตารางแบบนี้ เวลาลูปทำงานไม่ตรงที่คิด ให้เขียนตารางนี้บนกระดาษ จะเห็นจุดผิดเร็วกว่านั่งจ้องโค้ด

รอบค่า i ตอนตรวจi <= 5สิ่งที่เกิดขึ้น
11จริงพิมพ์ 1 แล้ว i เป็น 2
22จริงพิมพ์ 2 แล้ว i เป็น 3
33จริงพิมพ์ 3 แล้ว i เป็น 4
44จริงพิมพ์ 4 แล้ว i เป็น 5
55จริงพิมพ์ 5 แล้ว i เป็น 6
6เท็จออกจากลูป ไปพิมพ์ done

ลูปที่นับรอบแบบนี้มีสามส่วนเสมอ ขาดส่วนไหนไปลูปจะพัง

  1. ค่าเริ่มต้น int i = 1; ตั้งตัวนับก่อนเข้าลูป
  2. เงื่อนไข i <= 5 บอกว่าจะทำต่อไปถึงเมื่อไร
  3. การเปลี่ยนค่า i++; ทำให้ตัวนับเดินหน้า จนวันหนึ่งเงื่อนไขกลายเป็นเท็จ

ตัวนับรอบนิยมตั้งชื่อว่า i ซึ่งเป็นข้อยกเว้นของกฎตั้งชื่อให้อ่านรู้เรื่อง เพราะทุกคนในวงการรู้ว่า i คือตัวนับ ถ้าตัวนับมีความหมายชัด เช่นเป็นรอบที่วิ่ง ตั้งชื่อตามความหมายจะดีกว่า

คราวนี้รับจำนวนรอบจากข้อมูลเข้า น้องวิ่งรอบสนาม N รอบ ให้พิมพ์บันทึกทุกรอบ

#include <stdio.h>

int main()
{
    int n;
    scanf("%d", &n);
    int lap = 1;
    while (lap <= n) {
        printf("lap %d done\n", lap);
        lap++;
    }
    return 0;
}
3
lap 1 done
lap 2 done
lap 3 done

ลองป้อน 0 ดู เงื่อนไข 1 <= 0 เป็นเท็จตั้งแต่ครั้งแรก ตัวลูปจึงไม่ทำเลยสักรอบ และโปรแกรมไม่พิมพ์อะไรออกมา ซึ่งถูกต้องแล้ว วิ่ง 0 รอบก็ไม่มีบันทึก

for รวมสามส่วนไว้บรรทัดเดียว

ลูปนับรอบเจอบ่อยมาก ภาษา C จึงมี for ที่เอาค่าเริ่มต้น เงื่อนไข และการเปลี่ยนค่า มาไว้ในวงเล็บเดียว คั่นด้วย ; โปรแกรมนี้ทำงานเหมือนโปรแกรม count ข้างบนทุกขั้น

#include <stdio.h>

int main()
{
    for (int i = 1; i <= 5; i++) {
        printf("%d\n", i);
    }
    printf("done\n");
    return 0;
}
1
2
3
4
5
done
ส่วนในตัวอย่างทำเมื่อไร
ค่าเริ่มต้นint i = 1ครั้งเดียว ก่อนเริ่มลูป
เงื่อนไขi <= 5ก่อนเริ่มทุกรอบ เท็จเมื่อไรออกจากลูป
การเปลี่ยนค่าi++หลังทำตัวลูปจบแต่ละรอบ แล้วค่อยไปตรวจเงื่อนไขใหม่
int i = 1 i <= 5 printf i++ จริง เท็จ ออกจากลูป

จะเขียนลูปนับรอบด้วย while หรือ for ก็ได้ผลเหมือนกัน แต่ for ช่วยกันลืม เพราะทั้งสามส่วนอยู่ในที่เดียว มองบรรทัดเดียวก็รู้ว่าลูปนี้นับจากไหนถึงไหน

นับถอยหลังและนับข้าม

ส่วนเปลี่ยนค่าไม่จำเป็นต้องเป็น i++ เสมอ จะลดค่าหรือเพิ่มทีละหลายๆ ก็ได้ ขอแค่สุดท้ายเงื่อนไขกลายเป็นเท็จ

#include <stdio.h>

int main()
{
    for (int i = 5; i >= 1; i--) {
        printf("%d\n", i);
    }
    printf("go\n");
    for (int floor = 0; floor <= 20; floor += 5) {
        printf("floor %d\n", floor);
    }
    return 0;
}
5
4
3
2
1
go
floor 0
floor 5
floor 10
floor 15
floor 20

ลูปแรกนับถอยหลัง เริ่มที่ 5 ลดทีละ 1 จนเงื่อนไข i >= 1 เป็นเท็จตอน i เป็น 0 ลูปที่สองเหมือนลิฟต์ที่จอดทุก 5 ชั้น floor += 5 เพิ่มทีละ 5

พิมพ์ในบรรทัดเดียวคั่นด้วยช่องว่าง

โจทย์มักสั่งให้พิมพ์ตัวเลขหลายตัวในบรรทัดเดียว คั่นด้วยช่องว่างหนึ่งช่อง ถ้าเขียน printf("%d ", i) จะมีช่องว่างเกินท้ายบรรทัด ซึ่งมองไม่เห็นด้วยตา แต่ถ้า grader ของข้อนั้นเทียบทุกตัวอักษรก็ผิดได้ วิธีที่ปลอดภัยคือพิมพ์ช่องว่าง ก่อน ตัวเลข ยกเว้นตัวแรก

#include <stdio.h>

int main()
{
    int n;
    scanf("%d", &n);
    for (int i = 2; i <= n; i += 2) {
        if (i > 2) {
            printf(" ");
        }
        printf("%d", i);
    }
    printf("\n");
    return 0;
}
11
2 4 6 8 10

โปรแกรมนี้พิมพ์เลขคู่ตั้งแต่ 2 ถึง n ตัวเลขตัวแรกคือ 2 จึงพิมพ์ช่องว่างก่อนตัวเลขเฉพาะเมื่อ i > 2 ส่วน \n อยู่นอกลูป พิมพ์ครั้งเดียวหลังจบ

do while ทำก่อน ค่อยตรวจ

while กับ for ตรวจเงื่อนไขก่อนเริ่มรอบ ตัวลูปจึงอาจไม่ทำเลยสักรอบ แต่บางงานต้องทำอย่างน้อยหนึ่งครั้งก่อนถึงจะรู้ว่าควรทำต่อไหม เช่น รับคะแนนจากผู้ใช้ ต้องรับมาก่อนถึงจะตรวจได้ว่าคะแนนอยู่ระหว่าง 0 ถึง 100 หรือเปล่า ถ้าไม่อยู่ให้รับใหม่

#include <stdio.h>

int main()
{
    int score;
    int tries = 0;
    do {
        scanf("%d", &score);
        tries++;
    } while (score < 0 || score > 100);
    printf("score = %d after %d tries\n", score, tries);
    return 0;
}
-5 120 85
score = 85 after 3 tries
  • do แปลว่าทำ ตัวลูปทำหนึ่งรอบก่อน แล้วค่อยตรวจเงื่อนไขหลัง while จริงก็วนกลับไปทำใหม่ เท็จก็ออก
  • รอบแรกได้ -5 ซึ่งน้อยกว่า 0 จึงรับใหม่ รอบสองได้ 120 มากกว่า 100 รับใหม่อีก รอบสามได้ 85 เงื่อนไขเป็นเท็จ ออกจากลูป
  • ต้องมี ; หลังวงเล็บของ while ใน do while เสมอ ต่างจาก while ธรรมดาที่ห้ามใส่

เลือกแบบไหนดี

ทั้งสามแบบเขียนแทนกันได้หมด แต่เลือกให้ตรงกับงานแล้วโค้ดจะอ่านง่ายและผิดยากกว่า

ลักษณะงานใช้ตัวอย่าง
รู้จำนวนรอบก่อนเริ่มforรับคะแนน N คน พิมพ์ 1 ถึง N วาดรูปกว้าง N ช่อง
ไม่รู้จำนวนรอบ หยุดเมื่อเงื่อนไขบางอย่างเกิดขึ้นwhileหารเลขด้วย 10 ไปเรื่อยๆ จนเหลือ 0 รับข้อมูลจนหมด
ต้องทำอย่างน้อยหนึ่งครั้งก่อนจะรู้ว่าทำต่อไหมdo whileรับค่าจนกว่าจะถูกเงื่อนไข นับหลักของตัวเลขที่อาจเป็น 0

ในโจทย์แข่งขัน for ใช้บ่อยที่สุด รองลงมาคือ while ส่วน do while นานๆ ใช้ที ตัวอย่างนับหลักของตัวเลขจะได้เห็นในหน้า 4.5

ลูปไม่รู้จบ

ถ้าเงื่อนไขไม่มีวันเป็นเท็จ ลูปจะวนไปเรื่อยๆ ไม่จบ เรียกว่า ลูปไม่รู้จบ (infinite loop) สาเหตุที่เจอบ่อยที่สุดคือลืมเปลี่ยนค่าตัวนับ เช่นโปรแกรม forever.c ที่ตัดมาให้ดูเฉพาะส่วนใน main

int i = 1;
while (i <= 5) {
    printf("%d\n", i);
}

ไม่มี i++ ค่า i จึงเป็น 1 ตลอดไป เงื่อนไขจริงทุกรอบ อาการที่เห็นมีสองแบบ

  • ตัวหนังสือไหลไม่หยุด โปรแกรมนี้พิมพ์เลข 1 ซ้ำๆ จนเต็มหน้าต่างและเลื่อนไปเรื่อยๆ
  • หน้าต่างดำนิ่ง ไม่มีอะไรขึ้น ถ้าในลูปไม่มี printf โปรแกรมจะคำนวณวนอยู่เงียบๆ ไม่มีบรรทัด Process returned ขึ้นมาสักที พัดลมเครื่องอาจเริ่มดัง

หน้าต่างดำที่นิ่งอาจไม่ใช่ลูปไม่รู้จบก็ได้ ถ้าโปรแกรมมี scanf มันอาจกำลังรอให้น้องพิมพ์ข้อมูลอยู่ ลองพิมพ์ข้อมูลแล้วกด Enter ก่อน ถ้ายังนิ่งอยู่ค่อยสงสัยลูป

วิธีหยุดโปรแกรมที่ค้างใน Code::Blocks เลือกได้สามทาง

  1. คลิกที่หน้าต่างดำ แล้วกด Ctrl+C
  2. กดปุ่มกากบาทปิดหน้าต่างดำ
  3. ใน Code::Blocks เลือกเมนู Build แล้วเลือก Abort หรือกดปุ่ม Abort บนแถบเครื่องมือ ซึ่งจะกดได้เฉพาะตอนที่โปรแกรมกำลังทำงาน

ถ้าลืมหยุดโปรแกรมเก่า แก้โค้ดแล้วคอมไพล์ใหม่เลย จะเจอ error Permission denied แบบเดียวกับแบบที่ 3 ในหน้า 0.3 (ตัด path ยาวๆ หน้า ld.exe ออกแล้ว)

ld.exe: cannot open output file forever.exe: Permission denied
collect2.exe: error: ld returned 1 exit status

ld คือส่วนที่ประกอบไฟล์ .exe ขั้นสุดท้าย เหตุผลและวิธีแก้เหมือนหน้า 0.3 คือ forever.exe ยังทำงานค้างอยู่ หยุดโปรแกรมเก่าก่อนแล้วคอมไพล์ใหม่ก็หาย ส่วนใน grader ลูปไม่รู้จบจะทำงานจนเกินเวลาที่โจทย์กำหนด แล้วได้ผลตรวจว่า Time Limit Exceeded

ลองทำให้พัง

แบบที่ 1 ใส่ ; ท้าย for

#include <stdio.h>

int main()
{
    int i;
    for (i = 1; i <= 5; i++);
    {
        printf("%d\n", i);
    }
    return 0;
}
semicolon.c:6:5: warning: this 'for' clause does not guard... [-Wmisleading-indentation]
    6 |     for (i = 1; i <= 5; i++);
      |     ^~~
semicolon.c:7:5: note: ...this statement, but the latter is misleadingly indented as if it were guarded by the 'for'
    7 |     {
      |     ^
6

; ท้ายบรรทัดที่ 6 คือคำสั่งว่างเปล่า ลูปจึงวนทำ "ไม่ทำอะไร" ห้ารอบ จนได้ i เป็น 6 แล้วค่อยไปทำปีกกาข้างล่างครั้งเดียว compiler เตือนว่า for นี้ไม่ได้คุม (does not guard) ปีกกาที่เยื้องเหมือนเป็นตัวลูป ทุกครั้งที่เห็นลูปทำแค่รอบเดียวแบบงงๆ ให้หา ; ท้าย for หรือ while ก่อน

แบบที่ 2 ไม่ใส่ปีกกา

ค่าย 3 วัน ได้เบี้ยเลี้ยงวันละ 10 บาท ตั้งใจให้ลูปพิมพ์วันและบวกเงินทุกวัน

#include <stdio.h>

int main()
{
    int total = 0;
    for (int day = 1; day <= 3; day++)
        printf("day %d\n", day);
        total += 10;
    printf("total = %d\n", total);
    return 0;
}
braces.c:6:5: warning: this 'for' clause does not guard... [-Wmisleading-indentation]
    6 |     for (int day = 1; day <= 3; day++)
      |     ^~~
braces.c:8:9: note: ...this statement, but the latter is misleadingly indented as if it were guarded by the 'for'
    8 |         total += 10;
      |         ^~~~~
day 1
day 2
day 3
total = 10

ถ้าไม่มีปีกกา ตัวลูปมีแค่คำสั่งเดียวที่อยู่ถัดไป การเยื้องที่เห็นไม่มีผลกับ compiler เลย total += 10; จึงทำครั้งเดียวหลังลูปจบ ได้ 10 แทนที่จะเป็น 30 กฎง่ายๆ คือ ใส่ปีกกาให้ลูปทุกครั้ง แม้มีคำสั่งเดียว

แบบที่ 3 ใช้ตัวนับของ for หลังลูปจบ

ในโปรแกรม count-for เปลี่ยนบรรทัดที่ 8 จาก printf("done\n"); เป็น

printf("after loop i = %d\n", i);
count_for.c:8:35: error: 'i' undeclared (first use in this function)
    8 |     printf("after loop i = %d\n", i);
      |                                   ^
count_for.c:8:35: note: each undeclared identifier is reported only once for each function it appears in

ตัวแปรที่ประกาศในวงเล็บของ for มีอยู่แค่ในลูปนั้น ลูปจบตัวแปรก็หายไป ถ้าต้องการค่าตัวนับหลังลูปจบ ให้ประกาศไว้ก่อนลูปแบบโปรแกรม semicolon ในแบบที่ 1 เรื่องที่ว่าตัวแปรมีชีวิตอยู่ตรงไหนบ้างจะเรียนละเอียดในบทที่ 8

แบบที่ 4 < กับ <= สลับกัน

ในโปรแกรม laps แก้เงื่อนไขเป็น lap < n

#include <stdio.h>

int main()
{
    int n;
    scanf("%d", &n);
    int lap = 1;
    while (lap < n) {
        printf("lap %d done\n", lap);
        lap++;
    }
    return 0;
}
3
lap 1 done
lap 2 done

ขาดไปหนึ่งรอบ compiler ไม่เตือน เพราะโค้ดถูกไวยากรณ์ ความผิดพลาดที่นับเกินหรือขาดไปหนึ่งแบบนี้มีชื่อเรียกว่า off-by-one เจอบ่อยที่สุดในเรื่องลูป วิธีกันคือถามตัวเองสองข้อ รอบแรกตัวนับเป็นเท่าไร และรอบสุดท้ายตัวนับเป็นเท่าไร ถ้าเริ่มที่ 1 และต้องจบที่ n ใช้ <= n ถ้าเริ่มที่ 0 และต้องการ n รอบ ใช้ < n

สรุป

  • while (เงื่อนไข) ตรวจก่อนทุกรอบ จริงทำต่อ เท็จออก ตัวลูปอาจไม่ทำเลยสักรอบ
  • ลูปนับรอบมีสามส่วน ค่าเริ่มต้น เงื่อนไข การเปลี่ยนค่า for รวมทั้งสามไว้ในบรรทัดเดียว
  • do { ... } while (เงื่อนไข); ทำก่อนหนึ่งรอบเสมอ และต้องมี ; ท้าย
  • รู้จำนวนรอบใช้ for ไม่รู้ใช้ while ต้องทำก่อนอย่างน้อยครั้งหนึ่งใช้ do while
  • ใส่ปีกกาให้ลูปเสมอ และห้ามมี ; ท้าย for หรือ while
  • ลูปค้าง หยุดด้วย Ctrl+C ในหน้าต่างดำ หรือ Build แล้ว Abort