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

Con trỏ tới mảng

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

  • Đọc khai báo phức tạp bằng quy tắc xoắn ốc
  • Phân biệt bốn khai báo trông giống nhau
  • Dùng con trỏ tới mảng để duyệt ma trận
  • Dùng typedef để làm khai báo dễ đọc

Cú pháp khai báo của C là phần bị chê nhiều nhất của ngôn ngữ, và bài này là chỗ nó khó nhất. Nhưng có một quy tắc duy nhất giải mã được mọi khai báo, dù phức tạp tới đâu, và bài này dạy bạn quy tắc đó.

#Bốn khai báo trông giống nhau

int  *a[5];       /* 1 */
int (*b)[5];      /* 2 */
int  *c(void);    /* 3 */
int (*d)(void);   /* 4 */
Khai báoLà gìsizeof
int *a[5]Mảng 5 con trỏ tới int40
int (*b)[5]Con trỏ tới mảng 5 int8
int *c(void)Hàm trả về con trỏ tới intkhông áp dụng
int (*d)(void)Con trỏ tới hàm trả về int8
bon-khai-bao.c
#include <stdio.h>

int main(void)
{
    int  m[3][5] = { { 0 } };

    int  *a[5];        /* mảng 5 con trỏ */
    int (*b)[5];       /* con trỏ tới mảng 5 int */

    b = m;             /* hợp lệ: m phân rã thành int (*)[5] */

    printf("sizeof a = %zu\n", sizeof a);      /* 40 */
    printf("sizeof b = %zu\n", sizeof b);      /* 8 */
    printf("sizeof *b = %zu\n", sizeof *b);    /* 20, cả một hàng */

    printf("%d\n", (*b)[2]);                   /* phần tử m[0][2] */
    printf("%d\n", b[1][2]);                   /* phần tử m[1][2] */

    return 0;
}

#Quy tắc đọc từ trong ra

Quy tắc xoắn ốc
Bắt đầu từ tên biến, đi sang phải trước, rồi sang trái, rồi lặp lại, mở rộng dần theo cặp ngoặc.
  • Bước 1. Tìm tên biến, đó là điểm xuất phát.
  • Bước 2. Nhìn sang phải. Nếu gặp [] thì đọc là mảng, nếu gặp () thì đọc là hàm.
  • Bước 3. Nếu bên phải là ngoặc đóng thì quay sang trái. Nếu gặp * thì đọc là con trỏ tới.
  • Bước 4. Lặp lại cho tới hết, rồi đọc kiểu cơ sở ở ngoài cùng.
int (*b)[5];

/* Bước 1: b
   Bước 2: bên phải là ngoặc đóng, quay trái
   Bước 3: gặp *   -> b là con trỏ tới
   Bước 4: ra khỏi ngoặc, nhìn phải, gặp [5] -> mảng 5 phần tử
   Bước 5: nhìn trái, gặp int -> kiểu int

   Kết quả: b là con trỏ tới mảng 5 phần tử kiểu int */

Luyện tập với năm khai báo

Khai báoĐọc là
int *pp là con trỏ tới int
int **pp là con trỏ tới con trỏ tới int
int *p[3]p là mảng 3 con trỏ tới int
int (*p)[3]p là con trỏ tới mảng 3 int
int *(*p)[3]p là con trỏ tới mảng 3 con trỏ tới int
int (*p[3])(void)p là mảng 3 con trỏ tới hàm không tham số trả về int
int *(*p(void))[3]p là hàm không tham số, trả về con trỏ tới mảng 3 con trỏ tới int

#Duyệt ma trận bằng con trỏ tới mảng

duyet.c
#include <stdio.h>

enum { COT = 4 };

/* Ba cách viết tương đương cho cùng một nguyên mẫu */
void in_1(int m[3][COT], size_t hang);
void in_2(int m[][COT], size_t hang);
void in_3(int (*m)[COT], size_t hang);

void in_3(int (*m)[COT], size_t hang)
{
    for (size_t i = 0; i < hang; ++i) {
        for (size_t j = 0; j < COT; ++j)
            printf("%4d", m[i][j]);

        putchar('\n');
    }
}

/* Duyệt hoàn toàn bằng con trỏ */
void in_con_tro(int (*m)[COT], size_t hang)
{
    for (int (*hang_p)[COT] = m; hang_p < m + hang; ++hang_p)
        for (int *o = *hang_p; o < *hang_p + COT; ++o)
            printf("%4d", *o);
}

