[0]zero-index clytie0x/zero-index

4.3 break continue และอ่านจนหมดข้อมูล

ลูปในสองหน้าก่อนวนครบทุกรอบเสมอ แต่งานจริงบางทีอยากเลิกกลางทาง เช่น เจอสิ่งที่หาแล้วก็พอ หรืออยากข้ามข้อมูลบางตัวไป หน้านี้รู้จัก break กับ continue แล้วใช้มันอ่านข้อมูลแบบที่โจทย์ไม่บอกว่ามีกี่ตัว ซึ่งมีสองแบบ คือ อ่านจนเจอค่าหยุด และอ่านจนหมดไฟล์

break ออกจากลูปทันที

ครูอยากรู้ว่านักเรียนคนแรกในรายชื่อที่สอบตก (ได้ต่ำกว่า 50) คือคนที่เท่าไร พอเจอคนแรกแล้ว ไม่ต้องดูคนที่เหลืออีก

#include <stdio.h>

int main()
{
    int n;
    scanf("%d", &n);
    int fail_at = 0;
    for (int i = 1; i <= n; i++) {
        int score;
        scanf("%d", &score);
        if (score < 50) {
            fail_at = i;
            break;
        }
    }
    if (fail_at == 0) {
        printf("everyone passed\n");
    } else {
        printf("first fail is student %d\n", fail_at);
    }
    return 0;
}
6
70 85 49 62 48 50
first fail is student 3
  • break หยุดลูปที่มันอยู่ทันที แล้วกระโดดไปทำบรรทัดถัดจากปีกกาปิดของลูป ในตัวอย่างคือ if บรรทัดที่ 16
  • fail_at เป็นตัวแปรที่จำว่าเจอหรือยัง เริ่มที่ 0 ซึ่งไม่ใช่ลำดับของนักเรียนคนไหน ถ้าลูปวนจนครบโดยไม่เจอ ค่าก็ยังเป็น 0 ตัวแปรที่ใช้บอกสถานะแบบนี้เรียกว่า flag (ธง)

break ทิ้งข้อมูลที่ยังไม่ได้อ่าน

หลัง break ตัวเลข 62 48 50 ไม่ได้ถูกอ่านเลย ในตัวอย่างนี้ไม่เป็นไรเพราะโปรแกรมจบแล้ว แต่ถ้าหลังลูปยังต้องอ่านข้อมูลส่วนอื่นต่อ scanf ตัวถัดไปจะได้ 62 แทนที่จะได้ข้อมูลที่ตั้งใจ แบบนั้นห้ามใช้ break ให้วนอ่านจนครบ แล้วใช้ flag จำผลแทน

continue ข้ามไปรอบถัดไป

คะแนน -1 หมายถึงนักเรียนคนนั้นขาดสอบ ให้หาค่าเฉลี่ยของคนที่มาสอบ และนับคนที่ขาด

#include <stdio.h>

int main()
{
    int n;
    scanf("%d", &n);
    int total = 0, present = 0, absent = 0;
    for (int i = 1; i <= n; i++) {
        int score;
        scanf("%d", &score);
        if (score == -1) {
            absent++;
            continue;
        }
        total += score;
        present++;
    }
    printf("absent = %d\n", absent);
    printf("average = %.2f\n", (double)total / present);
    return 0;
}
5
80 -1 75 -1 90
absent = 2
average = 81.67

continue ข้ามคำสั่งที่เหลือในรอบนี้ทั้งหมด แล้วไปเริ่มรอบถัดไป ใน for มันจะไปทำส่วนเปลี่ยนค่า i++ ก่อน แล้วค่อยตรวจเงื่อนไข เหมือนรอบนั้นจบตามปกติ

จริงๆ แล้วเขียนด้วย if else แทนก็ได้ทุกครั้ง continue แค่ช่วยให้โค้ดไม่ต้องซ้อนลึก อ่านแล้วเข้าใจทันทีว่า "ตัวนี้ไม่เอา ข้ามไป" ถ้าใช้แล้วงง ใช้ if else ไปเลยไม่ผิด

อ่านจนเจอค่าหยุด

บางโจทย์ไม่บอกว่ามีข้อมูลกี่ตัว แต่บอกว่า "ข้อมูลจบด้วย -1" หรือ "จบด้วย 0" ค่าที่บอกว่าหมดแล้วนี้เรียกว่า ค่าหยุด (sentinel) แปลตรงตัวว่ายามเฝ้าประตู มันไม่ใช่ข้อมูลจริง ห้ามเอาไปคำนวณ

