Menu

Enum trong C: hằng có tên, đánh số tự động, enum sang chuỗi

Cách định nghĩa enum trong C: đánh số tự động, giá trị tường minh, dùng enum trong switch để trình biên dịch bắt các case bị thiếu, chuyển enum thành chuỗi và các quy ước đặt tên giúp code dễ đọc.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

Code đầy những con số trần trụi là code không ai đọc nổi. if (state == 2) không cho bạn biết gì; if (state == STATE_RUNNING) cho bạn biết tất cả. Một enum (kiểu liệt kê) là cách C đặt tên đàng hoàng cho một tập số nguyên nhỏ, cố định, và khác với một đống #define, chúng được gom vào một kiểu mà trình biên dịch biết.

Định nghĩa một enum

Khai báo đặt tên cho một tag và liệt kê các hằng enum (enumerator) trong cặp ngoặc nhọn:

Có hai điều cần để ý ngay. Thứ nhất, việc đánh số bắt đầu từ 0 và tăng dần một đơn vị, nên RED là 0, GREEN là 1, BLUE là 2. Thứ hai, tên các hằng enum nhìn thấy được ở mọi nơi trong phạm vi bao quanh: bạn viết GREEN, không phải Color.GREEN. Chúng nằm cùng không gian tên với các biến của bạn, vì thế code C thường viết chúng bằng CHỮ IN HOA hoặc thêm tiền tố theo kiểu (COLOR_RED) để tránh trùng tên.

In một enum thì dùng %d, vì giá trị liệt kê chính là một giá trị số nguyên.

Giá trị tường minh

Gán cho một hằng enum bất kỳ một hằng số tường minh, các hằng phía sau sẽ tiếp tục từ đó:

Các giá trị không bắt buộc phải khác nhau hay theo thứ tự: hai hằng enum có thể dùng chung một số, đôi khi có ích để tạo bí danh (COLOR_DEFAULT = COLOR_BLACK). Chúng phải là hằng số nguyên lúc biên dịch, nên bạn không thể tính một hằng từ một biến.

Một cách viết quen thuộc dùng điều này để ghi lại số lượng:

enum Suit {
    CLUBS, DIAMONDS, HEARTS, SPADES,
    SUIT_COUNT           /* automatically 4 - stays correct if you add a suit */
};

SUIT_COUNT không phải là một chất bài thật; nó là số lượng chất bài, được cơ chế đánh số tự động duy trì miễn phí. Vòng lặp và kích thước mảng có thể dùng nó mà không bao giờ bị lỗi thời.

typedef enum

Giống với struct, tên kiểu đầy đủ trong C là enum Color, và một typedef giúp bỏ từ khóa đó:

Vì các hằng enum là số nguyên bình thường bắt đầu từ 0, chúng kiêm luôn vai trò chỉ số mảng: dx[DIR_EAST] chạy được mà không cần chuyển đổi gì. Đó là tính chất hữu ích nhất của một enum bắt đầu từ 0.

Enum trong switch

Ghép enum với switch là lúc kiểu này bắt đầu phát huy giá trị. Biên dịch với -Wall và trình biên dịch sẽ báo khi bạn quên một case.

Thêm STATE_FAILED vào enum và GCC sẽ cảnh báo: enumeration value 'STATE_FAILED' not handled in switch. Cảnh báo đó là thứ gần nhất với phép kiểm tra tính đầy đủ mà C có, và là lý do mạnh mẽ để viết các switch này không có case default:: một default sẽ làm im cảnh báo và để các trạng thái mới lọt qua mà không được xử lý. Thay vào đó, hãy đặt phương án dự phòng sau switch, như hàm describe đã làm.

Chuyển enum thành chuỗi

C không giữ tên nào lúc chạy; printf("%s", GREEN) không thể chạy được vì GREEN đơn giản chỉ là 1. Có hai mẫu giải quyết việc này.

Cách dùng switch, được trình biên dịch kiểm tra:

Và cách dùng bảng tra cứu, ngắn hơn nhưng không được kiểm tra: nếu mảng và enum lệch nhau thì không có cảnh báo nào:

Các chỉ số mảng chỉ định ([RED] = "RED") ít nhất giữ tên gắn với đúng ô của nó, nên sắp xếp lại enum không thể âm thầm xáo trộn các chuỗi. Phép kiểm tra giới hạn quan trọng vì không có gì ngăn nơi gọi truyền vào một giá trị nằm ngoài enum, xem phần tiếp theo.

Enum không kín kẽ

Bên dưới, một biến enum là số nguyên, và C không kiểm soát phạm vi:

Phép toán số học trên hằng enum chạy được, so sánh với int thường chạy được, và một phép ép kiểu ra ngoài phạm vi vẫn được chấp nhận. Vì vậy hãy coi enum là tài liệu tuyệt vời và một gợi ý tốt cho trình biên dịch, không phải là một sự đảm bảo. Hãy kiểm tra mọi giá trị đến từ bên ngoài chương trình, như một tệp, một đối số, một thông điệp mạng, trước khi dùng nó trong switch.

Kích thước và lưu trữ

Trong C17, mọi hằng enum phải vừa trong một int: enum Huge { BIG = 3000000000 }; vi phạm ràng buộc và bị một trình biên dịch nghiêm ngặt (-pedantic-errors) từ chối, dù gcc và clang chấp nhận nó như một phần mở rộng ở chế độ mặc định. Bản thân các hằng enum có kiểu int, còn kích thước của kiểu enum do cài đặt quyết định (trong thực tế là int):

(C23 gỡ bỏ giới hạn này: một hằng enum có thể vượt phạm vi int, và bạn còn có thể chọn kiểu nền bằng enum E : unsigned long { ... }. Cho đến khi bộ công cụ của bạn nhắm tới C23, hãy giữ các hằng enum trong phạm vi int.)

Vì lựa chọn chính xác do cài đặt quyết định, đừng bao giờ ghi trực tiếp một enum vào tệp nhị phân hay gói tin mạng rồi mong máy khác đọc lại được. Hãy chuyển sang một kiểu có độ rộng cố định như uint8_t hoặc uint32_t từ <stdint.h> tại ranh giới đó.

Enum và #define

Cả hai đều đặt tên cho một con số. Nhìn chung enum tốt hơn:

enum#define
Được xử lý bởitrình biên dịchbộ tiền xử lý
Gom nhómcác giá trị liên quan dùng chung một kiểumỗi hằng độc lập
Đánh số tự độngcókhông, bạn tự đánh số
Thấy trong trình gỡ lỗithường có, theo tênkhông, chỉ thấy con số
Cảnh báo switch thiếu casecókhông
Phạm vituân theo phạm vi khối lệnhtheo văn bản, đến khi gặp #undef

Hãy dùng #define khi hằng không phải số nguyên (một chuỗi, một số thực) hoặc khi chính bộ tiền xử lý phải dùng được nó, như trong biên dịch có điều kiện. Với một tập trạng thái, cờ hoặc loại số nguyên có liên quan, hãy dùng enum.

Quy ước đặt tên

Enum rất dễ trùng tên vì tên của chúng nằm trong phạm vi thông thường. Những quy ước giúp các codebase C lớn luôn gọn gàng:

  • Thêm tiền tố là tên kiểu cho mỗi hằng enum: COLOR_RED, STATE_IDLE, HTTP_OK. Một RED không có tiền tố trong chương trình đồ họa sớm muộn cũng đụng phải RED của người khác.
  • Viết hằng enum bằng SHOUT_CASE, khớp với cách viết các hằng khác.
  • Đặt tên kiểu ở dạng số ít PascalCase hoặc snake_case_t: Color, State hoặc color_t, nhất quán trong toàn dự án.
  • Thêm một hằng đánh dấu _COUNT ở cuối khi bạn sẽ lặp hoặc lấy chỉ số bằng enum.

Câu hỏi thường gặp

Định nghĩa enum trong C như thế nào?

enum Color { RED, GREEN, BLUE }; định nghĩa một kiểu có giá trị là ba hằng có tên. Mặc định chúng được đánh số từ 0, nên RED là 0, GREEN là 1 và BLUE là 2. Khai báo biến bằng enum Color c = GREEN;, hoặc thêm một typedef để chỉ cần viết Color c.

Có gán giá trị cụ thể cho enum trong C được không?

Được: enum Status { OK = 200, NOT_FOUND = 404, ERROR = 500 };. Bạn có thể gán một số tên và để các tên khác tự nối tiếp: tên nào không có giá trị tường minh sẽ tiếp tục từ tên đứng trước, nên enum { A = 10, B, C }; cho B = 11 và C = 12.

Chuyển enum thành chuỗi trong C như thế nào?

C không có cách dựng sẵn; tên các hằng enum không tồn tại lúc chạy. Cách thường dùng là một hàm nhỏ có switch trả về một chuỗi literal cho mỗi giá trị, hoặc một mảng chuỗi lấy enum làm chỉ số. Cách dùng switch an toàn hơn vì -Wall sẽ cảnh báo khi một hằng enum mới chưa có case.

Kích thước của enum trong C là bao nhiêu?

Do cài đặt quyết định, nhưng trong thực tế là sizeof(int), tức 4 byte trên hầu hết hệ thống. Trình biên dịch chọn một kiểu số nguyên đủ chứa mọi hằng enum. Đừng giả định một kích thước cụ thể khi sắp xếp dữ liệu cho tệp nhị phân hay gói tin mạng; ở đó hãy dùng kiểu có độ rộng cố định từ <stdint.h>.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu