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

Máy trạng thái bằng con trỏ hàm

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

  • Cài máy trạng thái bằng hàm trả về trạng thái kế tiếp
  • Cài bằng con trỏ hàm trực tiếp, không qua bảng
  • Thêm hành động vào và ra cho mỗi trạng thái
  • Chọn giữa switch, bảng và con trỏ trực tiếp theo tình huống

Bài 16.3 đã cài máy trạng thái bằng switch. Bài này thêm hai cách nữa dùng con trỏ hàm, và chỉ ra chính xác khi nào chúng thắng, khi nào thua.

#Ba cách cài máy trạng thái

Máy trạng thái
Mô hình gồm một tập trạng thái hữu hạn, một trạng thái hiện tại, và các quy tắc chuyển. Mỗi sự kiện vào làm máy chạy một hành động và có thể chuyển sang trạng thái khác.
Bai toan chung cua ca bai: bo phan tich mot dong lenh.

   Vao:  "set  nguong   1200"
   Ra:   ba tu: "set", "nguong", "1200"

Trang thai:
   NGOAI_TU   dang o giua cac tu, doc khoang trang thi bo qua
   TRONG_TU   dang trong mot tu, doc ky tu thi ghi vao
   TRONG_NHAY dang trong dau nhay, khoang trang cung la ky tu thuong
CáchTrạng thái làChuyển bằngTrình biên dịch bắt thiếuBài
switchGiá trị enumGán biến trạng tháiCó, nhờ -Wswitch16.3
Bảng con trỏ hàmChỉ số vào bảngHàm trả về trạng thái kế tiếpKhôngBài này
Con trỏ hàm trực tiếpChính con trỏ hàmGán con trỏ mớiKhôngBài này
Cách một: switch, để so sánh
typedef enum { NGOAI_TU, TRONG_TU, TRONG_NHAY } TrangThai;

void tach(const char *s, void (*nhan_tu)(const char *, size_t, void *),
          void *ctx) {
    TrangThai   tt  = NGOAI_TU;
    const char *dau = NULL;

    for (const char *p = s; ; ++p) {
        switch (tt) {
            case NGOAI_TU:
                if (*p == '\0') return;
                if (*p == '"')  { tt = TRONG_NHAY; dau = p + 1; }
                else if (*p != ' ' && *p != '\t') { tt = TRONG_TU; dau = p; }
                break;

            case TRONG_TU:
                if (*p == '\0' || *p == ' ' || *p == '\t') {
                    nhan_tu(dau, (size_t)(p - dau), ctx);
                    tt = NGOAI_TU;

                    if (*p == '\0') return;
                }
                break;

            case TRONG_NHAY:
                if (*p == '\0') return;              /* nhay khong dong */

                if (*p == '"') {
                    nhan_tu(dau, (size_t)(p - dau), ctx);
                    tt = NGOAI_TU;
                }
                break;
        }
    }
}

#Hàm trả về trạng thái kế tiếp

tach-bang.c
typedef enum {
    TT_NGOAI_TU, TT_TRONG_TU, TT_TRONG_NHAY,
    TT_SO_LUONG
} TrangThai;

typedef struct {
    const char *p;               /* vi tri dang doc */
    const char *dau;             /* dau tu hien tai */
    void      (*nhan_tu)(const char *, size_t, void *);
    void       *ctx;
    int         xong;
} NgCanh;

typedef TrangThai (*HamTrangThai)(NgCanh *);

static TrangThai tt_ngoai_tu(NgCanh *c) {
    char k = *c->p;

    if (k == '\0')                 { c->xong = 1; return TT_NGOAI_TU; }
    if (k == '"')                  { c->dau = c->p + 1; ++c->p; return TT_TRONG_NHAY; }
    if (k == ' ' || k == '\t')     { ++c->p; return TT_NGOAI_TU; }

    c->dau = c->p;
    ++c->p;

    return TT_TRONG_TU;
}

