Rẽ nhánh trong C
Ở các bài trước, chương trình chạy tuần tự: máy đọc từng câu lệnh từ trên xuống dưới, không bỏ sót cũng không lặp lại. Nhưng phần lớn bài toán thực tế cần máy biết chọn việc để làm: chỉ in chữ đậu khi điểm đủ, chỉ chia khi mẫu khác không. Nhóm cấu trúc giúp máy chọn nhánh gọi là rẽ nhánh (branching).
Hãy hình dung một máy rút tiền tự động. Trước khi nhả tiền, nó phải trả lời một chuỗi câu hỏi: thẻ có hợp lệ không, mã PIN có đúng không, số dư có đủ không, máy còn tiền mặt không. Chỉ khi mọi câu trả lời đều thuận thì tiền mới ra; sai một câu là dừng lại và báo lỗi tương ứng. Không có rẽ nhánh, chương trình chỉ biết làm đúng một kịch bản cứng và vô dụng với mọi tình huống ngoài kịch bản đó. Rẽ nhánh chính là thứ biến một dãy lệnh thành một chương trình biết phản ứng.
Bài này đi qua bốn công cụ rẽ nhánh của C, từ đơn giản tới gọn gàng: if và if-else
cho quyết định hai chiều, if lồng nhau cho quyết định nhiều tầng, bậc thang else if
cho việc chia một đại lượng thành nhiều khoảng, và switch cho việc so một biến với nhiều
giá trị cố định. Xen giữa là hai chủ đề quyết định tính đúng đắn mà người mới hay bỏ qua:
cách C tính biểu thức logic (đặc biệt là chuyện nó dừng sớm khi đã biết kết quả) và những
cái bẫy cú pháp rất nhỏ nhưng gây lỗi rất lớn.
Thử ngay: xem bậc thang else if chạy từng bước
Bạn chưa cần hiểu cú pháp, cứ bấm ▶ Chạy (hoặc kéo thanh trượt) và nhìn hiện tượng
trước. Đoạn mã dưới đây xếp loại học lực với biến score đang giữ giá trị 7.2: máy thử
lần lượt từng điều kiện từ trên xuống rồi dừng lại ở nhánh đúng đầu tiên. Cách viết này
gọi là bậc thang else if, và các mục bên dưới sẽ đi từ if đơn giản nhất cho tới nó.
1#include <stdio.h>2int main(void) {3 float score = 7.2;4 if (score < 0 || score > 10) {5 printf("Invalid score.\n");6 } else if (score >= 8.0) {7 printf("Grade: Excellent.\n");8 } else if (score >= 6.5) {9 printf("Grade: Good.\n");10 } else if (score >= 5.0) {11 printf("Grade: Average.\n");12 } else {13 printf("Grade: Weak.\n");14 }15 return 0;16}
Hãy chú ý: dù score 7.2 cũng thỏa cả score >= 5.0, nhánh đó không bao giờ được xét vì
máy đã dừng ở nhánh đúng đầu tiên. Đó là lý do thứ tự ngưỡng phải xếp từ cao xuống thấp.
Khối lệnh và điều kiện
Khối lệnh (block) là một nhóm câu lệnh đặt giữa cặp ngoặc nhọn { }. Với máy, cả
khối được xem như một câu lệnh gộp. Khi một cấu trúc điều khiển cần chạy nhiều câu lệnh,
ta gói chúng vào một khối.
Điều kiện là một biểu thức cho kết quả đúng hoặc sai. C quy ước số 0 là sai, mọi
giá trị khác 0 là đúng. Vì vậy phép so sánh a > b trả về 1 khi đúng và 0 khi sai.
Chi tiết vừa nói không phải chuyện vụn vặt, nó là gốc rễ của nhiều lỗi. C nguyên thuỷ
không có kiểu luận lý riêng: đúng và sai chỉ là hai vùng giá trị của số nguyên. Từ chuẩn
C99 mới có kiểu _Bool, và nếu thêm #include <stdbool.h> thì viết được bool, true,
false cho dễ đọc; nhưng bên dưới bool vẫn là một kiểu số nguyên, và mọi cấu trúc điều
khiển vẫn chỉ hỏi "khác 0 hay bằng 0". Hệ quả là bất kỳ biểu thức nào cho ra một con số
đều đặt được vào chỗ điều kiện,
kể cả những biểu thức bạn không hề định dùng làm điều kiện. if (n) là hợp lệ và có nghĩa
"nếu n khác không"; if (n % 2) có nghĩa "nếu n lẻ". Cách viết này gọn và người viết C
lâu năm rất hay dùng. Nhưng chính sự dễ dãi đó khiến một lỗi đánh máy như if (x = 5) vẫn
biên dịch trôi chảy thay vì bị chặn lại.
= với ==Câu if (x = 5) gán 5 cho x rồi lấy kết quả 5 (khác 0, tức luôn đúng), nên nhánh
luôn chạy. Muốn so sánh phải viết if (x == 5). Đây là lỗi kinh điển của người mới học C.
Biểu thức logic và tính dừng sớm
Ba toán tử logic của C là "và", "hoặc" và "phủ định". Phép "và" chỉ đúng khi cả hai vế đúng, phép "hoặc" đúng khi ít nhất một vế đúng, còn phép phủ định đảo đúng thành sai và ngược lại. Đến đây thì giống toán học phổ thông. Nhưng C thêm một luật mà toán học không có, và luật ấy quan trọng tới mức bỏ qua nó là viết ra chương trình sập.
Luật đó gọi là dừng sớm (short-circuit). Khi tính một phép "và", C tính vế trái trước; nếu vế trái đã sai thì cả biểu thức chắc chắn sai, nên C không thèm tính vế phải nữa. Tương tự với phép "hoặc": vế trái đã đúng thì cả biểu thức đúng, vế phải bị bỏ qua. Nói cách khác, thứ tự các vế trong biểu thức logic của C là thứ tự có ý nghĩa, không phải thứ tự tuỳ ý.
Vì sao điều này đáng quan tâm? Vì nó cho ta viết những biểu thức mà nếu tính đủ cả hai vế thì sẽ hỏng. Ví dụ kinh điển là kiểm tra mẫu số trước khi chia.
#include <stdio.h>
int main(void) {
int a = 10, b = 0;
if (b != 0 && a / b > 2) { // b != 0 fails, so a / b is never evaluated
printf("Ket qua lon hon 2.\n");
} else {
printf("Khong chia duoc hoac ket qua khong lon hon 2.\n");
}
return 0;
}
Với b bằng 0, vế trái b != 0 sai, nên C bỏ hẳn vế phải và phép chia cho 0 không bao
giờ xảy ra. Nếu đảo lại thành a / b > 2 && b != 0 thì máy tính vế trái trước, chia cho 0
ngay lập tức và chương trình sập, dù cái kiểm tra bảo vệ vẫn nằm chình ình ở vế phải. Bài
học rút ra: trong một biểu thức "và", hãy đặt điều kiện bảo vệ ở bên trái và điều kiện
sử dụng ở bên phải. Cùng khuôn mẫu này còn dùng để kiểm tra con trỏ khác NULL trước
khi đọc qua nó, hoặc kiểm tra chỉ số nằm trong mảng trước khi truy cập phần tử.
Câu lệnh if
Dạng đơn giản nhất: nếu điều kiện đúng thì chạy thân lệnh, nếu sai thì bỏ qua.
#include <stdio.h>
int main(void) {
int age = 20;
if (age >= 18) { // condition is true when age is at least 18
printf("You are an adult.\n");
}
printf("End of program.\n"); // this line always runs
return 0;
}
Với age bằng 20, điều kiện age >= 18 đúng nên cả hai dòng được in. Nếu age bằng
15, khối if bị bỏ qua, chỉ còn dòng cuối chạy. Lưu ý dòng End of program nằm ngoài
khối if nên luôn chạy.
Ngoặc nhọn ở đây có vẻ thừa vì thân if chỉ có một câu lệnh, và C cho phép bỏ chúng. Đừng
bỏ. Khi thân chỉ có một lệnh mà không có ngoặc nhọn, if chỉ nhận đúng câu lệnh đầu tiên
làm thân, còn những câu lệnh viết thụt vào bên dưới trông như cùng một khối nhưng thực ra
nằm ngoài if và luôn chạy. Bạn thấy mã thụt lề đẹp đẽ nhưng máy hiểu hoàn toàn khác, và
kiểu lỗi này rất khó phát hiện vì mắt người tin vào thụt lề còn trình biên dịch thì không.
Thói quen luôn viết ngoặc nhọn, kể cả cho một dòng, loại bỏ hẳn nhóm lỗi này.
Câu if (x > 0); có một dấu chấm phẩy ngay sau ngoặc đơn. Trình biên dịch không báo lỗi,
vì dấu chấm phẩy đơn độc là một câu lệnh rỗng hoàn toàn hợp lệ, và nó trở thành thân của
if. Kết quả là if kiểm tra điều kiện rồi chạy câu lệnh rỗng, tức là không làm gì cả.
Còn khối ngoặc nhọn viết bên dưới thì không còn thuộc về if nữa: nó là một khối độc lập
và luôn luôn chạy, bất kể điều kiện đúng hay sai. Nguyên nhân gốc là ta quen kết thúc mọi
câu lệnh bằng dấu chấm phẩy, mà if lại không phải câu lệnh kết thúc ở đó. Cùng cái bẫy ấy
lặp lại với for và while. Khi một nhánh "chạy cả khi điều kiện sai", hãy soi ngay dấu
chấm phẩy sau ngoặc đơn.
Câu lệnh if-else
Thường ta muốn làm việc này hoặc việc kia: đúng thì làm A, sai thì làm B.
#include <stdio.h>
int main(void) {
int n = 7;
if (n % 2 == 0) { // remainder 0 means even
printf("%d is even.\n", n);
} else {
printf("%d is odd.\n", n);
}
return 0;
}
Phép % lấy phần dư của phép chia nguyên. Số chia hết cho 2 có dư 0 nên là số chẵn;
ngược lại là số lẻ. Hai nhánh loại trừ nhau: với mỗi lần chạy, đúng một nhánh được thực hiện.
Câu lệnh if lồng nhau
Bên trong một nhánh if hoặc else, ta lại có thể đặt một if khác, gọi là if lồng
nhau (nested if). Cách này dùng khi một quyết định chỉ có nghĩa sau khi một quyết định
trước đã xác định.
#include <stdio.h>
int main(void) {
int a = 3, b = 4, c = 5;
if (a > 0 && b > 0 && c > 0) { // outer check: all sides positive
if (a + b > c && a + c > b && b + c > a) { // inner check
printf("Valid triangle.\n");
} else {
printf("Positive numbers but not a triangle.\n");
}
} else {
printf("Side lengths must be positive.\n");
}
return 0;
}
if ngoài chỉ kiểm tra tính dương. Chỉ khi nó đúng, máy mới xét tới if trong. Nhờ lồng
nhau, ta tách bài toán thành hai tầng câu hỏi rõ ràng.
Bậc thang else if
Khi một đại lượng cần chia thành nhiều khoảng, ta dùng chuỗi if ... else if ... else,
gọi là bậc thang (else-if ladder). Máy thử lần lượt từng điều kiện từ trên xuống; gặp
điều kiện đúng đầu tiên thì chạy nhánh đó rồi bỏ qua toàn bộ phần còn lại.
#include <stdio.h>
int main(void) {
float score = 7.2;
if (score < 0 || score > 10) { // guard against invalid input
printf("Invalid score.\n");
} else if (score >= 8.0) {
printf("Grade: Excellent.\n");
} else if (score >= 6.5) {
printf("Grade: Good.\n");
} else if (score >= 5.0) {
printf("Grade: Average.\n");
} else {
printf("Grade: Weak.\n");
}
return 0;
}
Thứ tự các điều kiện rất quan trọng. Vì máy dừng ở nhánh đúng đầu tiên, ta phải xếp từ
ngưỡng cao xuống thấp. Nếu đặt score >= 5.0 lên trước score >= 8.0 thì điểm 9 cũng bị
bắt ở nhánh trung bình và không bao giờ tới được nhánh giỏi.
Hãy để ý cách bậc thang tiết kiệm công cho ta. Nhánh thứ hai chỉ viết score >= 8.0 chứ
không phải score >= 8.0 && score <= 10, và nhánh thứ ba chỉ viết score >= 6.5 chứ không
phải score >= 6.5 && score < 8.0. Sở dĩ viết ngắn được là vì khi máy tới được một nhánh
nào đó, mọi điều kiện phía trên nó chắc chắn đã sai rồi. Tới được nhánh score >= 6.5
nghĩa là score < 8.0 đã được bảo đảm, khỏi cần kiểm tra lại. Bậc thang tự động tích luỹ
những phủ định ấy, đó chính là lý do nó gọn hơn một loạt if rời rạc.
Điều này cũng nói cho ta biết khi nào không nên dùng bậc thang. Nếu các điều kiện của bạn
không loại trừ nhau, tức là nhiều điều kiện có thể cùng đúng và bạn muốn xử lý tất cả, thì
bậc thang là sai công cụ vì nó chỉ chạy đúng một nhánh. Lúc đó phải viết nhiều if độc lập
nối tiếp nhau. Nhìn thấy bậc thang là phải nghĩ ngay "đây là các trường hợp loại trừ nhau,
đúng một trường hợp sẽ xảy ra".
Nhánh else cuối cùng cũng đáng nói. Nó là lưới an toàn hứng mọi giá trị không rơi vào
nhánh nào ở trên. Bỏ else đi thì với một số dữ liệu đầu vào, chương trình lặng lẽ không
làm gì và không in gì, mà "không in gì" là triệu chứng cực kỳ khó truy nguyên. Cứ viết
else để mọi đầu vào đều có một câu trả lời, dù câu trả lời chỉ là một dòng báo lỗi.
Toán tử ba ngôi
C có cách viết gọn cho if-else đơn giản: toán tử ba ngôi (ternary operator), cú pháp
dieu_kien ? gia_tri_1 : gia_tri_2. Nếu điều kiện đúng, cả biểu thức nhận gia_tri_1,
ngược lại nhận gia_tri_2.
#include <stdio.h>
int main(void) {
int a = 7, b = 12;
int max = (a > b) ? a : b; // pick a if a > b, otherwise b
printf("Max = %d\n", max);
return 0;
}
Câu này tương đương khối if (a > b) max = a; else max = b; nhưng ngắn hơn. Đừng lồng
nhiều tầng toán tử ba ngôi vì sẽ khó đọc; khi logic phức tạp, hãy quay lại if-else.
Khác biệt cốt lõi giữa toán tử ba ngôi và if-else không nằm ở độ dài mà nằm ở bản chất:
if-else là một câu lệnh, còn toán tử ba ngôi là một biểu thức, tức là nó có giá trị.
Vì có giá trị nên nó đặt được vào những chỗ mà if-else không đặt vào nổi: bên phải một
phép gán, bên trong một lời gọi printf, hay ngay khi khởi tạo một hằng. Đó là lý do tồn
tại thật sự của nó, chứ không phải để tiết kiệm vài dòng. Đổi lại, thân của nó chỉ chứa
được một biểu thức chứ không chứa được cả một khối lệnh, nên khi mỗi nhánh cần làm nhiều
việc thì không có lựa chọn nào khác ngoài if-else.
Câu lệnh switch
Khi cần so sánh một biến nguyên hoặc ký tự với nhiều giá trị cố định, switch gọn và
rõ hơn bậc thang else if. Máy tính giá trị trong switch (...) rồi nhảy tới case có
nhãn trùng; mỗi nhãn case kết thúc bằng break để thoát khỏi switch; nhãn default
chạy khi không case nào khớp.
breakQuên break gây hiện tượng rơi xuyên (fall-through): sau khi chạy một case, máy
chạy luôn các case bên dưới cho tới khi gặp break hoặc hết switch. Đôi khi ta cố ý
gộp nhãn, nhưng đa số trường hợp thiếu break là lỗi.
Rơi xuyên không phải một lỗi thiết kế của C mà là hệ quả trực tiếp của cách switch làm
việc. Bậc thang else if thử lần lượt từng điều kiện, còn switch thì nhảy thẳng
tới nhãn khớp rồi chạy tiếp từ đó xuống dưới như thể các nhãn chỉ là điểm đánh dấu trên
một dải mã liền mạch. Hiểu vậy thì rơi xuyên hết bí ẩn: máy đã nhảy vào giữa dải mã và
không có gì bảo nó dừng, nên nó cứ chạy. break chính là cái bảo nó dừng.
Từ cơ chế nhảy đó suy ra luôn giới hạn của switch. Nhãn case phải là một hằng nguyên
biết được lúc biên dịch, vì trình biên dịch cần dựng sẵn bảng nhảy trước khi chương trình
chạy. Bạn không viết được case x > 5: hay case n: với n là biến, cũng không dùng
switch trên số thực hay chuỗi. Bù lại, khi số nhãn nhiều, switch nhanh hơn bậc thang vì
nó không phải thử từng điều kiện một mà nhảy một phát tới đích.
Cũng vì thế mà việc cố ý gộp nhãn lại trở nên tự nhiên: viết vài case liền nhau không
có break ở giữa để chúng cùng dùng một thân, chẳng hạn gộp case 'a' với case 'e' và
các nguyên âm khác vào chung một xử lý. Khi cố ý làm vậy, hãy thêm một dòng chú thích nói
rõ đây là rơi xuyên có chủ đích, để người đọc sau không tưởng bạn quên break.
So sánh số thực, cái bẫy im lặng
Mọi thứ tới giờ đều chạy tốt với số nguyên. Với số thực thì có một cái bẫy không báo lỗi, không cảnh báo, chỉ lặng lẽ cho ra kết quả sai.
#include <stdio.h>
#include <math.h>
int main(void) {
double x = 0.1 + 0.2;
if (x == 0.3) { // almost never true
printf("Bang nhau.\n");
} else {
printf("Khong bang nhau! x = %.20f\n", x);
}
if (fabs(x - 0.3) < 1e-9) { // compare with a tolerance
printf("Coi nhu bang nhau.\n");
}
return 0;
}
Chương trình in ra rằng 0.1 + 0.2 không bằng 0.3. Nguyên nhân không phải lỗi của máy
mà là giới hạn vật lý của cách lưu số thực. Máy lưu số theo hệ nhị phân với số bit hữu hạn,
mà 0.1 trong hệ nhị phân là một số thập phân vô hạn tuần hoàn, hệt như một phần ba trong
hệ mười. Không đủ chỗ để lưu vô hạn chữ số nên máy phải cắt bớt, và mỗi lần cắt là một sai
số cực nhỏ. Cộng hai số đã sai lệch thì kết quả lệch thêm chút nữa, đủ để phép so sánh bằng
tuyệt đối thất bại.
Cách chữa là đừng bao giờ hỏi hai số thực có bằng nhau đúng từng bit hay không, mà hỏi
chúng có đủ gần nhau hay không. Ta lấy trị tuyệt đối của hiệu rồi so với một ngưỡng sai
số rất nhỏ, thường gọi là epsilon. Hàm fabs nằm trong math.h cho trị tuyệt đối của số
thực. Riêng phép so sánh lớn hơn hoặc nhỏ hơn giữa hai số thực thì vẫn dùng bình thường,
vì sai số cực nhỏ hiếm khi làm đảo ngược một bất đẳng thức thực sự.
Bốn lỗi dưới đây có chung một nguyên nhân gốc: C chấp nhận biên dịch chúng nên bạn không hề được cảnh báo, và chương trình vẫn chạy, chỉ là chạy sai.
| Lỗi | Máy hiểu thành gì | Cách tránh |
|---|---|---|
if (x = 5) | gán 5 cho x rồi coi 5 là đúng, nhánh luôn chạy | viết if (5 == x) để trình biên dịch chặn khi gõ nhầm |
if (x > 0); | thân của if là câu lệnh rỗng, khối bên dưới luôn chạy | không gõ dấu chấm phẩy sau ngoặc đơn của if |
if (a == 0.3) | so sánh bằng tuyệt đối trên số thực, gần như luôn sai | so bằng ngưỡng sai số với fabs(a - 0.3) < 1e-9 |
thiếu break trong case | rơi xuyên xuống các case sau, in thừa | mỗi case một break, trừ khi cố ý gộp và có chú thích |
Bài tập thực hành
Bài tập 1: năm nhuận
Viết chương trình nhập một năm rồi cho biết năm đó có phải năm nhuận không. Năm nhuận là năm chia hết cho 4 nhưng không chia hết cho 100, hoặc chia hết cho 400.
Lời giải chi tiết
Gộp toàn bộ luật thành một biểu thức luận lý rồi đưa vào một if-else. Điều kiện năm
nhuận là (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0). Dấu ngoặc tách hai
vế của phép hoặc cho rõ ràng.
#include <stdio.h>
int main(void) {
int year;
printf("Enter a year: ");
scanf("%d", &year);
if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)) {
printf("%d is a leap year.\n", year);
} else {
printf("%d is not a leap year.\n", year);
}
return 0;
}
Ý chính: khi luật phức tạp, hãy gói nó thành một biểu thức điều kiện duy nhất bằng các
toán tử logic thay vì lồng nhiều if.
Bài tập 2: số lớn nhất trong ba số
Nhập ba số thực a, b, c. Dùng if lồng nhau để tìm và in ra số lớn nhất.
Lời giải chi tiết
Tầng ngoài so a với b để chọn ứng viên lớn hơn, tầng trong so ứng viên đó với c.
Mỗi tầng chỉ trả lời một câu hỏi nên dễ kiểm tra.
#include <stdio.h>
int main(void) {
float a, b, c;
printf("Enter three numbers: ");
scanf("%f %f %f", &a, &b, &c);
if (a >= b) {
if (a >= c) {
printf("Max = %.2f\n", a);
} else {
printf("Max = %.2f\n", c);
}
} else {
if (b >= c) {
printf("Max = %.2f\n", b);
} else {
printf("Max = %.2f\n", c);
}
}
return 0;
}
Ý chính: if lồng nhau tách bài toán nhiều điều kiện thành các quyết định nối tiếp, mỗi
quyết định chỉ dựa trên kết quả của quyết định trước.
Bài tập 3: máy tính bỏ túi bằng switch
Nhập hai số và một ký tự phép toán trong nhóm + - * /, in kết quả tương ứng. Nhớ xử lý
trường hợp chia cho 0 ở nhánh /.
Lời giải chi tiết
Mỗi phép toán là một case riêng, kết thúc bằng break để khỏi rơi xuyên. Nhánh /
kiểm tra mẫu trước khi chia; default bắt ký tự phép toán lạ.
#include <stdio.h>
int main(void) {
double x, y;
char op;
printf("Enter: number op number (op in + - * /): ");
scanf("%lf %c %lf", &x, &op, &y);
switch (op) {
case '+':
printf("%.2f\n", x + y);
break;
case '-':
printf("%.2f\n", x - y);
break;
case '*':
printf("%.2f\n", x * y);
break;
case '/':
if (y == 0) {
printf("Cannot divide by zero.\n");
} else {
printf("%.2f\n", x / y);
}
break;
default:
printf("Unknown operator.\n");
}
return 0;
}
Một câu hỏi hợp lý: mục trên nói không được so sánh bằng giữa hai số thực, sao ở đây lại
viết y == 0? Vì đây không phải phép so sánh hai kết quả tính toán mà là một lá chắn cho
phép chia: thứ ta cần chặn đúng là giá trị 0 tuyệt đối, giá trị duy nhất khiến phép chia
vô nghĩa. Nếu muốn chặn cả những mẫu số bé tới mức kết quả vô dụng thì đổi thành
fabs(y) < 1e-9 và thêm math.h.
Ý chính: switch so một biến với nhiều giá trị cố định rất gọn, nhưng mỗi case phải có
break, và những kiểm tra phụ như chia cho 0 vẫn đặt được bên trong một case.
Bài tập 4: xét dấu và tính căn an toàn
Nhập một số thực x. Nếu x âm thì báo không tính được căn bậc hai; nếu x bằng 0 (hiểu
theo nghĩa đủ gần 0) thì báo căn bằng 0; nếu x dương thì in căn bậc hai. Bài này ép bạn
dùng cả so sánh số thực có ngưỡng sai số lẫn bậc thang.
Lời giải chi tiết
Điểm mấu chốt là không được viết if (x == 0) với số thực. Ta coi x là 0 khi trị tuyệt
đối của nó nhỏ hơn một ngưỡng rất nhỏ. Thứ tự trong bậc thang cũng quan trọng: phải bắt
trường hợp âm trước, vì nếu để sqrt chạy trên số âm thì kết quả là một giá trị vô nghĩa
chứ chương trình không sập, và đó là kiểu sai lặng lẽ khó tìm nhất.
#include <stdio.h>
#include <math.h>
int main(void) {
double x;
printf("Nhap x: ");
scanf("%lf", &x);
if (x < -1e-9) { // clearly negative
printf("Khong tinh duoc can bac hai cua so am.\n");
} else if (fabs(x) < 1e-9) { // close enough to zero
printf("Can bac hai = 0\n");
} else { // clearly positive
printf("Can bac hai = %.4f\n", sqrt(x));
}
return 0;
}
Ba nhánh phủ kín mọi giá trị đầu vào và loại trừ nhau đôi một, đúng khuôn mẫu của một bậc
thang tốt. Nhớ biên dịch kèm thư viện toán khi cần, chẳng hạn thêm -lm với gcc.
Ý chính: với số thực, mọi phép so sánh bằng đều phải đổi thành so sánh với ngưỡng sai số.
Bài tập 5: đếm ngày trong tháng bằng switch có gộp nhãn
Nhập một tháng từ 1 đến 12 và một năm, in số ngày của tháng đó. Dùng switch và cố ý gộp
nhãn cho các tháng có cùng số ngày. Tháng hai phụ thuộc năm nhuận.
Lời giải chi tiết
Đây là chỗ rơi xuyên trở thành công cụ chứ không còn là lỗi. Bốn tháng 30 ngày dùng chung
một thân nên ta viết bốn nhãn case liền nhau, không break ở giữa, và chỉ đặt break
sau thân chung. Tháng hai tách riêng vì nó cần kiểm tra năm nhuận, và ta tái dùng biểu thức
năm nhuận đã dựng ở bài tập 1. Nhánh default bắt tháng không hợp lệ, giữ đúng nguyên tắc
mọi đầu vào đều phải có một câu trả lời.
#include <stdio.h>
int main(void) {
int thang, nam, so_ngay;
printf("Nhap thang va nam: ");
scanf("%d %d", &thang, &nam);
switch (thang) {
case 4: case 6: case 9: case 11: // intentional fall-through
so_ngay = 30;
break;
case 2: {
int nhuan = (nam % 4 == 0 && nam % 100 != 0) || (nam % 400 == 0);
so_ngay = nhuan ? 29 : 28; // ternary fits: one value either way
break;
}
case 1: case 3: case 5: case 7: // intentional fall-through
case 8: case 10: case 12:
so_ngay = 31;
break;
default:
printf("Thang khong hop le.\n");
return 1;
}
printf("Thang %d nam %d co %d ngay.\n", thang, nam, so_ngay);
return 0;
}
Chú ý hai điểm kỹ thuật. Thứ nhất, case 2 cần khai báo một biến nên ta bọc thân nó trong
ngoặc nhọn; không có ngoặc nhọn thì biến khai báo trong một case sẽ vướng phạm vi với các
case khác. Thứ hai, toán tử ba ngôi ở đây rất hợp vì mỗi nhánh chỉ cho ra một giá trị.
Ý chính: gộp nhãn case là cách dùng rơi xuyên có chủ đích, gọn hơn hẳn một bậc thang dài.
Tự kiểm tra
1) Trong bậc thang else if với score bằng 7.2, vì sao nhánh score >= 5.0 không chạy?
Vì bậc thang chỉ chạy đúng một nhánh: nhánh đúng đầu tiên. Máy gặp score >= 6.5 đã
đúng nên chạy nhánh đó rồi bỏ qua toàn bộ phần còn lại, kể cả score >= 5.0 dù điều kiện
này cũng đúng.
2) Trong switch, nếu quên break ở một case thì điều gì xảy ra?
Xảy ra rơi xuyên: sau khi chạy case đó, máy chạy luôn các case bên dưới cho tới
khi gặp break hoặc hết switch. Kết quả thường là in thừa, là lỗi hay gặp của người mới.
Câu hỏi tự kiểm
- 1Câu if (x = 5) làm gì?
- 2Với b bằng 0, câu if (b != 0 && a / b > 2) hành xử ra sao?
- 3Cách đúng để kiểm tra hai số thực x và y có coi như bằng nhau không?
- 4Nhãn case trong switch phải là loại giá trị nào?
Tóm tắt
Rẽ nhánh cho chương trình khả năng phản ứng thay vì chỉ chạy một kịch bản cứng. C cung cấp
bốn công cụ và mỗi công cụ hợp với một hình dạng bài toán khác nhau: if-else cho quyết
định hai chiều, if lồng nhau khi một câu hỏi chỉ có nghĩa sau khi câu hỏi trước đã trả
lời, bậc thang else if khi cần chia một đại lượng liên tục thành nhiều khoảng loại trừ
nhau, và switch khi cần so một biến nguyên với nhiều hằng cố định.
Ba cơ chế bên dưới quyết định chương trình đúng hay sai. Một là điều kiện trong C chỉ là
một con số, 0 là sai và mọi giá trị khác là đúng, nên nhiều lỗi đánh máy vẫn biên dịch
trôi. Hai là biểu thức logic dừng sớm, nhờ đó ta đặt điều kiện bảo vệ ở bên trái để chặn
những phép tính nguy hiểm ở bên phải. Ba là bậc thang chỉ chạy đúng một nhánh, nhánh đúng
đầu tiên, nên thứ tự các ngưỡng phải xếp từ cao xuống thấp.
Những lỗi hay gặp nhất đều thuộc loại lặng lẽ: gán nhầm = thay vì ==, dấu chấm phẩy lạc
biến thân if thành câu lệnh rỗng, quên break gây rơi xuyên, và so sánh bằng tuyệt đối
giữa hai số thực. Không cái nào bị trình biên dịch chặn lại, nên cách phòng vệ duy nhất là
biết trước chúng và soi đúng chỗ khi kết quả không như mong đợi.
Rẽ nhánh cho máy khả năng chọn việc để làm. if-else chọn giữa hai khả năng, bậc thang
else if chia thành nhiều khoảng và chỉ chạy nhánh đúng đầu tiên, còn switch so một
biến với nhiều giá trị cố định, nhớ break để tránh rơi xuyên.