#include <stdio.h>

int main()
{
    int total = 0, count = 0;
    while (1) {
        int score;
        scanf("%d", &score);
        if (score == -1) {
            break;
        }
        total += score;
        count++;
    }
    if (count == 0) {
        printf("no scores\n");
    } else {
        printf("count = %d\n", count);
        printf("average = %.2f\n", (double)total / count);
    }
    return 0;
}
72 65 88 91 -1
count = 4
average = 79.00
  • while (1) คือเงื่อนไขที่จริงตลอด เพราะภาษา C ถือว่า 0 คือเท็จ และค่าอื่นที่ไม่ใช่ 0 คือจริง ลูปนี้จึงจะไม่จบเอง ทางออกเดียวคือ break
  • ลำดับสำคัญมาก อ่าน ตรวจค่าหยุด แล้วค่อยใช้ ถ้าสลับเป็นใช้ก่อนตรวจ -1 จะหลุดเข้าไปในผลรวม ดูในส่วนลองทำให้พัง
  • ถ้าข้อมูลมีแค่ -1 ตัวเดียว count เป็น 0 ถ้าหารเลยจะได้ค่าเฉลี่ยที่ไม่มีความหมาย จึงต้องแยกกรณีนี้ไว้ ชุดทดสอบที่ไม่มีข้อมูลเลยเป็นชุดขอบที่เจอบ่อย

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

scanf("%d", &score);
while (score != -1) {
    total += score;
    count++;
    scanf("%d", &score);
}

อ่านจนหมดไฟล์

โจทย์บางข้อไม่มีทั้งจำนวนข้อมูลและค่าหยุด บอกแค่ว่า "อ่านจนจบข้อมูลเข้า" ใน grader ข้อมูลเข้ามาจากไฟล์ เราจึงต้องรู้ว่าไฟล์หมดแล้วหรือยัง คำตอบอยู่ที่ค่าที่ scanf ส่งกลับมา ซึ่งเราไม่เคยสนใจมาก่อน

#include <stdio.h>

int main()
{
    int score;
    int result = scanf("%d", &score);
    printf("result = %d\n", result);
    return 0;
}
70
result = 1

scanf ส่งกลับมาว่าอ่านค่าใส่ตัวแปรสำเร็จกี่ตัว รันโปรแกรมนี้กับข้อมูลเข้าสามแบบได้ผลแบบนี้

ข้อมูลเข้าresultความหมาย
701อ่านได้ 1 ตัว
abc0เจอข้อมูลแต่ไม่ใช่ตัวเลข อ่านไม่ได้สักตัว
ไม่มีอะไรเลย-1ข้อมูลหมดแล้ว ค่านี้มีชื่อว่า EOF (end of file)

ดังนั้น "อ่านจนหมด" เขียนได้ว่า ตราบใดที่ scanf อ่านได้ครบตามจำนวนที่ขอ ให้ทำต่อ

#include <stdio.h>

int main()
{
    int score;
    int total = 0, count = 0;
    while (scanf("%d", &score) == 1) {
        total += score;
        count++;
    }
    printf("count = %d\n", count);
    printf("total = %d\n", total);
    return 0;
}
70 85 90
62 48
count = 5
total = 355

ทุกรอบ scanf อ่านตัวเลขหนึ่งตัวใส่ score แล้วส่งกลับ 1 เงื่อนไขจริง จึงทำตัวลูป พออ่านครบห้าตัวแล้วไม่มีอะไรเหลือ scanf ส่งกลับ -1 เงื่อนไขเป็นเท็จ ลูปจบ ถ้ารับทีละสองค่า เช่น scanf("%d %d", &a, &b) ก็เทียบกับ 2 แทน

จบข้อมูลเองในหน้าต่างดำ

ตอนกด F9 ทดลองใน Code::Blocks ข้อมูลมาจากคีย์บอร์ด ซึ่งไม่มีวันหมดเอง โปรแกรมจะรอเราพิมพ์ไปเรื่อยๆ ต้องบอก Windows ว่าข้อมูลจบแล้ว ทำแบบนี้

  1. พิมพ์ข้อมูลให้ครบ แล้วกด Enter ให้เคอร์เซอร์ขึ้นบรรทัดใหม่
  2. ที่ต้นบรรทัดใหม่ กด Ctrl+Z จะเห็น ^Z ขึ้นมา
  3. กด Enter อีกครั้ง scanf จะได้ EOF แล้วโปรแกรมพิมพ์ผลลัพธ์