static TrangThai tt_trong_tu(NgCanh *c) {
    char k = *c->p;

    if (k == '\0' || k == ' ' || k == '\t') {
        c->nhan_tu(c->dau, (size_t)(c->p - c->dau), c->ctx);

        if (k == '\0') { c->xong = 1; return TT_NGOAI_TU; }

        ++c->p;

        return TT_NGOAI_TU;
    }

    ++c->p;

    return TT_TRONG_TU;
}

static TrangThai tt_trong_nhay(NgCanh *c) {
    char k = *c->p;

    if (k == '\0') { c->xong = 1; return TT_NGOAI_TU; }

    if (k == '"') {
        c->nhan_tu(c->dau, (size_t)(c->p - c->dau), c->ctx);
        ++c->p;

        return TT_NGOAI_TU;
    }

    ++c->p;

    return TT_TRONG_NHAY;
}

static const HamTrangThai BANG[TT_SO_LUONG] = {
    [TT_NGOAI_TU]   = tt_ngoai_tu,
    [TT_TRONG_TU]   = tt_trong_tu,
    [TT_TRONG_NHAY] = tt_trong_nhay,
};

_Static_assert(sizeof BANG / sizeof BANG[0] == TT_SO_LUONG,
               "bang trang thai khong khop enum");

void tach(const char *s, void (*nhan_tu)(const char *, size_t, void *),
          void *ctx) {
    NgCanh    c  = { .p = s, .nhan_tu = nhan_tu, .ctx = ctx };
    TrangThai tt = TT_NGOAI_TU;

    while (!c.xong) tt = BANG[tt](&c);
}
terminal
./tach 'set nguong 1200'
[set]
[nguong]
[1200]
./tach 'noi "xin chao ban" xong'
[noi]
[xin chao ban]
[xong]

#Con trỏ hàm trực tiếp

/* Bo han enum va bang. Trang thai CHINH LA con tro ham. */

typedef struct NgCanh NgCanh;
typedef void (*HamTrangThai)(NgCanh *);

struct NgCanh {
    const char   *p, *dau;
    void        (*nhan_tu)(const char *, size_t, void *);
    void         *ctx;
    HamTrangThai  tt;            /* trang thai nam NGAY TRONG ngu canh */
    int           xong;
};

static void tt_ngoai_tu(NgCanh *c);
static void tt_trong_tu(NgCanh *c);
static void tt_trong_nhay(NgCanh *c);

static void tt_ngoai_tu(NgCanh *c) {
    char k = *c->p;

    if (k == '\0')              { c->xong = 1; return; }
    if (k == '"')               { c->dau = ++c->p; c->tt = tt_trong_nhay; return; }
    if (k == ' ' || k == '\t')  { ++c->p; return; }

    c->dau = c->p++;
    c->tt  = tt_trong_tu;
}

static void tt_trong_tu(NgCanh *c) {
    char k = *c->p;

    if (k == '\0' || k == ' ' || k == '\t') {
        c->nhan_tu(c->dau, (size_t)(c->p - c->dau), c->ctx);

        if (k == '\0') { c->xong = 1; return; }

        ++c->p;
        c->tt = tt_ngoai_tu;

        return;
    }

    ++c->p;
}

void tach(const char *s, void (*nhan_tu)(const char *, size_t, void *),
          void *ctx) {
    NgCanh c = { .p = s, .nhan_tu = nhan_tu, .ctx = ctx, .tt = tt_ngoai_tu };

    while (!c.xong) c.tt(&c);
}
BảngCon trỏ trực tiếp
Số thứ phải giữ đồng bộenum, bảng, các hàmChỉ các hàm
Chuyển trạng tháiTrả về mã, vòng lặp tra bảngGán con trỏ, một bước
Số lần gián tiếp mỗi bướcMột, qua bảngMột, qua con trỏ
In tên trạng thái để gỡ lỗiDễ, tra bảng tênKhó, chỉ có địa chỉ
So sánh trạng tháiSo số nguyênSo con trỏ hàm
Lưu trạng thái ra tệpĐược, lưu sốKhông, địa chỉ đổi mỗi lần chạy

#Hành động vào và ra

