← Lập trình C nâng cao

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

Chỉ cần MỘT quy tắc đọc là giải được mọi câu đố loại này - không phải học thuộc từng mẫu. Bài này dạy quy tắc đó, kèm công cụ luyện từng bước.

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:

int x;
  1. x x là
bước 1/2

Trung thực

Công cụ trên hỗ trợ tập con khai báo đủ dùng cho bài học (con trỏ, mảng, hàm, ngoặc nhóm, const). Khai báo C đầy đủ còn nhiều món (con trỏ tới mảng nhiều chiều, hàm nhận con trỏ hàm…) - nhưng quy tắc đọc thì không đổi, chỉ là nhiều tầng hơn.

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

Con trỏ hàm - và lý do tang_dan không cần dấu & phía trước - được nói kỹ ở bài Bẫy & quản lý bộ nhớ. Bài này chỉ cần bạn ĐỌC được khai báo của nó.

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 thông khai báo rồi, bài kế quay về câu hỏi nền tảng hơn: mảng và con trỏ thật sự quan hệ thế nào - vì sao a[i] chỉ là *(a + i) viết gọn hơn.

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.

Theo đúng quy tắc: argv là mảng (chưa rõ kích thước), mỗi phần tử là con trỏ, trỏ tới char - tức một mảng các chuỗi. Giờ bạn hiểu vì sao argv[1] là chuỗi tham số đầu tiên. (Thử gõ vào công cụ ở Bước 2!)

Đọc được nó là một chuyện; VIẾT code dễ đọc là chuyện khác. Trong code của mình, hãy đặt typedef cho các kiểu con trỏ hàm/kiểu lồng nhau - người sau (và chính bạn 3 tháng nữa) đỡ giải đố. Khai báo rối chỉ nên tồn tại ở ranh giới với thư viện cũ.

Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.

Tick những điều em tự tin làm được sau khi học bài này. 0/6

Trả lời vài câu để chắc rằng em đã nắm bài.

Câu 1/3 Điểm: 0

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. 1

    Đọc ba câu đố

    Dùng công cụ Bước 2: tự đọc int *a[10], int (*p)[10]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. 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. 3

    Viết ngược

    Viết khai báo C cho mô tả: "a là 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. 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òn sizeof(*p) = 40?

    Hoàn thành khi: a là 10 con trỏ (10 × 8 byte); *p là mảng 10 int (10 × 4 byte).

  5. 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. 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.