Bài 3 · Nâng cao · 22 phút· Cập nhật 11/06/2026
Đọc khai báo C phức tạp
Biên soạn bởi Nguyễn Anh Tuấn
Đọc khai báo C phức tạp (C declarations): giải mã int (*f[10])(void) bằng quy tắc phải trước trái sau; *a[10] vs (*p)[10], cmp của qsort, typedef & cdecl.
Bạn đã nắm con trỏ (Bài 1) và truyền địa chỉ vào hàm (Bài 2). Nhưng mở một header thư viện thật, bạn sẽ gặp những dòng như thế này:
khai báo có thật trong code C ngoài đời
int (*f[10])(void); f là gì - mảng? con trỏ? hàm? Khai báo C rối vì Ritchie chọn nguyên tắc "khai báo giống cách dùng" (declaration mimics use): khai báo int *p nghĩa là "khi bạn viết *p, bạn nhận một int". Đẹp với khai báo ngắn - và thành câu đố khi mảng, hàm, con trỏ lồng vào nhau.
Tin tốt
Toàn bộ bí quyết nằm trong ba ý:
- ▸BẮT ĐẦU ở tên biến, không phải ở kiểu bên trái.
- ▸Đi sang PHẢI trước: gặp [n] đọc "mảng n phần tử", gặp (…) đọc "hàm trả về" - vì [] và () bám chặt hơn *.
- ▸Hết bên phải mới quay sang TRÁI: mỗi dấu * đọc "con trỏ, trỏ tới". Gặp ngoặc nhóm: xong trong ngoặc rồi mới ra ngoài.
Luyện ngay - chọn khai báo (hoặc tự gõ), đoán trước rồi bấm Tiếp → xem từng bước:
- x x là
Trung thực
Hai cặp này là nguồn bug kinh điển. Khác nhau đúng một cặp ngoặc - vì ngoặc đổi thứ tự đọc:
| Khai báo | Đọc là |
|---|---|
| int *a[10] | mảng 10 con trỏ, trỏ tới int |
| int (*p)[10] | con trỏ, trỏ tới mảng 10 int |
| int *f(void) | hàm trả về con trỏ tới int |
| int (*f)(void) | con trỏ, trỏ tới hàm trả về int |
Đừng tin suông - bắt trình biên dịch làm chứng bằng sizeof:
sinh-doi.c - chạy trên máy 64-bit
#include <stdio.h>
int main(void) {
int *a[10]; /* mang 10 con tro toi int */
int (*p)[10]; /* con tro toi mang 10 int */
printf("sizeof(a) = %zu\n", sizeof(a));
printf("sizeof(*p) = %zu\n", sizeof(*p));
return 0;
} Kết quả khi chạy
sizeof(a) = 80 sizeof(*p) = 40
- ▸a là 10 con trỏ → 10 × 8 byte = 80. Còn *p là mảng 10 int → 10 × 4 byte = 40.
- ▸Một cặp ngoặc đổi hẳn bản chất của biến - và sizeof phơi ra điều đó.
- ▸Khi nghi ngờ một khai báo: đọc bằng quy tắc, rồi kiểm bằng sizeof hoặc công cụ.
Hàm sắp xếp qsort trong thư viện chuẩn nhận một tham số mà giờ bạn đọc được: int (*cmp)(const void *, const void *) - cmp là con trỏ, trỏ tới hàm nhận hai con trỏ và trả về int. Tức là: bạn đưa cho qsort một hàm so sánh do bạn viết:
qsort-demo.c
#include <stdio.h>
#include <stdlib.h>
int tang_dan(const void *a, const void *b) {
return *(const int *)a - *(const int *)b;
}
int main(void) {
int diem[5] = { 7, 2, 9, 4, 6 };
qsort(diem, 5, sizeof(int), tang_dan);
for (int i = 0; i < 5; i++) printf("%d ", diem[i]);
printf("\n");
return 0;
} Kết quả khi chạy
2 4 6 7 9
Sẽ gặp lại
Kỹ năng đọc dùng khi gặp code của người khác. Còn trong code của bạn, hãy đặt tên cho kiểu phức tạp bằng typedef - khai báo rối biến mất:
trước và sau typedef
/* truoc: cau do */
int (*handlers[10])(void);
/* sau: dat ten kieu, roi dung ten do */
typedef int (*Handler)(void); /* Handler = con tro ham (void) -> int */
Handler handlers[10]; /* mang 10 Handler - doc mot phat ra ngay */ - ▸typedef đặt TÊN cho một kiểu - định nghĩa câu đố một lần, dùng tên mãi mãi.
- ▸Quy ước hay gặp: typedef cho kiểu con trỏ hàm và struct dùng nhiều nơi.
- ▸Kiểm tra chéo: công cụ Bước 2 và cdecl.org - đọc xong tự kiểm, không đoán mò.
Bài tiếp theo
Câu hỏi thường gặp
Ritchie chọn nguyên tắc "khai báo giống cách dùng" (declaration mimics use): nếu bạn viết *p để lấy int, thì khai báo là int *p. Với khai báo đơn giản, nguyên tắc này rất đẹp; nhưng khi mảng, hàm và con trỏ lồng nhau thì nó tạo ra những câu đố. Đổi lại, một khi nắm quy tắc đọc, bạn đọc được MỌI khai báo.
Vì luật ưu tiên của C: hậu tố [] (mảng) và () (hàm) bám vào tên chặt hơn tiền tố * (con trỏ). Muốn dấu * "thắng" thì phải bọc ngoặc: (*p)[10] - ngoặc ép đọc * trước khi ra tới [10].
Dùng được cho đa số trường hợp, nhưng nó là mẹo chứ không phải luật - có khai báo (vd mảng nhiều chiều lồng con trỏ) xoắn ốc cho kết quả sai. Quy tắc "phải trước, trái sau, ngoặc đổi tầng" trong bài mới là luật thật, bám đúng độ ưu tiên của ngôn ngữ.
Một công cụ kinh điển dịch khai báo C sang tiếng Anh và ngược lại - có bản web tại cdecl.org. Gõ int (*f[10])(void) nó trả về "declare f as array 10 of pointer to function (void) returning int". Dùng để KIỂM TRA sau khi tự đọc, đừng dùng để khỏi học đọc.
Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.
Trả lời vài câu để chắc rằng em đã nắm bài.
Theo quy tắc đọc khai báo, bắt đầu từ tên biến rồi đi hướng nào trước?
- 1
Đọc ba câu đố
Dùng công cụ Bước 2: tự đọc
int *a[10],int (*p)[10]vàint (*f[10])(void)TRƯỚC khi bấm "Tiếp", rồi đối chiếu từng bước.Hoàn thành khi: Tự đọc đúng cả ba trước khi công cụ xác nhận.
- 2
Khai báo của mèo con
Gõ vào công cụ một khai báo tự nghĩ (vd
double *m[3]) và đoán câu đọc trước khi bấm.Hoàn thành khi: Đoán khớp với kết quả công cụ ít nhất 2/3 lần thử.
- 3
Viết ngược
Viết khai báo C cho mô tả: "
alà mảng 5 con trỏ, trỏ tới hàm nhận (int), trả vềvoid". Kiểm bằng công cụ.Hoàn thành khi:
void (*a[5])(int)- công cụ đọc ra đúng mô tả. - 4
Chứng minh bằng sizeof
Chạy lại ví dụ Bước 3 trên máy mèo con. Vì sao
sizeof(a)= 80 cònsizeof(*p)= 40?Hoàn thành khi:
alà 10 con trỏ (10 × 8 byte);*plà mảng 10int(10 × 4 byte). - 5
Đối chiếu với cdecl
Mở cdecl.org, gõ
int (*f[10])(void)và so bản tiếng Anh với câu đọc tiếng Việt của bài.Hoàn thành khi: "array 10 of pointer to function (void) returning int" - khớp từng tầng với câu của công cụ.
- 6
Đọc sâu thêm (Expert C)
Nếu có cuốn Expert C Programming (giới thiệu ở bài 1 khoá cơ bản): đọc chương 3 "Unscrambling Declarations in C".
Hoàn thành khi: Kể lại được 1 ví dụ trong chương mà bài này chưa nhắc tới.