Hành động vào và ra
Đoạn mã chạy đúng một lần khi máy bước vào một trạng thái, và một lần nữa khi rời khỏi nó. Khác với hành động chính, thứ chạy mỗi lần máy ở trong trạng thái đó.
typedef struct {
    void       (*vao)(NgCanh *);     /* co the NULL */
    HamTrangThai chay;
    void       (*ra)(NgCanh *);      /* co the NULL */
    const char  *ten;
} TrangThai;

static void vao_trong_nhay(NgCanh *c) { c->dau = c->p; c->so_nhay++; }
static void ra_trong_nhay (NgCanh *c) { c->nhan_tu(c->dau, ..., c->ctx); }

static const TrangThai TT_TRONG_NHAY = {
    .vao  = vao_trong_nhay,
    .chay = tt_trong_nhay,
    .ra   = ra_trong_nhay,
    .ten  = "TRONG_NHAY",
};

/* Vong lap chinh lo phan goi vao va ra */
void chay(NgCanh *c) {
    const TrangThai *cu = NULL;

    while (!c->xong) {
        if (c->tt != cu) {
            if (cu && cu->ra)  cu->ra(c);
            if (c->tt->vao)    c->tt->vao(c);

            cu = c->tt;
        }

        c->tt->chay(c);
    }

    if (cu && cu->ra) cu->ra(c);      /* dung quen lan ra cuoi cung */
}

#Chọn cách nào

Tình huốngNên dùngVì sao
Dưới năm trạng thái, mỗi cái dưới mười dòngswitchĐọc hết trong một màn hình, và có -Wswitch
Trên tám trạng thái, mỗi cái vài chục dòngBảng con trỏ hàmMỗi trạng thái thành một hàm kiểm thử riêng được
Cần lưu và khôi phục trạng tháiswitch hoặc bảngTrạng thái phải là số, không phải địa chỉ
Cần hành động vào và raBảng structBa con trỏ hàm mỗi trạng thái, gói gọn trong một mục
Trạng thái thêm bớt lúc chạyCon trỏ trực tiếp hoặc bảng độngenum cố định lúc biên dịch
Vòng lặp cực nóng, mỗi byte một bướcswitchKhông có lời gọi gián tiếp nào
Máy trạng thái trong ISR trên vi điều khiểnswitch hoặc bảng constBảng const nằm trong Flash, xem Bài 50.1

Ba dấu hiệu cho biết đã tới lúc dùng bảng

  • Một nhánh case vượt quá một màn hình. Bạn không nhìn được cả switch cùng lúc nữa.
  • Bạn muốn kiểm thử một trạng thái riêng. Với switch thì phải chạy cả máy tới đúng trạng thái đó.
  • Có dữ liệu đi kèm mỗi trạng thái, ví dụ tên, thời gian chờ tối đa, hay hành động vào và ra. Bảng struct chứa được, còn switch thì phải thêm một switch nữa.

Tự làm thử

  1. Cài bộ tách từ bằng switch và chạy nó với ba chuỗi vào ở trên.
  2. Cài lại bằng bảng con trỏ hàm và xác nhận kết quả giống hệt.
  3. Thêm một trạng thái vào enum mà quên thêm vào bảng, xem chương trình hỏng ở đâu.
  4. Cài lại bằng con trỏ hàm trực tiếp và so ba bản về số dòng.
  5. Thêm trường tên vào trạng thái và in nhật ký mỗi bước chuyển.
  6. Thêm hành động vào và ra cho trạng thái trong dấu nháy, và kiểm tra lần ra cuối cùng có chạy không.

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

  • Ba cách cài máy trạng thái: switch, bảng con trỏ hàm, và con trỏ hàm trực tiếp trong ngữ cảnh.
  • Với bảng, vòng lặp chính rút xuống một dòng và mỗi trạng thái thành một hàm kiểm thử riêng được.
  • Cái mất khi bỏ switch là cảnh báo -Wswitch, nên phải bù bằng _Static_assert và kiểm ô NULL lúc chạy.
  • Trạng thái là con trỏ hàm thì không lưu ra tệp được, vì địa chỉ đổi mỗi lần chạy.
  • Hành động vào và ra gom mọi việc chuẩn bị và dọn dẹp vào một chỗ, thay vì rải ra từng đường chuyển.