Truy cập phần tử
Sau bài này bạn sẽ làm được
- Chứng minh a[i] và *(a + i) là cùng một thứ
- Tính địa chỉ phần tử theo công thức
- Tránh bẫy so sánh size_t với số âm
- Viết vòng lặp duyệt ngược không bị tràn ngầm
Truy cập phần tử là thao tác bạn sẽ viết nhiều nhất khi làm việc với mảng. Cú pháp thì đơn giản, nhưng phía sau nó là một sự thật về con trỏ, và đi kèm là một cái bẫy về kiểu chỉ số mà rất nhiều người vấp.
#Cú pháp truy cập
int a[5] = { 10, 20, 30, 40, 50 };
printf("%d\n", a[0]); /* 10 */
printf("%d\n", a[4]); /* 50 */
a[2] = 99; /* ghi vào phần tử thứ ba */
int i = 3;
printf("%d\n", a[i]); /* chỉ số có thể là biến hoặc biểu thức */
printf("%d\n", a[i - 1]); /* 99, vì a[2] vừa bị gán ở trên */Chỉ số có thể là bất kỳ biểu thức nguyên nào. Nó được tính trước, rồi mới dùng để tính địa chỉ. Điều này nghĩa là a[f()] hợp lệ, và cũng nghĩa là a[i++] hợp lệ nhưng nên tránh vì khó đọc.
#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, *(i + a) cũng đúng, và từ đó suy ra i[a] cũng hợp lệ. Cách viết cuối là trò đùa của ngôn ngữ, tuyệt đối đừng dùng, nhưng nó cho thấy rất rõ bản chất.
#include <stdio.h>
int main(void)
{
int a[5] = { 10, 20, 30, 40, 50 };
int i = 2;
printf("%d %d %d %d\n", a[i], *(a + i), *(i + a), i[a]);
return 0;
}30 30 30 30
#Bẫy chỉ số không dấu
Kiểu size_t là kiểu chuẩn cho chỉ số và kích thước. Nó không dấu, nghĩa là không bao giờ mang giá trị âm. Đó là ưu điểm nhưng cũng là nguồn của hai cái bẫy.
Bẫy 1: điều kiện luôn đúng
size_t i = 0;
while (i >= 0) { /* size_t luôn lớn hơn hoặc bằng 0, vòng lặp vô hạn */
a[i] = 0;
i--; /* khi i = 0, i-- cho ra 18446744073709551615 */
}Bẫy 2: trừ ra số khổng lồ
size_t n = 0;
for (size_t i = 0; i < n - 1; ++i) /* n - 1 khi n = 0 là 18446744073709551615 */
printf("%d ", a[i]); /* chạy hơn mười tám tỷ tỷ lượt */| Viết thế này | Hỏng khi | Viết thế này thay vào |
|---|---|---|
| i < n - 1 | n bằng 0 | i + 1 < n |
| i <= n - 1 | n bằng 0 | i < n |
| i >= 0 với size_t | luôn luôn | dùng vòng lặp ngược ở mục dưới |
| j < n - i - 1 | i bằng n | j + i + 1 < n |
#Duyệt ngược cho đúng
/* 1. Lặp vô hạn */
for (size_t i = n - 1; i >= 0; --i) { }
/* 2. Hỏng khi n bằng 0 */
size_t i = n - 1;
while (i > 0) { xu_ly(a[i]); --i; }
xu_ly(a[0]);
/* 3. Dùng int nên hỏng khi n lớn hơn giới hạn của int */
for (int i = (int)n - 1; i >= 0; --i) { }/* Cách 1: thành ngữ i-- > 0, đúng với mọi n kể cả 0 */
for (size_t i = n; i-- > 0; ) {
printf("%d ", a[i]);
}
/* Cách 2: lệch một, dễ đọc hơn với người mới */
for (size_t k = 0; k < n; ++k) {
size_t i = n - 1 - k;
printf("%d ", a[i]);
}Thành ngữ i-- > 0 hoạt động vì phép so sánh dùng giá trị trước khi giảm, còn thân vòng lặp dùng giá trị sau khi giảm. Với n bằng 5, biến i trong thân nhận lần lượt 4, 3, 2, 1, 0. Khi i bằng 0, phép so sánh cho sai và vòng lặp dừng, đúng lúc.
| Lượt | i trước | so sánh | i trong thân |
|---|---|---|---|
| 1 | 5 | 5 > 0 đúng | 4 |
| 2 | 4 | 4 > 0 đúng | 3 |
| 3 | 3 | 3 > 0 đúng | 2 |
| 4 | 2 | 2 > 0 đúng | 1 |
| 5 | 1 | 1 > 0 đúng | 0 |
| 6 | 0 | 0 > 0 sai | thoát |
#Đổi chỗ hai phần tử
Thao tác này xuất hiện trong mọi thuật toán sắp xếp, trong đảo ngược mảng, trong xáo trộn ngẫu nhiên. Đáng để viết đúng một lần rồi dùng lại.
static void doi_cho(int *a, size_t i, size_t j)
{
int t = a[i];
a[i] = a[j];
a[j] = t;
}void dao_nguoc(int *a, size_t n)
{
for (size_t i = 0; i + 1 < n - i; ++i)
doi_cho(a, i, n - 1 - i);
}
/* Cách viết bằng hai đầu chạy vào giữa, thường gặp hơn */
void dao_nguoc2(int *a, size_t n)
{
if (n == 0) return;
size_t trai = 0, phai = n - 1;
while (trai < phai) {
doi_cho(a, trai, phai);
trai++;
phai--;
}
}Trong dao_nguoc2, chú ý dòng bảo vệ if (n == 0) return;. Không có nó, n - 1 với size_t cho ra số khổng lồ và vòng lặp phá tan bộ nhớ. Đây chính là cái bẫy ở mục trên, xuất hiện trong một hàm rất bình thường.
Tự làm thử
- In cùng một phần tử bằng cả bốn cách viết tương đương và xác nhận kết quả giống nhau.
- Viết vòng lặp
for (size_t i = 0; i < n - 1; ++i)rồi gọi vớinbằng 0. Chạy dưới-fsanitize=addressvà đọc thông báo. - Cài
dao_nguoctheo cả hai cách trong bài, thử với mảng rỗng, mảng một phần tử và mảng số chẵn số lẻ phần tử. - Viết hàm xoay mảng sang trái k vị trí bằng kỹ thuật đảo ba lần: đảo k phần tử đầu, đảo phần còn lại, rồi đảo cả mảng.
- Đo khoảng cách byte giữa
a + 1vàavới mảngchar,int,doublevà một struct tự định nghĩa.
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
a[i]theo định nghĩa là*(a + i), và số học con trỏ đếm theo phần tử chứ không theo byte.size_tkhông dấu, nêni >= 0luôn đúng vàn - 1vớinbằng 0 cho ra số khổng lồ.- Đổi
i < n - 1thànhi + 1 < nđể an toàn với mọi giá trị của n. - Duyệt ngược bằng thành ngữ
for (size_t i = n; i-- > 0; ). - Đổi chỗ bằng biến tạm, không dùng mẹo XOR vì nó hỏng khi hai chỉ số trùng nhau.