70 85 90
62 48
^Z
count = 5
total = 355

กด Ctrl+Z ที่ต้นบรรทัดใหม่เท่านั้น อย่ากดต่อท้ายตัวเลขในบรรทัดเดียวกัน ส่วนใน grader ไม่ต้องทำอะไร ไฟล์ข้อมูลเข้ามีจุดจบอยู่แล้ว

ลองทำให้พัง

แบบที่ 1 ไม่เทียบกับ 1

ในโปรแกรม until-eof ตัด == 1 ออก เหลือ while (scanf("%d", &score))

คอมไพล์ผ่านไม่มีคำเตือน แต่ป้อน 70 85 90 แล้วจบข้อมูล โปรแกรมไม่พิมพ์อะไรเลยและไม่จบ ต้องหยุดเองตามวิธีในหน้า 4.1 สาเหตุคือตอนข้อมูลหมด scanf ส่งกลับ -1 ซึ่งไม่ใช่ 0 จึงถือว่าจริง ลูปวนต่อไป scanf ก็ส่ง -1 กลับมาอีก วนอย่างนี้ไม่มีที่สิ้นสุด ใน grader ข้อนี้จะได้ Time Limit Exceeded ทุกชุด

บางคนแก้เป็น != EOF ซึ่งจบได้ตอนไฟล์หมดก็จริง แต่ถ้าข้อมูลมีตัวที่ไม่ใช่ตัวเลขปนมา scanf จะส่ง 0 ซ้ำๆ โดยไม่ขยับไปไหน แล้วค้างแบบเดิม เทียบ == 1 ปลอดภัยที่สุด

แบบที่ 2 continue ใน while

อยากพิมพ์ 1 ถึง 10 ยกเว้นเลขที่หารด้วย 3 ลงตัว ถ้าใช้ for ทำงานถูก

#include <stdio.h>

int main()
{
    for (int i = 1; i <= 10; i++) {
        if (i % 3 == 0) {
            continue;
        }
        printf("%d\n", i);
    }
    return 0;
}
1
2
4
5
7
8
10

แต่ถ้าแปลงเป็น while ตรงๆ

int i = 1;
while (i <= 10) {
    if (i % 3 == 0) {
        continue;
    }
    printf("%d\n", i);
    i++;
}

โปรแกรมพิมพ์ 1 กับ 2 แล้วนิ่งไม่จบ พอ i เป็น 3 continue กระโดดกลับไปตรวจเงื่อนไขทันที ข้าม i++ ที่อยู่ท้ายลูปไปด้วย i จึงเป็น 3 ตลอดกาล ใน for ไม่มีปัญหานี้เพราะ i++ อยู่ในวงเล็บของ for ซึ่ง continue ไม่ได้ข้าม

แบบที่ 3 นับค่าหยุดเป็นข้อมูล

เห็นว่าต้องอ่านอย่างน้อยหนึ่งครั้ง เลยเลือก do while แต่ใช้ค่าก่อนตรวจ

#include <stdio.h>

int main()
{
    int score;
    int total = 0, count = 0;
    do {
        scanf("%d", &score);
        total += score;
        count++;
    } while (score != -1);
    printf("count = %d\n", count);
    printf("average = %.2f\n", (double)total / count);
    return 0;
}
72 65 88 91 -1
count = 5
average = 63.00

-1 ถูกนับเป็นคะแนนคนที่ห้า ผลรวมจึงเป็น 315 หารด้วย 5 ได้ 63.00 แทนที่จะเป็น 79.00 ค่าหยุดต้องถูกตรวจก่อนนำไปใช้เสมอ

สรุป

  • break ออกจากลูปทันที continue ข้ามส่วนที่เหลือของรอบนี้ไปรอบถัดไป
  • ใช้ flag จำว่าเจอหรือยัง และระวังว่า break ทิ้งข้อมูลที่ยังไม่ได้อ่าน
  • continue ใน while ข้ามการเปลี่ยนค่าตัวนับ อาจกลายเป็นลูปไม่รู้จบ
  • อ่านจนเจอค่าหยุด ใช้ while (1) อ่าน ตรวจ แล้ว break ก่อนนำไปใช้
  • อ่านจนหมดไฟล์ ใช้ while (scanf("%d", &x) == 1) ต้องมี == 1
  • ทดลองในหน้าต่างดำ จบข้อมูลด้วย Enter Ctrl+Z Enter