int main(void)
{
    int m[3][COT] = {
        { 1, 2, 3, 4 },
        { 5, 6, 7, 8 },
        { 9, 10, 11, 12 },
    };

    in_3(m, 3);

    return 0;
}
Biểu thức với int (*m)[4]KiểuNghĩa là
mint (*)[4]Con trỏ tới hàng hiện tại
m + 1int (*)[4]Con trỏ tới hàng kế tiếp, cách 16 byte
*mint *Hàng hiện tại, phân rã thành con trỏ phần tử đầu
**mintPhần tử đầu tiên
m[i]int *Hàng thứ i sau phân rã
m[i][j]intPhần tử ở hàng i cột j

#Dùng typedef cho dễ đọc

Khai báo trần
int (*bang[10])(const char *, size_t);

int *(*f(int))[5];

void (*sig(int, void (*)(int)))(int);      /* nguyên mẫu thật của signal */
Tách bằng typedef
typedef int (*XuLy)(const char *, size_t);

XuLy bang[10];                    /* mảng 10 con trỏ hàm, đọc là hiểu ngay */

typedef int *Mang5ConTro[5];

Mang5ConTro *f(int);              /* f trả về con trỏ tới mảng 5 con trỏ int */

typedef void (*XuLyTinHieu)(int);

XuLyTinHieu sig(int, XuLyTinHieu); /* rõ hơn hẳn */
typedef.c
/* Ba typedef hữu ích cho ma trận */
typedef int Hang[4];              /* một hàng 4 phần tử */
typedef Hang *ConTroHang;         /* con trỏ tới hàng */

void in(ConTroHang m, size_t hang)
{
    for (size_t i = 0; i < hang; ++i)
        for (size_t j = 0; j < 4; ++j)
            printf("%4d", m[i][j]);
}

int m[3][4];

in(m, 3);       /* hoạt động y hệt int (*m)[4] */

#Công cụ giải mã khai báo

terminal
# Trên Linux, cài gói cdecl rồi dùng
cdecl explain 'int (*p)[5]'
declare p as pointer to array 5 of int
cdecl explain 'void (*sig(int, void (*)(int)))(int)'
declare sig as function (int, pointer to function (int) returning void) returning pointer to function (int) returning void
# Chiều ngược lại cũng được
cdecl declare p as pointer to array 5 of int
int (*p)[5]

Bài kiểm tra tự đánh giá

/* Đọc năm khai báo sau, viết ra bằng lời rồi đối chiếu với đáp án */
char *(*a)[10];
int (*b[5])(void);
double *(*c)(int, int);
void (*d[3])(char *, size_t);
int *(*(*e)(void))[4];
Khai báoĐáp án
char *(*a)[10]a là con trỏ tới mảng 10 con trỏ tới char
int (*b[5])(void)b là mảng 5 con trỏ tới hàm không tham số trả về int
double *(*c)(int, int)c là con trỏ tới hàm nhận hai int, trả về con trỏ tới double
void (*d[3])(char *, size_t)d là mảng 3 con trỏ tới hàm nhận char sao và size_t, trả về void
int *(*(*e)(void))[4]e là con trỏ tới hàm không tham số, trả về con trỏ tới mảng 4 con trỏ tới int

Tự làm thử

  1. In sizeof của cả bốn khai báo ở đầu bài, giải thích từng con số.
  2. Gán int (*b)[5] bằng một mảng hai chiều, in b + 1 và tính chênh lệch byte.
  3. Đọc năm khai báo trong bài kiểm tra bằng quy tắc xoắn ốc, đối chiếu với bảng đáp án.
  4. Viết hàm in ma trận với cả ba dạng nguyên mẫu tương đương, xác nhận cả ba chạy giống nhau.
  5. Duyệt ma trận hoàn toàn bằng con trỏ tới mảng, không dùng ngoặc vuông nào.
  6. Viết lại ba khai báo phức tạp nhất trong bài bằng typedef, so sánh độ dễ đọc.
  7. Dùng cdecl hoặc trang cdecl.org để kiểm tra năm khai báo bạn tự nghĩ ra.

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

  • int *a[5] là mảng con trỏ, int (*b)[5] là con trỏ tới mảng. Cặp ngoặc đổi hoàn toàn ý nghĩa.
  • Đọc khai báo bằng quy tắc xoắn ốc: từ tên biến, sang phải trước, sang trái sau, mở rộng theo cặp ngoặc.
  • Mảng hai chiều phân rã thành int (*)[N], và đó là lý do số cột bắt buộc phải ghi còn số hàng thì không.
  • Dùng typedef để tách khai báo phức tạp, nhưng nhớ rằng const áp lên typedef con trỏ không chui vào bên trong.
  • Dùng cdecl khi gặp khai báo lạ, và coi việc phải dùng nó cho mã của chính mình là dấu hiệu cần viết lại.