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

Enum cơ bản

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

  • Khai báo enum và hiểu quy tắc tự đánh số
  • Biết kiểu thật mà enum được cài đặt thành
  • So sánh enum với define và với const
  • Nhận ra bốn giới hạn của enum trong C

Enum cho bạn đặt tên cho một tập hợp giá trị nguyên có liên quan. Nó là công cụ đơn giản nhất trong Phần 7, nhưng cũng là công cụ làm mã dễ đọc nhất, và nó mở ra một mẹo rất giá trị ở Bài 16.2.

#Cú pháp và quy tắc đánh số

enum TrangThai {
    CHO,          /* 0 */
    DANG_CHAY,    /* 1 */
    TAM_DUNG,     /* 2 */
    KET_THUC      /* 3 */
};

enum TrangThai t = DANG_CHAY;
Enum
Một kiểu liệt kê, gồm một tập hằng số nguyên có tên. Giá trị mặc định bắt đầu từ 0 và tăng dần một đơn vị.
danh-so.c
/* Tự đánh số từ 0 */
enum A { X, Y, Z };                    /* 0, 1, 2 */

/* Đặt giá trị đầu, phần sau tiếp tục từ đó */
enum B { P = 5, Q, R };                /* 5, 6, 7 */

/* Đặt riêng từng cái */
enum Http {
    OK          = 200,
    NOT_FOUND   = 404,
    SERVER_LOI  = 500,
};

/* Trộn: cái nào không ghi thì tiếp tục từ cái trước */
enum C { M = 1, N, O = 10, P2, Q2 };   /* 1, 2, 10, 11, 12 */

/* Giá trị trùng nhau hoàn toàn hợp lệ */
enum D { DAU = 0, KHONG = 0, MOT = 1 };

/* Giá trị âm cũng được */
enum E { LOI = -1, THANH_CONG = 0 };

Kết hợp typedef

/* Không có typedef: phải viết chữ enum mỗi lần */
enum TrangThai t1;

/* Có typedef: gọn hơn */
typedef enum {
    TT_CHO,
    TT_DANG_CHAY,
    TT_TAM_DUNG,
    TT_KET_THUC,
} TrangThai;

TrangThai t2 = TT_DANG_CHAY;

/* Giữ cả tên thẻ, giống khuyến nghị ở Bài 15.2 */
typedef enum TrangThai {
    TT_CHO,
    TT_DANG_CHAY,
} TrangThai;

#Kiểu thật của enum

kieu-that.c
#include <stdio.h>

typedef enum { A, B, C } E;

int main(void)
{
    E e = B;

    printf("sizeof(E) = %zu\n", sizeof(E));
    printf("sizeof(e) = %zu\n", sizeof(e));
    printf("gia tri   = %d\n", e);       /* in bằng %d, vì enum là số nguyên */

    int i = e;        /* enum chuyển sang int tự động */
    e = 2;            /* và ngược lại, không cần ép kiểu */

    return 0;
}
terminal
./kieu-that
sizeof(E) = 4
sizeof(e) = 4
gia tri   = 1
Câu hỏiTrả lời
Enum có phải kiểu riêng khôngCó, nhưng nó tương thích với một kiểu nguyên nào đó
Kiểu nguyên nàoDo trình biên dịch chọn, thường là int nếu mọi giá trị vừa trong int
sizeof bằng bao nhiêuThường là 4, nhưng chuẩn không bảo đảm
In bằng định dạng gì%d, vì giá trị được nâng lên int khi truyền cho printf
Chuyển sang intTự động, không cần ép kiểu
Gán số nguyên vào biến enumĐược, kể cả số không có trong danh sách

#So với define và const

/* Ba cách đặt tên cho một hằng số */

#define TT_CHO 0                    /* macro */

static const int tt_cho = 0;        /* biến const */

enum { TT_CHO = 0 };                /* hằng enum */
defineconst intenum
Có kiểuKhông
Trình gỡ lỗi thấy tênKhông
Dùng làm kích thước mảngĐượcKHÔNGĐược
Dùng làm nhãn caseĐượcKHÔNGĐược
Có phạm viKhông, tới hết file
Chiếm bộ nhớKhôngCó thểKhông
Nhóm các hằng liên quanKhôngKhông
Lấy địa chỉ đượcKhôngKhông

Mẹo enum vô danh cho hằng số đơn lẻ

/* Không cần tên kiểu, chỉ cần hằng số */
enum { TOI_DA = 100, DAI_TEN = 64 };

char ten[DAI_TEN];
int  ds[TOI_DA];

switch (x) {
    case TOI_DA: break;      /* dùng được làm nhãn case */
}

#Bốn giới hạn của enum trong C

Giới hạn 1: không có kiểm tra phạm vi

typedef enum { DO, XANH, LUC } Mau;

Mau m = 42;               /* hợp lệ, không cảnh báo */
Mau n = DO + XANH;        /* hợp lệ, bằng 1, tình cờ là XANH */
++m;                      /* hợp lệ, enum là số nguyên */

Giới hạn 2: hai enum khác nhau trộn được với nhau

typedef enum { DO, XANH } Mau;
typedef enum { NHO, LON } CoLon;

Mau  m = DO;
CoLon c = NHO;

m = c;                    /* HỢP LỆ trong C, không cảnh báo mặc định */

if (m == LON) { }         /* cũng hợp lệ, so hai kiểu khác nhau */

Giới hạn 3: không có cách lấy số phần tử

Đếm tay
typedef enum { TT_CHO, TT_CHAY, TT_DUNG } TrangThai;

const char *ten[3] = { "Cho", "Chay", "Dung" };

