typedef làm một việc nhỏ: đặt thêm một tên cho một kiểu đã tồn tại. Không có kiểu mới nào được tạo, không có bộ nhớ nào được dành ra, và cách giá trị hoạt động không thay đổi gì. Đây là công cụ đặt tên, và trong C, nơi tên các kiểu dựng sẵn khá dài (unsigned long long, struct Employee, void (*)(int)), một cái tên tốt rất đáng giá.
Dạng cơ bản
Cú pháp đọc giống như khai báo biến có gắn thêm typedef ở đầu. Ở chỗ lẽ ra là tên biến, bạn đặt tên kiểu mới:
unsigned long count; // declares a variable named count
typedef unsigned long ulong; // declares a TYPE named ulong
Đó là toàn bộ quy tắc, và nó giải thích mọi typedef bạn sẽ đọc, kể cả những dạng trông kỳ lạ ở phần sau của trang này.
Hãy để ý điều mà cặp Celsius/Fahrenheit không mang lại: an toàn kiểu. Cả hai đều chỉ là double, nên truyền một giá trị Fahrenheit vào chỗ cần Celsius vẫn biên dịch bình thường. Các tên này thể hiện ý định cho người đọc; chúng không tạo ra bước kiểm tra nào.
Mẫu typedef struct
Đây là lý do phần lớn chương trình C có dùng typedef. Trong C, tên kiểu của một struct bao gồm cả từ khóa:
struct Point { int x; int y; };
struct Point p; /* the word "struct" every single time */
Một typedef rút gọn điều đó:
Đọc nó theo quy tắc ở trên: bỏ typedef đi, bạn có struct Point {...} Point;, khai báo một biến tên là Point. Thêm typedef trở lại thì Point trở thành tên kiểu.
Có tag hay không có tag
Bạn có thể bỏ hẳn tag, tạo ra một struct ẩn danh chỉ có tên typedef:
typedef struct {
int x;
int y;
} Point; /* no "struct Point" exists - only "Point" */
Cách này gọn hơn, và với một bản ghi dữ liệu đơn giản thì đây là phong cách phổ biến. Nhưng nó thất bại ngay khi struct cần nhắc tới chính nó, vì tên typedef chưa dùng được cho đến khi khai báo của chính nó kết thúc:
/* Does NOT compile: "Node" is not a type yet inside its own braces. */
typedef struct {
int value;
Node *next;
} Node;
Giữ lại tag thì việc tự tham chiếu hoạt động, vì struct Node dùng được ngay khi tag xuất hiện:
Quy tắc thực tế: giữ lại tag. Nó chỉ tốn một từ, trùng với tên typedef nên không gây nhầm lẫn, và vẫn cho phép tự tham chiếu cũng như khai báo trước trong tệp header. Xem trang struct và con trỏ để biết nút liên kết đó phát triển thành gì.
Khai báo trước và kiểu opaque
Vì con trỏ tới struct có kích thước xác định ngay cả khi chưa biết nội dung struct, một tệp header có thể cung cấp một kiểu mà không để lộ các thành viên:
/* stack.h */
typedef struct Stack Stack; /* declared, not defined */
Stack *stack_create(void);
void stack_push(Stack *s, int value);
int stack_pop(Stack *s);
void stack_destroy(Stack *s);
Định nghĩa đầy đủ struct Stack { ... }; nằm trong stack.c và không ai bên ngoài đụng được vào các thành viên của nó. Mẫu kiểu opaque này là cách các thư viện C đảm bảo tính đóng gói, và đó chính xác là FILE trong thư viện chuẩn: bạn nhận một FILE * từ fopen mà không được biết gì về bên trong nó. Xem xử lý tệp.
typedef cho enum và union
Cùng mẫu đó áp dụng cho hai loại kiểu phức hợp còn lại:
Không có typedef, chúng sẽ là enum Color c; và union Number n;. Xem enum và union để biết chúng thực sự làm gì.
typedef cho con trỏ hàm
Đây là chỗ typedef không còn chỉ là tiện lợi mà gần như trở nên cần thiết. Cú pháp gốc của con trỏ hàm nổi tiếng là khó đọc:
void (*handler)(int, const char *); /* a variable */
void register_cb(void (*cb)(int, const char *)); /* a parameter */
Đặt tên kiểu một lần và cả hai trở nên dễ đọc:
Áp dụng lại quy tắc bỏ typedef: chỉ riêng int (*IntBinaryOp)(int, int); sẽ khai báo một biến tên IntBinaryOp trỏ tới một hàm nhận hai số int và trả về một int. Có typedef, đó trở thành tên kiểu. Xem thêm ở trang con trỏ hàm.
typedef và #define
Với các bí danh đơn giản chúng trông có vẻ thay thế được cho nhau, nhưng thực ra không:
#define INT_PTR int *
typedef int *int_ptr;
INT_PTR a, b; /* expands to: int *a, b; -- b is a plain int! */
int_ptr c, d; /* both c and d are int * */
#define là phép thay thế văn bản mù quáng thực hiện trước khi trình biên dịch nhìn thấy code; typedef là một khai báo thật mà trình biên dịch hiểu, và nó áp dụng cho mọi tên trong danh sách. Hãy dùng typedef cho kiểu và để dành #define cho hằng số và macro.
Khi typedef gây hại
Typedef cũng có thể che giấu thông tin mà người đọc cần. Ví dụ rõ nhất là giấu một con trỏ:
typedef struct Node *NodeRef; /* the star disappears */
NodeRef n = get_node();
Nhìn NodeRef n, người đọc không biết n có thể là NULL không, có phải free không, n và bản sao của nó có dùng chung một đối tượng không, hay các thành viên được truy cập bằng . hoặc ->. Cả bốn câu hỏi đều được trả lời ngay lập tức với struct Node *n.
Thư viện chuẩn cũng đồng ý: nó typedef struct (FILE) và để dấu sao ở chỗ sử dụng (FILE *fp). Từ đó rút ra hai nguyên tắc hợp lý:
- Nên typedef struct, union, enum và con trỏ hàm: phần rườm rà bị loại bỏ không mang thông tin gì.
- Không nên typedef một con trỏ chỉ để viết ngắn hơn, và không typedef một kiểu cơ bản để ngụy trang nó (
typedef int Boolean;mời gọi ai đó lưu7vào đó; hãy dùngbooltrong<stdbool.h>, được trình bày ở trang boolean).
Chính các tên có độ rộng cố định của thư viện chuẩn trong <stdint.h> (uint32_t, int64_t, size_t) đều là typedef, và chúng là lý lẽ tốt nhất ủng hộ tính năng này: chúng nói rõ chúng là gì, chúng khả chuyển, và không có gì bị che giấu.
Câu hỏi thường gặp
typedef trong C dùng để làm gì?
Nó đặt tên thứ hai cho một kiểu có sẵn. typedef unsigned long ulong; nghĩa là từ đó trở đi ulong và unsigned long là cùng một kiểu. Nó không tạo kiểu mới, không cấp phát bộ nhớ mới, chỉ là một cách viết ngắn hơn hoặc dễ hiểu hơn cho kiểu bạn đã có.
Mẫu typedef struct trong C là gì?
typedef struct Point { int x; int y; } Point; khai báo struct và đặt tên kiểu Point trong cùng một câu lệnh, nên bạn có thể viết Point p; thay vì struct Point p;. Giữ lại tag (struct Point) là cần thiết khi struct phải tham chiếu tới chính nó, như một nút danh sách liên kết.
typedef và #define khác nhau thế nào?
typedef được trình biên dịch xử lý và tạo ra một bí danh kiểu thật; #define là phép thay thế văn bản do bộ tiền xử lý thực hiện trước khi biên dịch. Khác biệt đó gây rắc rối với con trỏ: #define PTR int* khiến PTR a, b; được mở rộng thành int* a, b;, chỉ a là con trỏ. typedef int *PTR; thì biến cả hai thành con trỏ.
Có nên typedef một kiểu con trỏ trong C không?
Thường là không. typedef struct Node *NodeRef; che giấu việc kiểu đó là con trỏ, nên người đọc không biết một biến có thể là NULL không, có phải free không, hay có cần dùng -> không. FILE * của thư viện chuẩn giữ dấu sao hiển thị chính vì lý do này. Hãy typedef struct, không typedef con trỏ tới nó.