[0]zero-index clytie0x/zero-index

3.4 switch และ ? :

เมื่อต้องเอาตัวแปรตัวเดียวไปเทียบกับค่าตายตัวหลายค่า เช่น เลขวันเป็นชื่อวัน เลขเดือนเป็นไตรมาส ตัวอักษรเกรดเป็นแต้ม else if ทำได้ แต่ต้องเขียน day == ซ้ำทุกบรรทัด ภาษา C มีคำสั่ง switch ที่เขียนเรื่องแบบนี้ได้เป็นระเบียบกว่า ท้ายหน้ามีตัวดำเนินการ ? : สำหรับเลือกค่าหนึ่งในสองค่าให้จบในบรรทัดเดียว

คำสั่ง switch

#include <stdio.h>

int main()
{
    int day;
    scanf("%d", &day);
    switch (day) { // (1)!
        case 1: // (2)!
            printf("Monday\n");
            break; // (3)!
        case 2:
            printf("Tuesday\n");
            break;
        case 3:
            printf("Wednesday\n");
            break;
        case 4:
            printf("Thursday\n");
            break;
        case 5:
            printf("Friday\n");
            break;
        case 6:
            printf("Saturday\n");
            break;
        case 7:
            printf("Sunday\n");
            break;
        default: // (4)!
            printf("invalid\n");
    }
    return 0;
}
  1. ค่าในวงเล็บคือค่าที่จะเอาไปเทียบ ต้องเป็นจำนวนเต็มหรือ char
  2. case ตามด้วยค่าคงที่หนึ่งค่า และเครื่องหมาย : ถ้า day เท่ากับค่านี้ โปรแกรมจะกระโดดมาเริ่มทำตรงนี้
  3. break คือออกจาก switch ไปทำบรรทัดถัดจากปีกกาปิดของมัน
  4. default ทำเมื่อไม่ตรงกับ case ไหนเลย เหมือน else ตัวท้าย จะมีหรือไม่มีก็ได้
3
Wednesday

โปรแกรมนี้ทำงานเหมือนสาย if (day == 1) ... else if (day == 2) ... ทุกอย่าง แต่อ่านง่ายกว่าเพราะเห็นค่าที่เทียบเรียงกันเป็นแถว ถ้าป้อน 9 จะได้ invalid ถ้าตัด default ออกแล้วป้อน 9 โปรแกรมจะไม่พิมพ์อะไรเลย ในโจทย์ที่รับประกันว่าข้อมูลเข้าถูกต้องเสมอ ไม่ใส่ default ก็ได้ แต่ใส่ไว้ช่วยจับตอนป้อนผิดระหว่างทดสอบ

ไม่มี break แล้วไหลต่อ

สิ่งที่ switch ต่างจาก else if มากที่สุด คือ case เป็นแค่ จุดเริ่มต้น ไม่ใช่ block ที่มีขอบ พอกระโดดมาที่ case ที่ตรงแล้ว โปรแกรมจะทำคำสั่งไล่ลงไปเรื่อยๆ ผ่าน case ตัวถัดไปด้วย จนกว่าจะเจอ break หรือถึงปีกกาปิดของ switch การไหลต่อแบบนี้เรียกว่า fall through

ส่วนใหญ่มันคือบั๊ก ดูแบบที่ 1 ในส่วนลองทำให้พัง แต่มีกรณีหนึ่งที่ใช้ประโยชน์ได้ คือให้หลายค่าทำสิ่งเดียวกัน โดยวาง case ติดกันไว้โดยไม่มีคำสั่งคั่น

#include <stdio.h>

int main()
{
    int month;
    scanf("%d", &month);
    switch (month) {
        case 1:
        case 2:
        case 3:
            printf("Q1\n");
            break;
        case 4:
        case 5:
        case 6:
            printf("Q2\n");
            break;
        case 7:
        case 8:
        case 9:
            printf("Q3\n");
            break;
        case 10:
        case 11:
        case 12:
            printf("Q4\n");
            break;
        default:
            printf("invalid\n");
    }
    return 0;
}
8
Q3

เดือน 8 กระโดดมาที่ case 8: บรรทัดที่ 19 ซึ่งไม่มีคำสั่ง จึงไหลลงไป case 9: ซึ่งก็ไม่มีคำสั่งอีก แล้วไหลต่อถึง printf("Q3\n") และ break วิธีนี้ใช้บ่อยกับโจทย์จำนวนวันในแต่ละเดือน ที่เดือน 1 3 5 7 8 10 12 มี 31 วันเหมือนกัน

