Chuyển tới nội dung chính

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: ifif-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ó.

Bậc thang else if chọn nhánh theo điều kiện C
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}
Ngăn xếp stack
main()
Bộ nhớ động heap
(trống)
Bắt đầu hàm main(). Chưa có biến nào trên ngăn xếp.
1/7

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.

Đừng nhầm = 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.

Dấu chấm phẩy lạc sau if

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 forwhile. 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.

Rơi xuyên khi quên break

Quê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 rẽ nhánh hay gặp nhất

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ỗiMá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ạyviế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ạykhô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 saiso bằng ngưỡng sai số với fabs(a - 0.3) < 1e-9
thiếu break trong caserơi xuyên xuống các case sau, in thừamỗ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

Kiểm tra nhanh: rẽ nhánh if và switch0/4 đúngchưa trả lời
  1. 1Câu if (x = 5) làm gì?
  2. 2Với b bằng 0, câu if (b != 0 && a / b > 2) hành xử ra sao?
  3. 3Cách đúng để kiểm tra hai số thực x và y có coi như bằng nhau không?
  4. 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.

Ghi nhớ

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.