switch
Sau bài này bạn sẽ làm được
- Kể được năm ràng buộc của switch
- Phân biệt fallthrough có chủ ý với fallthrough do quên break
- Giải thích vì sao switch có thể nhanh hơn chuỗi else if
- Khai báo biến trong case đúng cách
switch so sánh một giá trị với nhiều hằng số. Nó dễ đọc hơn chuỗi else if và có thể nhanh hơn nhiều lần. Đổi lại, nó có năm ràng buộc nghiêm ngặt và một hành vi mặc định gây bất ngờ cho hầu hết người mới học.
#Cú pháp
#include <stdio.h>
int main(void)
{
int lua_chon = 2;
switch (lua_chon) {
case 1:
printf("Thêm mới\n");
break;
case 2:
printf("Xóa\n");
break;
case 3:
printf("Sửa\n");
break;
default:
printf("Lựa chọn không hợp lệ\n");
break;
}
return 0;
}Giá trị trong ngoặc được so sánh lần lượt với từng nhãn case. Khi khớp, luồng nhảy tới đó và chạy tiếp cho tới khi gặp break hoặc hết khối switch. Nhãn default chạy khi không nhãn nào khớp.
#Năm ràng buộc
| Ràng buộc | Vì sao |
|---|---|
| Biểu thức phải là kiểu nguyên | int, char, enum được. float, double, chuỗi thì không |
| Nhãn case phải là hằng số biên dịch | Không dùng biến, không dùng kết quả lời gọi hàm |
| Không được có hai case trùng giá trị | Trình biên dịch không biết chọn nhánh nào |
| Chỉ so sánh bằng, không so sánh khoảng | Không viết được case lớn hơn 5 |
| Nhãn case không có phạm vi riêng | Cả khối switch dùng chung một phạm vi |
switch (diem) { /* LỖI nếu diem là double */
case 8.5: break; /* LỖI: không phải hằng nguyên */
}
switch (ten) { /* LỖI: không so sánh chuỗi được */
case "abc": break;
}
int x = 5;
switch (n) {
case x: break; /* LỖI: x là biến, không phải hằng */
}
const int MAX = 100;
switch (n) {
case MAX: break; /* LỖI trong C: const không phải hằng nguyên */
}
#define MAX2 100
enum { MAX3 = 100 };
switch (n) {
case MAX2: break; /* đúng */
case MAX3: break; /* LỖI: trùng giá trị với MAX2 */
}#Fallthrough
Đây là hành vi gây bất ngờ nhất của switch. Khi luồng đã nhảy vào một case, nó chạy tiếp xuống các case bên dưới cho tới khi gặp break. Nhãn case chỉ là điểm nhảy vào, không phải ranh giới của khối.
switch (x) {
case 1:
printf("Một\n"); /* quên break */
case 2:
printf("Hai\n");
break;
}
/* Với x bằng 1, in ra CẢ HAI dòng. */switch (c) {
case 'a':
case 'e':
case 'i':
case 'o':
case 'u':
printf("Nguyên âm\n");
break;
default:
printf("Phụ âm\n");
break;
}
/* Nhiều nhãn dùng chung một khối lệnh. */bai.c:8:9: warning: this statement may fall through
#Khai báo biến trong case
Cả khối switch là một phạm vi duy nhất, nhãn case không tạo phạm vi mới. Điều này gây ra một lỗi biên dịch khó hiểu.
switch (x) {
case 1:
int y = 5; /* LỖI biên dịch */
printf("%d\n", y);
break;
case 2:
break;
}
/* Thông báo lỗi:
a label can only be part of a statement
and a declaration is not a statement */switch (x) {
case 1: {
int y = 5; /* ngoặc nhọn tạo phạm vi riêng */
printf("%d\n", y);
break;
}
case 2:
break;
}#Bảng nhảy
Với chuỗi else if, trình biên dịch phải sinh ra một loạt phép so sánh tuần tự. Với switch có nhiều nhãn liên tiếp, nó có thể sinh ra bảng nhảy: một mảng địa chỉ, và luồng nhảy thẳng tới đúng nhánh chỉ bằng một phép tra bảng.
| Cấu trúc | Số phép so sánh | Độ phức tạp |
|---|---|---|
| Chuỗi else if với n nhánh | Trung bình n chia 2 | O(n) |
| switch sinh ra bảng nhảy | 0 | O(1) |
| switch với nhãn thưa thớt | Tra nhị phân | O(log n) |
Trình biên dịch tự quyết định có sinh bảng nhảy hay không, dựa trên số lượng nhãn và độ thưa của chúng. Bạn không điều khiển được, nhưng biết điều này giúp hiểu vì sao switch được ưa dùng trong các vòng lặp nóng, ví dụ trong trình thông dịch mã máy ảo.
#Khi nào dùng switch
| Tình huống | Chọn |
|---|---|
| So sánh bằng với nhiều hằng số nguyên | switch |
| Xử lý các giá trị của một enum | switch |
| Máy trạng thái | switch |
| Xử lý ký tự lệnh trong trình phân tích cú pháp | switch |
| So sánh khoảng giá trị | else if |
| Điều kiện phức tạp có nhiều biến | else if |
| So sánh chuỗi | else if với strcmp |
| So sánh số thực | else if |
Mẹo bỏ default khi switch trên enum
typedef enum { CHO, DANG_CHAY, TAM_DUNG, KET_THUC } TrangThai;
const char *ten_trang_thai(TrangThai t)
{
switch (t) {
case CHO: return "Chờ";
case DANG_CHAY: return "Đang chạy";
case TAM_DUNG: return "Tạm dừng";
case KET_THUC: return "Kết thúc";
}
return "Không xác định";
}Tự làm thử
- Viết máy tính đơn giản nhận một ký tự phép toán và hai số, dùng
switch. Xử lý cả trường hợp chia cho 0 và toán tử không hợp lệ. - Viết chương trình nhập tháng và năm rồi in số ngày của tháng đó, dùng fallthrough có chủ ý để gom các tháng 31 ngày.
- Tái hiện lỗi quên
break, biên dịch với-Wimplicit-fallthroughvà đọc cảnh báo. - Thử khai báo biến ngay sau nhãn
case, đọc thông báo lỗi, rồi sửa bằng ngoặc nhọn. - Viết một
switchmười nhánh liên tiếp và một chuỗielse iftương đương, so sánh assembly sinh ra ở mức-O2. - Viết hàm chuyển
enumthành chuỗi không dùngdefault, rồi thêm một giá trị enum mới và quan sát cảnh báo.
Trình chấm điểm tự động sẽ được bổ sung ở giai đoạn sau. Hiện tại bạn tự chạy thử trên máy.
Tóm tắt
- switch chỉ so sánh bằng, chỉ với kiểu nguyên, và nhãn phải là hằng số biên dịch.
- Trong C,
const intkhông dùng làm nhãncaseđược, phải dùng#definehoặcenum. - Nhãn case là điểm nhảy vào, không phải ranh giới khối. Thiếu break thì rơi xuống case dưới.
- Fallthrough có chủ ý phải đánh dấu bằng chú thích hoặc
[[fallthrough]]. - Khai báo biến trong case phải bọc ngoặc nhọn để tạo phạm vi riêng.
- switch với nhiều nhãn liên tiếp có thể thành bảng nhảy, nhanh hơn chuỗi else if.
- Bỏ
defaultkhi switch trên enum để trình biên dịch nhắc bạn khi thêm giá trị mới.