/* Thêm một trạng thái mới thì phải nhớ sửa con số 3 ở đây,
   và ở mọi chỗ khác trong chương trình. */
Thêm một hằng đếm ở cuối
typedef enum {
    TT_CHO,
    TT_CHAY,
    TT_DUNG,
    TT_SO_LUONG,      /* luôn là phần tử cuối, giá trị bằng số phần tử thật */
} TrangThai;

const char *ten[TT_SO_LUONG] = {
    [TT_CHO]  = "Cho",
    [TT_CHAY] = "Chay",
    [TT_DUNG] = "Dung",
};

/* Thêm TT_TAM_DUNG trước TT_SO_LUONG thì mảng tự lớn thêm một ô,
   và trình biên dịch cảnh báo nếu bạn quên điền tên cho nó. */

Giới hạn 4: không có cách in tên tự động

in-ten.c
typedef enum { TT_CHO, TT_CHAY, TT_DUNG, TT_SO_LUONG } TrangThai;

/* Cách 1: bảng tra, đơn giản nhất */
static const char *TEN[TT_SO_LUONG] = {
    [TT_CHO]  = "CHO",
    [TT_CHAY] = "CHAY",
    [TT_DUNG] = "DUNG",
};

const char *tt_ten(TrangThai t)
{
    if (t < 0 || t >= TT_SO_LUONG) return "KHONG_XAC_DINH";

    return TEN[t] != NULL ? TEN[t] : "CHUA_DAT_TEN";
}

/* Cách 2: switch, để trình biên dịch bắt lỗi thiếu nhánh, xem Bài 16.2 */
const char *tt_ten2(TrangThai t)
{
    switch (t) {
        case TT_CHO:       return "CHO";
        case TT_CHAY:      return "CHAY";
        case TT_DUNG:      return "DUNG";
        case TT_SO_LUONG:  break;
    }

    return "KHONG_XAC_DINH";
}

#Bốn mẫu hữu ích

Mẫu 1: mã lỗi có tên

typedef enum {
    LOI_KHONG      =  0,
    LOI_THAM_SO    = -1,
    LOI_BO_NHO     = -2,
    LOI_MO_FILE    = -3,
    LOI_DINH_DANG  = -4,
} MaLoi;

MaLoi doc_cau_hinh(const char *duong_dan);

if (doc_cau_hinh("a.cfg") != LOI_KHONG)
    fprintf(stderr, "That bai\n");

So với việc trả về số âm một không tên, cách này cho người gọi biết chính xác chuyện gì hỏng, và trình gỡ lỗi hiện tên chứ không hiện con số.

Mẫu 2: cờ bit

co-bit.c
typedef enum {
    CO_DOC   = 1 << 0,      /* 1 */
    CO_GHI   = 1 << 1,      /* 2 */
    CO_CHAY  = 1 << 2,      /* 4 */
    CO_AN    = 1 << 3,      /* 8 */
} Co;

unsigned co = CO_DOC | CO_GHI;        /* kết hợp bằng phép hoặc */

if (co & CO_DOC)  printf("doc duoc\n");
if (co & CO_CHAY) printf("chay duoc\n");

co |= CO_CHAY;                        /* bật thêm một cờ */
co &= ~CO_GHI;                        /* tắt một cờ */

Mẫu 3: enum làm chỉ số bảng

typedef enum { NGON_VI, NGON_EN, NGON_JA, NGON_SO_LUONG } NgonNgu;

static const char *CHAO[NGON_SO_LUONG] = {
    [NGON_VI] = "Xin chao",
    [NGON_EN] = "Hello",
    [NGON_JA] = "Konnichiwa",
};

const char *loi_chao(NgonNgu n)
{
    return (n >= 0 && n < NGON_SO_LUONG) ? CHAO[n] : CHAO[NGON_EN];
}

Mẫu 4: nhãn cho union có nhãn

typedef enum { GT_INT, GT_DOUBLE, GT_CHUOI } LoaiGiaTri;

typedef struct {
    LoaiGiaTri loai;

    union {
        long   i;
        double d;
        char  *s;
    };
} GiaTri;

Đây là mẫu quan trọng nhất trong ba chương của Phần 7, và Bài 17.2 dành riêng cho nó.

Tự làm thử

  1. Khai báo enum theo cả bốn kiểu đánh số trong bài, in giá trị từng hằng.
  2. Gán một số nằm ngoài tập enum vào biến enum, in ra và giải thích vì sao trình biên dịch cho qua.
  3. So sánh hai enum khác kiểu, biên dịch có và không có -Wenum-compare.
  4. Thử dùng static const int làm kích thước mảng và làm nhãn case, chép lại hai thông báo lỗi.
  5. Cài mẫu hằng đếm ở cuối enum, thêm một trạng thái mới và xác nhận bảng tên tự lớn theo.
  6. Cài kỹ thuật danh sách X sinh cả enum lẫn bảng tên, thêm một hằng và kiểm chứng cả hai cùng cập nhật.
  7. Cài mẫu cờ bit với bốn cờ, viết hàm bật, tắt và kiểm tra từng cờ.

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

  • Enum đánh số từ 0 và tăng dần, nhưng đặt giá trị riêng cho từng hằng được, kể cả trùng nhau và âm.
  • Trong C, enum chỉ là hằng số nguyên có tên. Không có kiểm tra phạm vi và hai enum khác kiểu trộn được với nhau.
  • Dùng enum thay cho const int khi cần hằng số nguyên, vì const int không phải hằng biên dịch trong C.
  • Thêm một hằng đếm ở cuối để biết số phần tử, và dùng nó làm kích thước bảng tra.
  • Tên hằng enum nằm ở không gian tên chung, nên luôn đặt tiền tố để tránh đụng tên.