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

Bảng điều phối

Sau bài này bạn sẽ làm được

  • Cài bảng lệnh bằng mảng struct chứa con trỏ hàm
  • Dùng bộ khởi tạo có chỉ định để bảng không lệch với enum
  • So sánh bảng điều phối với switch trên bốn tiêu chí
  • Kiểm tra bảng đầy đủ bằng Static_assert

Một switch hai mươi nhánh là chỗ mà thêm một chức năng luôn có nghĩa là sửa một hàm đã có. Bảng điều phối đảo ngược điều đó: thêm chức năng là thêm một dòng dữ liệu, không sửa hàm nào.

#Từ switch tới bảng

Chuỗi if, và danh sách lặp lại
int thuc_thi(const char *ten, int argc, char **argv) {
    if      (strcmp(ten, "help") == 0) return cmd_help(argc, argv);
    else if (strcmp(ten, "add")  == 0) return cmd_add(argc, argv);
    else if (strcmp(ten, "del")  == 0) return cmd_del(argc, argv);
    else if (strcmp(ten, "list") == 0) return cmd_list(argc, argv);
    else if (strcmp(ten, "quit") == 0) return cmd_quit(argc, argv);

    fprintf(stderr, "Lenh khong ton tai: %s\n", ten);

    return -1;
}

/* Va o mot cho khac, mot ham nua liet ke lai het: */
void in_tro_giup(void) {
    puts("  help  Hien thi tro giup");
    puts("  add   Them ban ghi");
    puts("  del   Xoa ban ghi");
    puts("  list  Liet ke");
    puts("  quit  Thoat");
}
Một bảng dữ liệu
typedef struct {
    const char *ten;
    int       (*ham)(int argc, char **argv);
    const char *mo_ta;
} Lenh;

static const Lenh BANG_LENH[] = {
    { "help", cmd_help, "Hien thi tro giup" },
    { "add",  cmd_add,  "Them ban ghi"      },
    { "del",  cmd_del,  "Xoa ban ghi"       },
    { "list", cmd_list, "Liet ke"           },
    { "quit", cmd_quit, "Thoat"             },
};

#define SO_LENH (sizeof BANG_LENH / sizeof BANG_LENH[0])

int thuc_thi(const char *ten, int argc, char **argv) {
    for (size_t i = 0; i < SO_LENH; ++i)
        if (strcmp(BANG_LENH[i].ten, ten) == 0)
            return BANG_LENH[i].ham(argc, argv);

    fprintf(stderr, "Lenh khong ton tai: %s\n", ten);

    return -1;
}

void in_tro_giup(void) {
    for (size_t i = 0; i < SO_LENH; ++i)
        printf("  %-6s %s\n", BANG_LENH[i].ten, BANG_LENH[i].mo_ta);
}
Bảng là dữ liệu, không phải mã. Thêm một dòng là thêm một lệnh.

#Bảng lệnh tra theo tên

lenh.h
#ifndef LENH_H
#define LENH_H

typedef int (*HamLenh)(int argc, char **argv);

typedef struct {
    const char *ten;
    HamLenh     ham;
    const char *mo_ta;
    const char *cach_dung;
    int         so_doi_so_toi_thieu;
} Lenh;

int  lenh_thuc_thi(const char *ten, int argc, char **argv);
void lenh_in_tro_giup(void);

#endif /* LENH_H */
lenh.c
#include <stdio.h>
#include <string.h>

#include "lenh.h"

static int cmd_help(int argc, char **argv);
static int cmd_add (int argc, char **argv);
static int cmd_del (int argc, char **argv);
static int cmd_list(int argc, char **argv);

static const Lenh BANG[] = {
    { "help", cmd_help, "Hien thi tro giup", "help [lenh]",     1 },
    { "add",  cmd_add,  "Them ban ghi",      "add <ten> <tuoi>", 3 },
    { "del",  cmd_del,  "Xoa ban ghi",       "del <ma>",         2 },
    { "list", cmd_list, "Liet ke",           "list [--sap]",     1 },
};

#define SO_LENH (sizeof BANG / sizeof BANG[0])

