Bài 12.720 phút đọc
Con trỏ và mảng
Sau bài này bạn sẽ làm được
- Giải thích chính xác cơ chế phân rã mảng
- Liệt kê ba trường hợp mảng không phân rã
- Phân biệt mảng với con trỏ trên năm tiêu chí
- Duyệt mảng bằng con trỏ thành thạo
Câu nói mảng và con trỏ là một thứ vừa đúng vừa sai, và nó gây hiểu nhầm cho rất nhiều người học. Bài này nói chính xác chúng giống nhau ở đâu, khác nhau ở đâu, và vì sao phân biệt được điều đó là quan trọng.
#Cơ chế phân rã mảng
Phân rã mảng
Trong hầu hết các biểu thức, tên mảng tự động được chuyển thành một con trỏ tới phần tử đầu tiên. Chuyển đổi này xảy ra âm thầm, không có cú pháp nào cho nó.
int a[5];
a /* trong biểu thức, tương đương &a[0], kiểu int * */
a + 1 /* tương đương &a[1] */
*a /* tương đương a[0] */phan-ra.c
#include <stdio.h>
int main(void)
{
int a[5] = { 10, 20, 30, 40, 50 };
printf("a = %p\n", (void *)a);
printf("&a[0] = %p\n", (void *)&a[0]);
printf("bang nhau: %d\n", a == &a[0]); /* 1 */
int *p = a; /* không cần dấu và */
printf("*p = %d\n", *p); /* 10 */
printf("p[3] = %d\n", p[3]); /* 40 */
return 0;
}#Bốn cách viết tương đương
a[i] == *(a + i) == *(i + a) == i[a]Chuẩn C định nghĩa a[i] chính xác là viết tắt của *(a + i). Vì phép cộng có tính giao hoán, ba dạng còn lại cũng hợp lệ. Dạng cuối là trò đùa của ngôn ngữ, đừng bao giờ viết, nhưng nó chứng minh rõ nhất rằng ngoặc vuông chỉ là cú pháp đường.
| Viết | Trình biên dịch hiểu là | Dùng được với |
|---|---|---|
| a[i] | *(a + i) | Cả mảng lẫn con trỏ |
| p[i] | *(p + i) | Cả mảng lẫn con trỏ |
| *(a + i) | chính nó | Cả hai |
| i[a] | *(i + a) | Cả hai, nhưng đừng viết |
bon-cach.c
int a[5] = { 10, 20, 30, 40, 50 };
int *p = a;
/* Tám cách viết sau đều cho ra 30 */
a[2]; *(a + 2); *(2 + a); 2[a];
p[2]; *(p + 2); *(2 + p); 2[p];#Ba chỗ mảng không phân rã
| Chỗ | Ví dụ | Kết quả |
|---|---|---|
| Toán hạng của sizeof | sizeof a | Kích thước cả mảng, 20 với int a[5] |
| Toán hạng của toán tử và | &a | Con trỏ tới cả mảng, kiểu int (*)[5] |
| Hằng chuỗi khởi tạo mảng ký tự | char s[] = "abc"; | Chép nội dung vào s chứ không lấy địa chỉ |
khong-phan-ra.c
#include <stdio.h>
int main(void)
{
int a[5];
printf("sizeof a = %zu\n", sizeof a); /* 20 */
printf("sizeof &a[0]= %zu\n", sizeof &a[0]); /* 8, con trỏ */
printf("sizeof a[0] = %zu\n", sizeof a[0]); /* 4 */
printf("a = %p\n", (void *)a);
printf("&a = %p\n", (void *)&a); /* cùng giá trị */
printf("a + 1 = %p\n", (void *)(a + 1)); /* cách a 4 byte */
printf("&a + 1= %p\n", (void *)(&a + 1)); /* cách a 20 byte */
return 0;
}terminal
./khong-phan-ra
sizeof a = 20 sizeof &a[0]= 8 sizeof a[0] = 4 a = 0x7ffd3a2c1e30 &a = 0x7ffd3a2c1e30 a + 1 = 0x7ffd3a2c1e34 &a + 1= 0x7ffd3a2c1e44
Hệ quả trong hàm
void f(int a[])
{
printf("%zu\n", sizeof a); /* 8, vì a ở đây là int * */
}
int main(void)
{
int a[5];
printf("%zu\n", sizeof a); /* 20, vì a vẫn là mảng */
f(a);
return 0;
}terminal
gcc -std=c17 -Wall bai.c -o bai
bai.c:3:21: warning: 'sizeof' on array function parameter 'a' will return size of 'int *' [-Wsizeof-array-argument]
#Mảng khác con trỏ ở đâu
| Tiêu chí | int a[5] | int *p |
|---|---|---|
| Là gì | Hai mươi byte chứa dữ liệu | Tám byte chứa một địa chỉ |
| sizeof | 20 | 8 |
| Gán lại được không | Không, a không phải biến gán được | Có |
| Trỏ đi chỗ khác | Không bao giờ | Có |
| Cần khởi tạo không | Không, vùng nhớ đã có sẵn | Có, nếu không thì là con trỏ hoang |
khac-nhau.c
int a[5] = { 1, 2, 3, 4, 5 };
int b[5];
int *p;
p = a; /* hợp lệ: con trỏ nhận địa chỉ */
p = b; /* hợp lệ: trỏ sang mảng khác */
p++; /* hợp lệ: con trỏ tiến lên */
a = b; /* LỖI: assignment to expression with array type */
a++; /* LỖI: cùng lý do */
a = p; /* LỖI */Bảng tổng kết đúng và sai
| Câu nói | Đúng hay sai | Chính xác thì là |
|---|---|---|
| Mảng là con trỏ | Sai | Mảng phân rã thành con trỏ trong hầu hết biểu thức |
| Tên mảng là một con trỏ hằng | Sai | Tên mảng không phải biến, nó là tên của một vùng nhớ |
| a[i] và p[i] cùng cú pháp | Đúng | Cả hai đều là *(x + i) |
| Truyền mảng vào hàm là truyền con trỏ | Đúng | Mảng phân rã trước khi truyền |
| sizeof của mảng và con trỏ giống nhau | Sai | Chỉ giống khi mảng đã phân rã |
#Duyệt mảng bằng con trỏ
duyet.c
int a[5] = { 10, 20, 30, 40, 50 };
size_t n = sizeof a / sizeof a[0];
/* Cách 1: chỉ số */
for (size_t i = 0; i < n; ++i)
printf("%d ", a[i]);
/* Cách 2: con trỏ chạy */
for (int *p = a; p < a + n; ++p)
printf("%d ", *p);
/* Cách 3: hai con trỏ đầu cuối */
int *dau = a;
int *cuoi = a + n;
while (dau < cuoi)
printf("%d ", *dau++);
/* Cách 4: duyệt ngược bằng con trỏ */
for (int *p = a + n; p-- > a; )
printf("%d ", *p);Ba lỗi khi duyệt bằng con trỏ
/* 1. Đi quá một bước */
for (int *p = a; p <= a + n; ++p)
printf("%d ", *p); /* đọc a[n], ngoài biên */
/* 2. Quên con trỏ đã bị đổi */
for (int *p = a; p < a + n; ++p)
;
printf("%d\n", *p); /* p giờ trỏ ra ngoài, và cũng ngoài phạm vi */
/* 3. Duyệt ngược bằng dấu lớn hơn hoặc bằng */
for (int *p = a + n - 1; p >= a; --p)
printf("%d ", *p); /* lượt cuối tính a - 1, không xác định */Ba cách sửa
/* 1. Dùng dấu bé hơn */
for (int *p = a; p < a + n; ++p)
printf("%d ", *p);
/* 2. Giữ một con trỏ riêng nếu cần dùng sau vòng lặp */
int *cuoi = a;
while (cuoi < a + n && *cuoi != 0)
++cuoi;
/* 3. Duyệt ngược bằng thành ngữ giảm trong điều kiện */
for (int *p = a + n; p-- > a; )
printf("%d ", *p);Khi nào chọn con trỏ, khi nào chọn chỉ số
| Chọn | Khi nào |
|---|---|
| Chỉ số | Cần biết vị trí, cần duyệt song song nhiều mảng, cần đọc dễ |
| Con trỏ | Làm việc với chuỗi, thu hẹp từ hai đầu, hoặc viết hàm thư viện |
Về tốc độ thì hai cách gần như bằng nhau với trình biên dịch hiện đại bật tối ưu. Bài 9.4 đã nói điều này. Hãy chọn theo độ rõ ràng của ý định.
Tự làm thử
- In tám cách viết cùng một phần tử trong bài, xác nhận tất cả cho ra cùng giá trị.
- In
sizeofcủaa,&a,a[0]và&a[0], giải thích bốn con số. - In
a + 1và&a + 1, tính chênh lệch byte và giải thích vì sao khác nhau. - Gọi
sizeoftrên tham số mảng bên trong hàm, đọc cảnh báo của trình biên dịch. - Thử gán
a = bgiữa hai mảng vàa++, chép lại hai thông báo lỗi. - Tạo hai file, một file định nghĩa
int a[100]và file kia khai báoextern int *a. Chạy và quan sát hậu quả. - Cài đủ bốn cách duyệt trong bài, xác nhận cùng kết quả, rồi đo thời gian trên mảng mười triệu phần tử.
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
- Trong hầu hết biểu thức, tên mảng phân rã thành con trỏ tới phần tử đầu. Đó là lý do mọi thứ trông giống nhau.
- Ba chỗ không phân rã: toán hạng của
sizeof, toán hạng của&, và hằng chuỗi khởi tạo mảng ký tự. avà&acùng giá trị nhưng khác kiểu, nên cộng 1 vào chúng cho hai kết quả khác nhau.- Mảng không phải con trỏ: không gán lại được, sizeof khác nhau, và khai báo extern phải khớp đúng kiểu.
- Biểu thức
a + nlà hợp lệ để so sánh nhưng không được đọc. Biểu thứca - 1thì không hợp lệ.