Bỏ qua điều hướng, tới nội dung chính
Học C
Bài 5.519 phút đọc

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

menu.c
#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ộcVì sao
Biểu thức phải là kiểu nguyênint, char, enum được. float, double, chuỗi thì không
Nhãn case phải là hằng số biên dịchKhô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ảngKhông viết được case lớn hơn 5
Nhãn case không có phạm vi riêngCả 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.

Case 2 thiếu break nên luồng chạy tiếp xuống case 3, in ra cả hai.
Fallthrough do quên
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. */
Fallthrough có chủ ý
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. */
terminal
# Bật cảnh báo cho fallthrough không được đánh dấu
gcc -Wall -Wextra -Wimplicit-fallthrough bai.c -o bai
bai.c:8:9: warning: this statement may fall through

#Khai báo biến trong case

Cả khối switchmộ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.

Không nên
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    */
Nên
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úcSố phép so sánhĐộ phức tạp
Chuỗi else if với n nhánhTrung bình n chia 2O(n)
switch sinh ra bảng nhảy0O(1)
switch với nhãn thưa thớtTra nhị phânO(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ốngChọn
So sánh bằng với nhiều hằng số nguyênswitch
Xử lý các giá trị của một enumswitch
Máy trạng tháiswitch
Xử lý ký tự lệnh trong trình phân tích cú phápswitch
So sánh khoảng giá trịelse if
Điều kiện phức tạp có nhiều biếnelse if
So sánh chuỗielse if với strcmp
So sánh số thựcelse 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ử

  1. 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ệ.
  2. 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.
  3. Tái hiện lỗi quên break, biên dịch với -Wimplicit-fallthrough và đọc cảnh báo.
  4. 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.
  5. Viết một switch mười nhánh liên tiếp và một chuỗi else if tương đương, so sánh assembly sinh ra ở mức -O2.
  6. Viết hàm chuyển enum thành chuỗi không dùng default, 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 int không dùng làm nhãn case được, phải dùng #define hoặc enum.
  • 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ỏ default khi switch trên enum để trình biên dịch nhắc bạn khi thêm giá trị mới.