static const Lenh *tim(const char *ten) {
    for (size_t i = 0; i < SO_LENH; ++i)
        if (strcmp(BANG[i].ten, ten) == 0) return &BANG[i];

    return NULL;
}

int lenh_thuc_thi(const char *ten, int argc, char **argv) {
    const Lenh *l = tim(ten);

    if (!l) {
        fprintf(stderr, "Lenh khong biet: %s\n", ten);
        fprintf(stderr, "Go 'help' de xem danh sach.\n");

        return -1;
    }

    if (argc < l->so_doi_so_toi_thieu) {
        fprintf(stderr, "Cach dung: %s\n", l->cach_dung);

        return -1;
    }

    return l->ham(argc, argv);
}

void lenh_in_tro_giup(void) {
    for (size_t i = 0; i < SO_LENH; ++i)
        printf("  %-6s %-22s %s\n",
               BANG[i].ten, BANG[i].cach_dung, BANG[i].mo_ta);
}

static int cmd_help(int argc, char **argv) {
    if (argc >= 2) {
        const Lenh *l = tim(argv[1]);

        if (!l) { fprintf(stderr, "Khong co lenh %s\n", argv[1]); return -1; }

        printf("%s\n%s\n", l->cach_dung, l->mo_ta);

        return 0;
    }

    lenh_in_tro_giup();

    return 0;
}
terminal
./ql help
  help   help [lenh]            Hien thi tro giup
  add    add <ten> <tuoi>      Them ban ghi
  del    del <ma>              Xoa ban ghi
  list   list [--sap]          Liet ke
./ql add
Cach dung: add <ten> <tuoi>
./ql xyz
Lenh khong biet: xyz
Go 'help' de xem danh sach.

#Bảng tra theo chỉ số

typedef enum {
    OP_CONG, OP_TRU, OP_NHAN, OP_CHIA,
    OP_SO_LUONG                          /* luon de cuoi */
} MaPhepToan;

typedef int (*PhepToan)(int, int);

static int p_cong(int a, int b) { return a + b; }
static int p_tru (int a, int b) { return a - b; }
static int p_nhan(int a, int b) { return a * b; }
static int p_chia(int a, int b) { return b ? a / b : 0; }

/* Bo khoi tao CO CHI DINH: thu tu trong mang khong phu thuoc
   thu tu ban viet, va them mot phep toan khong lam lech bang */
static const PhepToan BANG[OP_SO_LUONG] = {
    [OP_CONG] = p_cong,
    [OP_TRU]  = p_tru,
    [OP_NHAN] = p_nhan,
    [OP_CHIA] = p_chia,
};

int tinh(MaPhepToan op, int a, int b) {
    if ((unsigned)op >= OP_SO_LUONG || !BANG[op]) return 0;

    return BANG[op](a, b);
}

Khi mã không liên tục

/* Ma trang thai HTTP: 200, 301, 404, 500 ... khong lien tuc.
   Mot mang 501 phan tu de dung 4 cho la lang phi. */

typedef struct { int ma; const char *mo_ta; } MucHTTP;

static const MucHTTP BANG_HTTP[] = {
    { 200, "OK"                    },
    { 301, "Moved Permanently"     },
    { 400, "Bad Request"           },
    { 404, "Not Found"             },
    { 500, "Internal Server Error" },
};

/* Bang da sap theo ma, nen tim nhi phan duoc */
static int ss_ma(const void *k, const void *m) {
    int a = *(const int *)k, b = ((const MucHTTP *)m)->ma;

    return (a > b) - (a < b);
}

const char *http_mo_ta(int ma) {
    const MucHTTP *m = bsearch(&ma, BANG_HTTP,
                               sizeof BANG_HTTP / sizeof BANG_HTTP[0],
                               sizeof BANG_HTTP[0], ss_ma);

    return m ? m->mo_ta : "Unknown";
}

#Bảo đảm bảng đầy đủ

#include <assert.h>

static const PhepToan BANG[OP_SO_LUONG] = {
    [OP_CONG] = p_cong,
    [OP_TRU]  = p_tru,
    [OP_NHAN] = p_nhan,
    [OP_CHIA] = p_chia,
};

