[0]zero-index clytie0x/zero-index

3.3 ตรรกะ และ หรือ ไม่

เงื่อนไขในชีวิตจริงมักมีหลายข้อพร้อมกัน สมัครค่ายได้ถ้าอายุไม่เกิน 18 และ สอบคัดเลือกผ่าน ตอบ y หรือ Y ก็นับว่าตกลง คะแนนที่ ไม่ อยู่ในช่วง 0 ถึง 100 ถือว่าผิด หน้านี้รู้จักตัวดำเนินการตรรกะสามตัวที่ใช้ต่อเงื่อนไขเข้าด้วยกัน วิธีเขียนช่วงของตัวเลขให้ถูก และใช้ทั้งหมดตรวจปีอธิกสุรทิน ซึ่งเป็นโจทย์คลาสสิกของเรื่องนี้

&& และ

A && B เป็นจริงเมื่อ ทั้ง A และ B จริง ข้อใดข้อหนึ่งเท็จก็เท็จทั้งหมด

#include <stdio.h>

int main()
{
    int age, score;
    scanf("%d %d", &age, &score);
    if (age <= 18 && score >= 60) {
        printf("accepted\n");
    } else {
        printf("rejected\n");
    }
    return 0;
}
16 75
accepted

ถ้าป้อน 19 75 หรือ 16 40 จะได้ rejected ทั้งคู่ เพราะเงื่อนไขข้อหนึ่งไม่ผ่าน ทั้งหมดนี้เขียนด้วย if ซ้อนแบบหน้า 3.2 ก็ได้ แต่ && สั้นกว่าเมื่อทั้งสองข้อนำไปสู่ผลเดียวกัน

|| หรือ

A || B เป็นจริงเมื่อ อย่างน้อยหนึ่ง ใน A หรือ B จริง จะเท็จก็ต่อเมื่อเท็จทั้งคู่ เครื่องหมาย | อยู่บนปุ่มเดียวกับ \ กด Shift+\ สองครั้ง

#include <stdio.h>

int main()
{
    char answer;
    scanf(" %c", &answer);
    if (answer == 'y' || answer == 'Y') {
        printf("yes\n");
    } else {
        printf("no\n");
    }
    return 0;
}
Y
yes

นี่คือวิธีรับได้ทั้งตัวพิมพ์เล็กและพิมพ์ใหญ่ ที่ค้างไว้จากหน้า 3.1 สังเกตว่าต้องเขียน answer == ซ้ำทั้งสองข้าง ถ้าเขียนย่อจะพัง ดูแบบที่ 2 ในส่วนลองทำให้พัง

! ไม่

!A กลับค่าความจริง จริงกลายเป็นเท็จ เท็จกลายเป็นจริง ใช้กับตัวแปรที่เก็บ 1 กับ 0 ไว้บอกสถานะได้อ่านง่าย ร้านกาแฟคิดแก้วละ 60 บาท คนที่ไม่ใช่สมาชิกจ่ายเพิ่ม 10 บาท

#include <stdio.h>

int main()
{
    int is_member;
    scanf("%d", &is_member);
    int price = 60;
    if (!is_member) {
        price = price + 10;
    }
    printf("%d\n", price);
    return 0;
}
0
70

!is_member อ่านว่า "ไม่ใช่สมาชิก" is_member เป็น 0 จึงได้ 1 ซึ่งเป็นจริง ราคาเลยเพิ่ม ชื่อตัวแปรที่ขึ้นต้นด้วย is_ ช่วยให้รู้ว่าเก็บแค่จริงกับเท็จ

ส่วนเงื่อนไขเปรียบเทียบ ไม่ค่อยต้องใช้ ! เพราะมีตัวตรงข้ามอยู่แล้ว !(score >= 50) คือ score < 50 และ !(a == b) คือ a != b แบบหลังอ่านง่ายกว่า

ตารางสรุป

ABA && BA || B!A
00001
01011
10010
11110

ผลของ && || ! เป็น 1 หรือ 0 เสมอ เหมือนการเปรียบเทียบ จำง่ายๆ ว่า && เข้มงวด ต้องผ่านทุกข้อ || ใจดี ผ่านข้อเดียวก็พอ

