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ách | Trạng thái là | Chuyển bằng | Trình biên dịch bắt thiếu | Bài |
|---|---|---|---|---|
| switch | Giá trị enum | Gán biến trạng thái | Có, nhờ -Wswitch | 16.3 |
| Bảng con trỏ hàm | Chỉ số vào bảng | Hàm trả về trạng thái kế tiếp | Không | Bài này |
| Con trỏ hàm trực tiếp | Chính con trỏ hàm | Gán con trỏ mới | Không | Bà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ảng | Con trỏ trực tiếp | |
|---|---|---|
| Số thứ phải giữ đồng bộ | enum, bảng, các hàm | Chỉ các hàm |
| Chuyển trạng thái | Trả về mã, vòng lặp tra bảng | Gán con trỏ, một bước |
| Số lần gián tiếp mỗi bước | Một, qua bảng | Một, qua con trỏ |
| In tên trạng thái để gỡ lỗi | Dễ, tra bảng tên | Khó, chỉ có địa chỉ |
| So sánh trạng thái | So số nguyên | So 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ống | Nên dùng | Vì sao |
|---|---|---|
| Dưới năm trạng thái, mỗi cái dưới mười dòng | switch | Đọ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òng | Bảng con trỏ hàm | Mỗ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ái | switch hoặc bảng | Trạng thái phải là số, không phải địa chỉ |
| Cần hành động vào và ra | Bảng struct | Ba 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ạy | Con trỏ trực tiếp hoặc bảng động | enum cố định lúc biên dịch |
| Vòng lặp cực nóng, mỗi byte một bước | switch | Khô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ển | switch hoặc bảng const | Bả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
casevượt quá một màn hình. Bạn không nhìn được cảswitchcùng lúc nữa. - Bạn muốn kiểm thử một trạng thái riêng. Với
switchthì 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
switchthì phải thêm mộtswitchnữa.
Tự làm thử
- Cài bộ tách từ bằng
switchvà chạy nó với ba chuỗi vào ở trên. - Cài lại bằng bảng con trỏ hàm và xác nhận kết quả giống hệt.
- 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.
- Cài lại bằng con trỏ hàm trực tiếp và so ba bản về số dòng.
- Thêm trường tên vào trạng thái và in nhật ký mỗi bước chuyển.
- 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ỏ
switchlà cảnh báo-Wswitch, nên phải bù bằng_Static_assertvà 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.