Bài 12.820 phút đọc
Số học con trỏ
Sau bài này bạn sẽ làm được
- Tính đúng kết quả của phép cộng và trừ con trỏ
- Dùng hiệu hai con trỏ để tính số phần tử
- Biết phép toán nào trên con trỏ là hợp lệ và phép nào không
- Hiểu quy tắc chỉ được đi tới ngay sau phần tử cuối
Số học con trỏ là thứ làm C mạnh và cũng làm C nguy hiểm. Quy tắc cốt lõi chỉ có một câu: mọi phép toán đếm theo phần tử, không đếm theo byte. Nhưng quanh câu đó là một loạt giới hạn của chuẩn mà rất ít người học biết.
#Cộng và trừ một số nguyên
int a[5];
int *p = a;
p + 1 /* địa chỉ của a[1], cách p đúng sizeof(int) byte */
p + 3 /* địa chỉ của a[3] */
p - 1 /* địa chỉ của phần tử trước p */
++p /* p tiến một phần tử */
p += 2 /* p tiến hai phần tử */| Kiểu con trỏ | sizeof kiểu | p + 1 nhảy bao nhiêu byte |
|---|---|---|
| char * | 1 | 1 |
| short * | 2 | 2 |
| int * | 4 | 4 |
| double * | 8 | 8 |
| struct S * với S rộng 24 byte | 24 | 24 |
buoc-nhay.c
#include <stdio.h>
typedef struct { double a, b, c; } Ba; /* 24 byte */
int main(void)
{
char c[3];
int i[3];
double d[3];
Ba s[3];
printf("char : %td\n", (char *)(c + 1) - (char *)c);
printf("int : %td\n", (char *)(i + 1) - (char *)i);
printf("double : %td\n", (char *)(d + 1) - (char *)d);
printf("struct : %td\n", (char *)(s + 1) - (char *)s);
return 0;
}terminal
./buoc-nhay
char : 1 int : 4 double : 8 struct : 24
#Hiệu hai con trỏ
Hiệu hai con trỏ
Trừ hai con trỏ cùng kiểu và cùng trỏ vào một mảng cho ra số phần tử giữa chúng. Kiểu kết quả là
ptrdiff_t, một kiểu nguyên có dấu.hieu.c
#include <stddef.h>
#include <stdio.h>
int main(void)
{
int a[10];
int *p = &a[2];
int *q = &a[7];
ptrdiff_t d = q - p;
printf("%td\n", d); /* 5, không phải 20 */
printf("%td\n", p - q); /* -5, kết quả có dấu */
return 0;
}#So sánh con trỏ
int a[10];
int *p = &a[2];
int *q = &a[7];
p < q /* đúng, so sánh vị trí trong mảng */
p == q /* sai */
p != NULL /* so với con trỏ rỗng, luôn hợp lệ */| Phép so sánh | Hợp lệ khi nào |
|---|---|
| p == q, p != q | Luôn hợp lệ với hai con trỏ cùng kiểu |
| p < q, p > q, p <= q, p >= q | Chỉ khi cả hai trỏ vào cùng một mảng, hoặc tới ngay sau phần tử cuối |
| p == NULL | Luôn hợp lệ |
| p < NULL | Không xác định, đừng bao giờ viết |
vong-lap.c
int a[5] = { 1, 2, 3, 4, 5 };
size_t n = 5;
/* Mẫu chuẩn: dừng khi chạm địa chỉ ngay sau phần tử cuối */
for (int *p = a; p < a + n; ++p)
printf("%d ", *p);
/* Thu hẹp từ hai đầu */
int *trai = a;
int *phai = a + n - 1;
while (trai < phai) {
int t = *trai;
*trai = *phai;
*phai = t;
++trai;
--phai;
}#Những phép không được phép
| Phép | Ví dụ | Vì sao không |
|---|---|---|
| Cộng hai con trỏ | p + q | Không có ý nghĩa nào cả |
| Nhân hoặc chia con trỏ | p * 2 | Cùng lý do |
| Phép toán bit | p & 0xFF | Không hợp lệ, dù đôi khi gặp trong mã hệ thống |
| Số học trên void sao | v + 1 | Không biết sizeof, GCC cho phép như phần mở rộng và coi là 1 |
| Số học trên con trỏ hàm | f + 1 | Hàm không phải mảng nên không có phần tử kế tiếp |
int *p, *q;
p + q; /* error: invalid operands to binary + */
p * 2; /* error */
p / 2; /* error */
p % 2; /* error */
void *v;
v + 1; /* GCC cho phép và coi như char *, nhưng không chuẩn */
int (*f)(void);
f + 1; /* error trên trình biên dịch chuẩn */Ép về số nguyên rồi tính toán
#include <stdint.h>
/* Đôi khi mã hệ thống cần căn chỉnh địa chỉ về bội của 8.
Cách làm chuẩn là ép về uintptr_t, tính, rồi ép ngược lại. */
void *can_chinh(void *p, size_t can)
{
uintptr_t u = (uintptr_t)p;
u = (u + can - 1) & ~(uintptr_t)(can - 1);
return (void *)u;
}#Giới hạn của chuẩn
Quy tắc trong cùng một mảng
Mọi phép số học và so sánh thứ tự trên con trỏ chỉ được định nghĩa khi cả hai con trỏ trỏ vào cùng một mảng, hoặc trỏ tới ngay sau phần tử cuối của mảng đó. Một biến đơn được coi là mảng một phần tử.
| Tình huống | Hợp lệ | Ghi chú |
|---|---|---|
| a + 0 tới a + n với int a[n] | Có | Bao gồm cả con trỏ quá cuối |
| a - 1 | Không | Đi trước phần tử đầu |
| a + n + 1 | Không | Đi quá một bước sau con trỏ quá cuối |
| *(a + n) | Không | Đọc qua con trỏ quá cuối |
| &x + 1 với int x | Có | Biến đơn coi như mảng một phần tử |
| So sánh con trỏ vào hai mảng khác nhau | Không | Thực tế vẫn chạy nhưng không xác định |
Ba cách viết vi phạm chuẩn
/* 1. Duyệt ngược chạm a - 1 */
for (int *p = a + n - 1; p >= a; --p)
...
/* 2. Kiểm tra biên sau khi đã đi ra ngoài */
p += k;
if (p > a + n) p = a + n; /* p đã ra ngoài trước khi kiểm tra */
/* 3. So sánh hai mảng khác nhau */
if (p >= a && p < b) ...Ba cách viết an toàn
/* 1. Thành ngữ giảm trong điều kiện */
for (int *p = a + n; p-- > a; )
...
/* 2. Kiểm tra trước khi cộng */
if (k <= (size_t)(a + n - p))
p += k;
/* 3. So sánh bằng chỉ số thay vì bằng con trỏ */
size_t i = (size_t)(p - a);
if (i < n) ...terminal
# Trình dò hành vi không xác định bắt được vài trường hợp
gcc -std=c17 -g -fsanitize=undefined,address bai.c -o bai && ./bai
bai.c:12:15: runtime error: index -1 out of bounds for type 'int [5]'
Tự làm thử
- In khoảng cách byte của
p + 1so vớipcho bốn kiểu khác nhau, đối chiếu vớisizeof. - Viết
p = p + 10 * sizeof *prồi in địa chỉ, so vớip = p + 10và giải thích chênh lệch. - Dùng hiệu hai con trỏ để cài lại
strlen, kiểm thử với chuỗi rỗng. - Trừ hai con trỏ vào hai mảng khác nhau, in kết quả, rồi thêm một biến giữa hai mảng và chạy lại.
- Viết vòng lặp duyệt ngược bằng dấu lớn hơn hoặc bằng, chạy dưới
-fsanitize=undefinedvà đọc thông báo. - Cài hàm căn chỉnh địa chỉ về bội của 8 và 16, kiểm chứng bằng cách in địa chỉ dạng thập lục phân.
- Viết chương trình so sánh thời gian duyệt mảng mười triệu phần tử bằng chỉ số và bằng con trỏ, với
-O0và với-O2.
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
- Cộng
nvào con trỏ nhảyn * sizeofbyte. Đừng nhân thêmsizeoflần nữa. - Hiệu hai con trỏ cho ra số phần tử, kiểu
ptrdiff_t, in bằng%td. - So sánh thứ tự chỉ hợp lệ khi hai con trỏ cùng trỏ vào một mảng. So sánh bằng thì luôn hợp lệ.
- Địa chỉ ngay sau phần tử cuối là hợp lệ để tính và so sánh nhưng không được đọc. Địa chỉ trước phần tử đầu thì không hợp lệ.
- Không cộng, nhân, chia hai con trỏ. Muốn làm toán trên địa chỉ thì ép về uintptr_t.