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áo | Là gì | sizeof |
|---|---|---|
| int *a[5] | Mảng 5 con trỏ tới int | 40 |
| int (*b)[5] | Con trỏ tới mảng 5 int | 8 |
| int *c(void) | Hàm trả về con trỏ tới int | không áp dụng |
| int (*d)(void) | Con trỏ tới hàm trả về int | 8 |
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 *p | p là con trỏ tới int |
| int **p | p 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ểu | Nghĩa là |
|---|---|---|
| m | int (*)[4] | Con trỏ tới hàng hiện tại |
| m + 1 | int (*)[4] | Con trỏ tới hàng kế tiếp, cách 16 byte |
| *m | int * | Hàng hiện tại, phân rã thành con trỏ phần tử đầu |
| **m | int | Phần tử đầu tiên |
| m[i] | int * | Hàng thứ i sau phân rã |
| m[i][j] | int | Phầ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ử
- In
sizeofcủa cả bốn khai báo ở đầu bài, giải thích từng con số. - Gán
int (*b)[5]bằng một mảng hai chiều, inb + 1và tính chênh lệch byte. - Đọ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.
- 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.
- 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.
- 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. - Dùng
cdeclhoặc trangcdecl.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ằngconstáp lên typedef con trỏ không chui vào bên trong. - Dùng
cdeclkhi 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.