_Static_assert(sizeof BANG / sizeof BANG[0] == OP_SO_LUONG,
               "bang phep toan khong khop so luong enum");
Cách vừa có bảng vừa có kiểm lúc biên dịch
/* Dung X macro de sinh CA enum lan bang tu mot danh sach duy nhat.
   Bai 19.5 da gioi thieu ky thuat nay. */

#define DANH_SACH_PHEP_TOAN   \
    X(OP_CONG, p_cong, "+")   \
    X(OP_TRU,  p_tru,  "-")   \
    X(OP_NHAN, p_nhan, "*")   \
    X(OP_CHIA, p_chia, "/")

/* Sinh enum */
#define X(ma, ham, ky_hieu) ma,
typedef enum { DANH_SACH_PHEP_TOAN OP_SO_LUONG } MaPhepToan;
#undef X

/* Sinh khai bao ham */
#define X(ma, ham, ky_hieu) static int ham(int, int);
DANH_SACH_PHEP_TOAN
#undef X

/* Sinh bang ham */
#define X(ma, ham, ky_hieu) [ma] = ham,
static const PhepToan BANG[OP_SO_LUONG] = { DANH_SACH_PHEP_TOAN };
#undef X

/* Sinh bang ky hieu */
#define X(ma, ham, ky_hieu) [ma] = ky_hieu,
static const char *const KY_HIEU[OP_SO_LUONG] = { DANH_SACH_PHEP_TOAN };
#undef X

/* Them mot phep toan = them MOT dong vao DANH_SACH_PHEP_TOAN.
   Bon bang khong the lech nhau, vi chung sinh ra tu cung mot danh sach. */

#Bảng hay switch

Tiêu chíswitchBảng điều phối
Trình biên dịch bắt được thiếu nhánhCó, nếu không có default và bật -WswitchKhông, phải tự kiểm lúc chạy
Thêm một mụcSửa hàm đã cóThêm một dòng dữ liệu
Nhiều bảng song song, ví dụ tên và mô tảPhải viết nhiều switchThêm trường vào cùng struct
Nội tuyến đượcCóKhông, luôn là lời gọi gián tiếp
Đọc luồng chạyNhìn thấy hết trong một hàmPhải tra bảng mới biết đi đâu
Thay đổi lúc chạyKhôngĐược, gán lại ô trong bảng
Thân nhánh ngắn, ví dụ trả về một hằngGọn hơnRườm rà, phải viết một hàm

Tự làm thử

  1. Đổi một chuỗi if năm nhánh thành bảng lệnh, rồi thêm lệnh thứ sáu và đếm số dòng phải sửa.
  2. Thêm trường so_doi_so_toi_thieu vào bảng và bỏ đoạn kiểm tra trùng lặp khỏi từng hàm lệnh.
  3. Viết bảng theo chỉ số bằng bộ khởi tạo có chỉ định, rồi thêm một giá trị vào giữa enum và xác nhận bảng không lệch.
  4. Thêm _Static_assert kiểm kích thước bảng, rồi thử xóa một dòng khỏi enum.
  5. Viết bảng mã HTTP rời rạc và tra bằng bsearch.
  6. Sinh cả enum lẫn hai bảng bằng X macro, rồi chạy gcc -E -P để xem chúng bung ra thành gì.

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

  • Bảng điều phối biến danh sách chức năng thành dữ liệu, nên thêm một mục là thêm một dòng chứ không sửa hàm nào.
  • Nhiều bảng song song, ví dụ tên và mô tả và cách dùng, nằm trong cùng một struct nên chúng không thể lệch nhau.
  • Bảng theo chỉ số phải dùng bộ khởi tạo có chỉ định, nếu không thì thêm một giá trị vào giữa enum sẽ làm lệch mọi ô.
  • Luôn ép sang unsigned khi kiểm chỉ số, để bắt cả giá trị âm lẫn giá trị quá lớn bằng một phép so sánh.
  • Lý do chọn bảng là dễ bảo trì, không phải tốc độ. Trình biên dịch biến switch trên enum liên tục thành bảng nhảy, và bảng nhảy không có lời gọi gián tiếp.