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 แบบหลังอ่านง่ายกว่า
ตารางสรุป
| A | B | A && B | A || B | !A |
|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 1 |
| 0 | 1 | 0 | 1 | 1 |
| 1 | 0 | 0 | 1 | 0 |
| 1 | 1 | 1 | 1 | 0 |
ผลของ && || ! เป็น 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 วัน กฎตามปฏิทินสากลมีสามข้อ
- ปีที่หารด้วย 4 ลงตัว เป็นปีอธิกสุรทิน
- ยกเว้นปีที่หารด้วย 100 ลงตัว ไม่เป็น
- ยกเว้นอีกชั้น ปีที่หารด้วย 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 ปีที่ต้องลองทุกครั้งที่เขียนเรื่องนี้มีสี่ปี
| ปี | ผล | เพราะ |
|---|---|---|
| 2024 | leap | หาร 4 ลงตัว หาร 100 ไม่ลงตัว |
| 2023 | common | หาร 4 ไม่ลงตัว |
| 1900 | common | หาร 100 ลงตัว แต่หาร 400 ไม่ลงตัว |
| 2000 | leap | หาร 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 หรือเลี่ยงไปใช้จำนวนเต็ม