Bỏ qua điều hướng, tới nội dung chính
Học C
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

Cộng 2 vào con trỏ int nhảy tám byte chứ không phải hai. Bước nhảy luôn nhân với sizeof của kiểu.
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ểup + 1 nhảy bao nhiêu byte
char *11
short *22
int *44
double *88
struct S * với S rộng 24 byte2424
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ểucù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ánhHợp lệ khi nào
p == q, p != qLuôn hợp lệ với hai con trỏ cùng kiểu
p < q, p > q, p <= q, p >= qChỉ khi cả hai trỏ vào cùng một mảng, hoặc tới ngay sau phần tử cuối
p == NULLLuôn hợp lệ
p < NULLKhô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épVí dụVì sao không
Cộng hai con trỏp + qKhông có ý nghĩa nào cả
Nhân hoặc chia con trỏp * 2Cùng lý do
Phép toán bitp & 0xFFKhông hợp lệ, dù đôi khi gặp trong mã hệ thống
Số học trên void saov + 1Khô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àmf + 1Hà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ốngHợp lệGhi chú
a + 0 tới a + n với int a[n]Bao gồm cả con trỏ quá cuối
a - 1KhôngĐi trước phần tử đầu
a + n + 1Khô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 xBiến đơn coi như mảng một phần tử
So sánh con trỏ vào hai mảng khác nhauKhôngThự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ử

  1. In khoảng cách byte của p + 1 so với p cho bốn kiểu khác nhau, đối chiếu với sizeof.
  2. Viết p = p + 10 * sizeof *p rồi in địa chỉ, so với p = p + 10 và giải thích chênh lệch.
  3. Dùng hiệu hai con trỏ để cài lại strlen, kiểm thử với chuỗi rỗng.
  4. 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.
  5. 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=undefined và đọc thông báo.
  6. 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.
  7. 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 -O0 và 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 n vào con trỏ nhảy n * sizeof byte. Đừng nhân thêm sizeof lầ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.