switch กับตัวอักษร

char คือตัวเลข จึงใช้กับ switch ได้ ใส่ตัวอักษรในเครื่องหมายคำพูดเดี่ยวหลัง case ตัวอย่างนี้แปลงเกรดเป็นแต้ม และรับได้ทั้งตัวพิมพ์เล็กและพิมพ์ใหญ่ด้วยการวาง case คู่กัน

#include <stdio.h>

int main()
{
    char grade;
    scanf(" %c", &grade);
    switch (grade) {
        case 'A':
        case 'a':
            printf("4\n");
            break;
        case 'B':
        case 'b':
            printf("3\n");
            break;
        case 'C':
        case 'c':
            printf("2\n");
            break;
        case 'D':
        case 'd':
            printf("1\n");
            break;
        case 'F':
        case 'f':
            printf("0\n");
            break;
        default:
            printf("unknown\n");
    }
    return 0;
}
b
3

switch ทำอะไรไม่ได้

switch เทียบได้แค่ว่า เท่ากับค่าคงที่ไหม เท่านั้น จึงมีข้อจำกัดที่ต้องรู้

  • เทียบช่วงไม่ได้ ตัดเกรดจากคะแนนด้วย case score >= 80: ไม่ได้ ต้องใช้ else if แบบหน้า 3.2
  • ใช้กับ double ไม่ได้ ค่าในวงเล็บต้องเป็นจำนวนเต็มหรือ char
  • ค่าหลัง case ต้องเป็นค่าคงที่ เช่น 5 หรือ 'A' ใส่ชื่อตัวแปรไม่ได้
  • ค่าหลัง case ห้ามซ้ำกัน

ถ้าลองฝ่าฝืน จะได้ error ตอนคอมไพล์ ดูได้ในส่วนลองทำให้พัง กฎง่ายๆ ในการเลือกคือ ค่าตายตัวไม่กี่ค่าใช้ switch ช่วงหรือเงื่อนไขซับซ้อนใช้ if

? : เลือกค่าแบบสั้น

บางครั้งเราแค่อยากได้ ค่า หนึ่งในสองค่าตามเงื่อนไข เช่น ค่าที่มากกว่าของสองตัว ถ้าใช้ if else ต้องเขียนห้าบรรทัด ตัวดำเนินการ ? : ทำได้ในนิพจน์เดียว

เงื่อนไข ? ค่าเมื่อจริง : ค่าเมื่อเท็จ
#include <stdio.h>

int main()
{
    int a, b;
    scanf("%d %d", &a, &b);
    int larger = a > b ? a : b;
    printf("larger = %d\n", larger);
    return 0;
}
14 9
larger = 14

อ่านบรรทัดที่ 7 ว่า "a มากกว่า b ไหม ถ้าใช่เอา a ถ้าไม่ใช่เอา b" ได้ผลเหมือนเขียน if (a > b) { larger = a; } else { larger = b; } ตัวดำเนินการนี้มีสามส่วน จึงมักเรียกว่า ternary operator

เพราะ ? : ให้ผลเป็นค่า จึงวางไว้ใน printf ได้เลย ใช้บ่อยกับการเลือกคำให้ถูกกับจำนวน

#include <stdio.h>

int main()
{
    int count;
    scanf("%d", &count);
    printf("%d %s\n", count, count == 1 ? "item" : "items");
    return 0;
}
1
1 item

ป้อน 3 จะได้ 3 items ใช้ ? : เฉพาะตอนที่เลือกค่าง่ายๆ แบบนี้ ถ้าต้องทำหลายคำสั่ง หรือมีมากกว่าสองทาง ให้กลับไปใช้ if การเอา ? : มาซ้อนกันหลายชั้นเขียนได้ แต่อ่านยากมาก

ลองทำให้พัง

แบบที่ 1 ลืม break

โปรแกรมชื่อวันที่ตัดเหลือสามวัน และลืม break ของสอง case แรก

#include <stdio.h>

int main()
{
    int day;
    scanf("%d", &day);
    switch (day) {
        case 1:
            printf("Monday\n");
        case 2:
            printf("Tuesday\n");
        case 3:
            printf("Wednesday\n");
            break;
        default:
            printf("other\n");
    }
    return 0;
}
1
Monday
Tuesday
Wednesday

