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 | สิ่งที่เกิดขึ้น |
|---|---|---|---|
| 1 | 1 | จริง | พิมพ์ 1 แล้ว i เป็น 2 |
| 2 | 2 | จริง | พิมพ์ 2 แล้ว i เป็น 3 |
| 3 | 3 | จริง | พิมพ์ 3 แล้ว i เป็น 4 |
| 4 | 4 | จริง | พิมพ์ 4 แล้ว i เป็น 5 |
| 5 | 5 | จริง | พิมพ์ 5 แล้ว i เป็น 6 |
| 6 | เท็จ | ออกจากลูป ไปพิมพ์ done |
ลูปที่นับรอบแบบนี้มีสามส่วนเสมอ ขาดส่วนไหนไปลูปจะพัง
- ค่าเริ่มต้น
int i = 1;ตั้งตัวนับก่อนเข้าลูป - เงื่อนไข
i <= 5บอกว่าจะทำต่อไปถึงเมื่อไร - การเปลี่ยนค่า
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++ | หลังทำตัวลูปจบแต่ละรอบ แล้วค่อยไปตรวจเงื่อนไขใหม่ |
จะเขียนลูปนับรอบด้วย 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 เลือกได้สามทาง
- คลิกที่หน้าต่างดำ แล้วกด Ctrl+C
- กดปุ่มกากบาทปิดหน้าต่างดำ
- ใน 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