เขียนช่วงของตัวเลข

ในคณิตศาสตร์เขียน 0 ≤ score ≤ 100 แต่ในภาษา C ต้องแยกเป็นสองเงื่อนไขแล้วต่อด้วย && คือ score ต้องไม่น้อยกว่า 0 และ ไม่มากกว่า 100

#include <stdio.h>

int main()
{
    int score;
    scanf("%d", &score);
    if (0 <= score && score <= 100) {
        printf("valid\n");
    } else {
        printf("invalid\n");
    }
    return 0;
}
105
invalid

เขียนตัวเล็กไว้ซ้าย ตัวใหญ่ไว้ขวา เรียงเหมือนเส้นจำนวน low <= x && x <= high อ่านแล้วเห็นภาพว่า x อยู่ระหว่างกลาง ส่วน นอกช่วง คือน้อยกว่าขอบล่าง หรือ มากกว่าขอบบน

อยู่ในช่วง   0 <= score && score <= 100
อยู่นอกช่วง  score < 0 || score > 100

สังเกตว่าพอกลับจากในช่วงเป็นนอกช่วง && กลายเป็น || และเครื่องหมายเปรียบเทียบกลับด้านทุกตัว ถ้าสับสน เขียน !(0 <= score && score <= 100) ก็ได้ผลเดียวกัน

ตัวอักษรก็เขียนช่วงได้ เพราะ 'A' ถึง 'Z' มีรหัสเรียงกัน ตัวเลข '0' ถึง '9' ก็เช่นกัน

#include <stdio.h>

int main()
{
    char c;
    scanf(" %c", &c);
    if ('A' <= c && c <= 'Z') {
        printf("upper\n");
    } else if ('a' <= c && c <= 'z') {
        printf("lower\n");
    } else if ('0' <= c && c <= '9') {
        printf("digit\n");
    } else {
        printf("other\n");
    }
    return 0;
}
g
lower

ลำดับการประเมิน

นิพจน์อย่าง a + 1 > b * 2 && c != 0 ทำส่วนไหนก่อน ภาษา C มีลำดับตายตัว เรียงจากทำก่อนไปทำทีหลัง

ลำดับตัวดำเนินการ
1! และเครื่องหมายลบหน้าตัวเลข
2* / %
3+ -
4< <= > >=
5== !=
6&&
7||

ลำดับนี้ออกแบบมาให้ใช้งานทั่วไปได้โดยไม่ต้องใส่วงเล็บ คำนวณก่อน แล้วเปรียบเทียบ แล้วค่อยต่อด้วยตรรกะ a + 1 > b * 2 && c != 0 จึงหมายถึง ((a + 1) > (b * 2)) && (c != 0) ตามที่ตั้งใจ แต่มีสองจุดที่ทำให้พลาดบ่อย