ป้อน 1 ได้สามวัน เพราะเริ่มที่ case 1: แล้วไหลผ่าน case 2: กับ case 3: ไปจนเจอ break บรรทัดที่ 14 ป้อน 2 จะได้ Tuesday กับ Wednesday ส่วนป้อน 3 ได้ถูกต้องเพราะมี break พอดี

ที่น่ากลัวคือ -Wall ไม่เตือนเรื่องนี้เลย ถ้าเปิดคำเตือนเพิ่มด้วย -Wextra (ในหน้า Compiler Flags เดียวกับที่ติ๊ก -Wall ไว้ในบทที่ 0 มีช่อง Enable extra compiler warnings [-Wextra]) จะได้

day.c:9:13: warning: this statement may fall through [-Wimplicit-fallthrough=]
    9 |             printf("Monday\n");
      |             ^~~~~~~~~~~~~~~~~~
day.c:10:9: note: here
   10 |         case 2:
      |         ^~~~
day.c:11:13: warning: this statement may fall through [-Wimplicit-fallthrough=]
   11 |             printf("Tuesday\n");
      |             ^~~~~~~~~~~~~~~~~~~
day.c:12:9: note: here
   12 |         case 3:
      |         ^~~~

คำเตือนนี้ไม่ขึ้นกับ case ที่วางติดกันโดยไม่มีคำสั่งคั่นแบบโปรแกรม quarter เพราะแบบนั้นชัดเจนว่าตั้งใจ นิสัยที่ช่วยได้มากกว่าคำเตือน คือเขียน case แล้วพิมพ์ break; ปิดไว้ก่อนทันที ค่อยเติมคำสั่งตรงกลาง

แบบที่ 2 ใส่ช่วงหลัง case

    switch (score) {
        case score >= 80:
            printf("A\n");
            break;
grade.c:8:9: error: case label does not reduce to an integer constant
    8 |         case score >= 80:
      |         ^~~~

case label คือค่าหลัง case ข้อความบอกว่ามันไม่ได้เป็นค่าคงที่จำนวนเต็ม เพราะมีตัวแปร score อยู่ข้างใน ค่าของมันรู้ตอนรันเท่านั้น switch ต้องรู้ทุกค่าตั้งแต่ตอนคอมไพล์ งานแบ่งช่วงให้ใช้ else if

แบบที่ 3 switch กับ double

    double height;
    scanf("%lf", &height);
    switch (height) {
        case 1.5:
height.c:7:13: error: switch quantity not an integer
    7 |     switch (height) {
      |             ^~~~~~
height.c:8:9: error: case label does not reduce to an integer constant
    8 |         case 1.5:
      |         ^~~~

ได้สอง error ตัวแรกบอกว่าค่าที่เอามา switch ไม่ใช่จำนวนเต็ม ตัวที่สองบอกว่า 1.5 ไม่ใช่ค่าคงที่จำนวนเต็ม ทศนิยมเทียบเท่ากันตรงๆ ไม่ได้อยู่แล้วตามหน้า 3.3 ภาษา C จึงไม่ยอมตั้งแต่ต้น

แบบที่ 4 case ซ้ำ

ในโปรแกรม quarter พิมพ์ case 4: ผิดเป็น case 3:

quarter.c:13:9: error: duplicate case value
   13 |         case 3:
      |         ^~~~
quarter.c:10:9: note: previously used here
   10 |         case 3:
      |         ^~~~

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

สรุป

  • switch เทียบตัวแปรหนึ่งตัวกับค่าคงที่หลายค่า ใช้ได้กับจำนวนเต็มและ char
  • ทุก case ต้องจบด้วย break ไม่อย่างนั้นจะไหลต่อไป case ถัดไป และ -Wall ไม่เตือน
  • วาง case ติดกันโดยไม่มีคำสั่งคั่น เพื่อให้หลายค่าทำสิ่งเดียวกัน
  • default ทำเมื่อไม่ตรงกับ case ไหน เหมือน else ตัวท้าย
  • ช่วงตัวเลข ทศนิยม และเงื่อนไขที่มี && || ใช้ if
  • เงื่อนไข ? ก : ข ให้ค่า ก เมื่อจริง ข เมื่อเท็จ ใช้กับการเลือกค่าง่ายๆ เท่านั้น

กลับไปเช็กรายการ ผ่านบทนี้เมื่อ แล้วไปทำแบบฝึกหัด