จุดแรก && มาก่อน || สวนสาธารณะลดราคาให้เด็กต่ำกว่า 12 ปี และผู้สูงอายุตั้งแต่ 60 ปีที่เป็นสมาชิก

    if (age < 12 || age >= 60 && member == 1) {
senior.c:7:31: warning: suggest parentheses around '&&' within '||' [-Wparentheses]
    7 |     if (age < 12 || age >= 60 && member == 1) {
      |                     ~~~~~~~~~~^~~~~~~~~~~~~~

โค้ดนี้ทำงานเหมือน age < 12 || (age >= 60 && member == 1) ซึ่งบังเอิญตรงกับที่ต้องการ แต่ถ้าโจทย์คือ "เด็กหรือผู้สูงอายุ ที่เป็นสมาชิกทั้งคู่" ต้องเขียน (age < 12 || age >= 60) && member == 1 คำเตือนนี้ขอให้ใส่วงเล็บบอกให้ชัดว่าหมายถึงแบบไหน ผสม && กับ || เมื่อไร ใส่วงเล็บทุกครั้ง

จุดที่สอง ! มาก่อนทุกอย่าง เขียน !score >= 50 ตั้งใจให้หมายถึงคะแนนไม่ถึง 50

fail.c:7:16: warning: comparison of constant '50' with boolean expression is always false [-Wbool-compare]
    7 |     if (!score >= 50) {
      |                ^~
fail.c:7:16: warning: logical not is only applied to the left hand side of comparison [-Wlogical-not-parentheses]
fail.c:7:9: note: add parentheses around left hand side expression to silence this warning
    7 |     if (!score >= 50) {
      |         ^~~~~~
      |         (     )

C ทำ !score ก่อน ได้ 0 หรือ 1 แล้วค่อยเอาไปเทียบกับ 50 ซึ่งไม่มีทางถึง compiler บอกตรงๆ ว่า always false คือเท็จเสมอ ที่ตั้งใจคือ !(score >= 50) หรือเขียน score < 50 ไปเลยดีที่สุด

หยุดเมื่อรู้คำตอบแล้ว

C ประเมิน && กับ || จากซ้ายไปขวา และ หยุดทันทีที่รู้คำตอบ เรียกว่า short-circuit evaluation

  • A && B ถ้า A เท็จ ผลเป็นเท็จแน่นอน ไม่ประเมิน B เลย
  • A || B ถ้า A จริง ผลเป็นจริงแน่นอน ไม่ประเมิน B เลย

นิสัยนี้ใช้เป็นด่านกันพังได้ แบ่งลูกอมให้เพื่อนเท่าๆ กันได้ไหม ต้องหารลงตัว แต่ถ้าจำนวนคนเป็น 0 การใช้ % จะพังตามที่เห็นในหน้า 1.2

#include <stdio.h>

int main()
{
    int total, people;
    scanf("%d %d", &total, &people);
    if (people != 0 && total % people == 0) {
        printf("fair\n");
    } else {
        printf("not fair\n");
    }
    return 0;
}
10 0
not fair

people != 0 เป็นเท็จ C จึงไม่ไปทำ total % people เลย โปรแกรมไม่พัง ถ้าสลับเป็น total % people == 0 && people != 0 ส่วนที่หารจะถูกทำก่อน แล้วโปรแกรมก็ปิดตัวพร้อมรหัสเดียวกับหน้า 1.2

Process returned -1073741676 (0xC0000094)

ลำดับซ้ายขวาของ && จึงมีความหมาย ให้วางเงื่อนไขที่เป็นด่านกันไว้ซ้ายสุดเสมอ

ปีอธิกสุรทิน

ปีอธิกสุรทินคือปีที่เดือนกุมภาพันธ์มี 29 วัน ทั้งปีจึงมี 366 วัน กฎตามปฏิทินสากลมีสามข้อ

  1. ปีที่หารด้วย 4 ลงตัว เป็นปีอธิกสุรทิน
  2. ยกเว้นปีที่หารด้วย 100 ลงตัว ไม่เป็น
  3. ยกเว้นอีกชั้น ปีที่หารด้วย 400 ลงตัว เป็น

รวบเป็นประโยคเดียวได้ว่า หาร 4 ลงตัว และ หาร 100 ไม่ลงตัว หรือ หาร 400 ลงตัว

#include <stdio.h>

int main()
{
    int year;
    scanf("%d", &year);
    if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0) {
        printf("leap\n");
    } else {
        printf("common\n");
    }
    return 0;
}
1900
common

อีกวิธีที่อ่านง่ายไม่แพ้กัน คือไล่ด้วย else if จาก กฎที่เจาะจงที่สุดก่อน หาร 400 ลงตัวเป็นกรณีพิเศษที่สุด จึงตรวจก่อน

#include <stdio.h>

int main()
{
    int year;
    scanf("%d", &year);
    if (year % 400 == 0) {
        printf("leap\n");
    } else if (year % 100 == 0) {
        printf("common\n");
    } else if (year % 4 == 0) {
        printf("leap\n");
    } else {
        printf("common\n");
    }
    return 0;
}
2000
leap

ถ้าสลับให้ตรวจ % 4 ก่อน ปี 1900 จะหลุดไปเป็น leap ตั้งแต่ข้อแรก เป็นกับดักเดียวกับการเรียงเกรดผิดลำดับในหน้า 3.2 ปีที่ต้องลองทุกครั้งที่เขียนเรื่องนี้มีสี่ปี

ปีผลเพราะ
2024leapหาร 4 ลงตัว หาร 100 ไม่ลงตัว
2023commonหาร 4 ไม่ลงตัว
1900commonหาร 100 ลงตัว แต่หาร 400 ไม่ลงตัว
2000leapหาร 400 ลงตัว

โจทย์ภาษาไทยบางข้อให้ปีเป็นพุทธศักราช ต้องลบ 543 ให้เป็นคริสต์ศักราชก่อน แล้วค่อยใช้กฎ เช่น พ.ศ. 2567 คือ ค.ศ. 2024

เทียบทศนิยมว่าเท่ากัน

หน้า 1.4 เห็นแล้วว่า 0.1 + 0.2 ไม่ได้เป็น 0.3 พอดี ผลคือการเทียบด้วย == ใช้ไม่ได้

#include <stdio.h>

int main()
{
    double total = 0.1 + 0.2;
    if (total == 0.3) {
        printf("equal\n");
    } else {
        printf("not equal\n");
    }
    return 0;
}
not equal

ไม่มีคำเตือนอะไรเลย วิธีที่ใช้กันคือถามว่า ใกล้กันมากพอไหม แทนที่จะถามว่าเท่ากันเป๊ะไหม คือผลต่างอยู่ในช่วงแคบๆ รอบ 0 ใช้วิธีเขียนช่วงที่เพิ่งเรียน

#include <stdio.h>

int main()
{
    double total = 0.1 + 0.2;
    double diff = total - 0.3;
    if (-1e-9 < diff && diff < 1e-9) {
        printf("equal\n");
    } else {
        printf("not equal\n");
    }
    return 0;
}
equal

1e-9 คือวิธีเขียนเลขแบบวิทยาศาสตร์ในภาษา C แปลว่า 1 × 10-9 หรือ 0.000000001 ค่าเล็กๆ นี้เรียกว่า epsilon จะเลือกเท่าไรขึ้นกับขนาดของตัวเลขในโจทย์ ทางที่ดีกว่านั้นคือเลี่ยงทศนิยมไปเลยถ้าทำได้ เช่น เก็บเงินเป็นสตางค์ด้วย int แทนการเก็บบาทเป็น double แล้วเทียบด้วย == ได้ตามปกติ

ลองทำให้พัง

แบบที่ 1 เขียนช่วงแบบคณิตศาสตร์

#include <stdio.h>

int main()
{
    int x;
    scanf("%d", &x);
    if (1 <= x <= 10) {
        printf("in range\n");
    } else {
        printf("out of range\n");
    }
    return 0;
}
range.c:7:16: warning: comparison of constant '10' with boolean expression is always true [-Wbool-compare]
    7 |     if (1 <= x <= 10) {
      |                ^~
range.c:7:11: warning: comparisons like 'X<=Y<=Z' do not have their mathematical meaning [-Wparentheses]
    7 |     if (1 <= x <= 10) {
      |         ~~^~~~
0
in range

0 ไม่อยู่ในช่วง 1 ถึง 10 แต่ได้ in range C ไม่ได้อ่านแบบคณิตศาสตร์ มันทำทีละคู่จากซ้ายไปขวา 1 <= 0 ได้ 0 แล้วเอา 0 ไปเทียบต่อ 0 <= 10 ได้ 1 ไม่ว่า x เป็นอะไร ผลของคู่แรกเป็นได้แค่ 0 หรือ 1 ซึ่งไม่เกิน 10 เสมอ คำเตือนแรกจึงบอกว่า always true คำเตือนที่สองบอกตรงๆ ว่าการเขียนแบบ X<=Y<=Z ไม่ได้มีความหมายแบบคณิตศาสตร์ แก้เป็น 1 <= x && x <= 10

แบบที่ 2 เขียน || แบบย่อ

#include <stdio.h>

int main()
{
    char answer;
    scanf(" %c", &answer);
    if (answer == 'y' || 'Y') {
        printf("yes\n");
    } else {
        printf("no\n");
    }
    return 0;
}
n
yes

ตอบ n แต่ได้ yes ภาษาพูดว่า "answer เป็น y หรือ Y" แต่ C อ่านเป็นสองเงื่อนไขแยกกัน คือ answer == 'y' หรือ 'Y' เฉยๆ ซึ่งเป็นรหัส 89 ไม่ใช่ 0 จึงจริงเสมอ คราวนี้ไม่มีคำเตือนแม้แต่บรรทัดเดียว ทั้งสองข้างของ || ต้องเป็นเงื่อนไขที่สมบูรณ์ในตัวเอง

แบบที่ 3 ใช้ && ตรงที่ต้องใช้ ||

ตรวจคะแนนนอกช่วง 0 ถึง 100 แต่ต่อด้วย &&

#include <stdio.h>

int main()
{
    int score;
    scanf("%d", &score);
    if (score < 0 && score > 100) {
        printf("invalid\n");
    } else {
        printf("valid\n");
    }
    return 0;
}
-5
valid

ไม่มีตัวเลขไหนที่น้อยกว่า 0 และมากกว่า 100 พร้อมกัน เงื่อนไขนี้จึงเท็จเสมอ ทุกคะแนนกลายเป็น valid วิธีจับผิดคือถามตัวเองว่า มีค่าไหนบ้างไหมที่ทำให้เงื่อนไขนี้จริง ถ้าหาไม่ได้เลย แปลว่าเขียนผิด นอกช่วงต้องใช้ ||

แบบที่ 4 หาค่ามากสุดด้วย && แล้วเจอค่าเท่ากัน

อีกวิธีหาค่ามากสุดของสามตัวที่หลายคนคิดได้ก่อน คือถามว่า a มากกว่าทั้ง b และ c ไหม

#include <stdio.h>

int main()
{
    int a, b, c;
    scanf("%d %d %d", &a, &b, &c);
    int largest;
    if (a > b && a > c) {
        largest = a;
    } else if (b > a && b > c) {
        largest = b;
    } else {
        largest = c;
    }
    printf("%d\n", largest);
    return 0;
}
5 5 3
3

ค่ามากสุดคือ 5 แต่ได้ 3 เพราะ a กับ b เท่ากัน ไม่มีใคร มากกว่า อีกตัว ทั้งสองข้อแรกจึงเท็จ ตกไปที่ else ซึ่งเดาว่าเป็น c ทั้งที่ c น้อยที่สุด ชุดทดสอบที่ตัวเลขไม่ซ้ำกันเลยจะผ่านหมด แต่ตกชุดที่มีค่าเท่ากัน แก้ได้ด้วยการเปลี่ยนเป็น >= แต่วิธีตั้งแชมป์ในหน้า 3.2 ไม่มีปัญหานี้ตั้งแต่ต้น

สรุป

  • && จริงเมื่อจริงทั้งคู่ || จริงเมื่อจริงอย่างน้อยหนึ่ง ! กลับค่าความจริง
  • ช่วงเขียนเป็น low <= x && x <= high นอกช่วงเขียนเป็น x < low || x > high ห้ามเขียน low <= x <= high
  • ทั้งสองข้างของ && และ || ต้องเป็นเงื่อนไขเต็ม c == 'y' || c == 'Y'
  • ! ทำก่อนการเปรียบเทียบ และ && ทำก่อน || ผสมกันเมื่อไรใส่วงเล็บ
  • && กับ || หยุดเมื่อรู้คำตอบ วางเงื่อนไขกันพังไว้ซ้าย เช่น b != 0 && a % b == 0
  • ปีอธิกสุรทินคือ (y % 4 == 0 && y % 100 != 0) || y % 400 == 0 ทดสอบด้วย 2024 2023 1900 2000
  • ทศนิยมห้ามเทียบด้วย == ให้ดูว่าผลต่างอยู่ในช่วงแคบรอบ 0 หรือเลี่ยงไปใช้จำนวนเต็ม