Bỏ qua điều hướng, tới nội dung chính
Học C
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ếtTrì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 sizeofsizeof aKích thước cả mảng, 20 với int a[5]
Toán hạng của toán tử và&aCon 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ệuTám byte chứa một địa chỉ
sizeof208
Gán lại được khôngKhông, a không phải biến gán được
Trỏ đi chỗ khácKhông bao giờ
Cần khởi tạo khôngKhông, vùng nhớ đã có sẵnCó, 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 saiChính xác thì là
Mảng là con trỏSaiMả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ằngSaiTê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ĐúngCả hai đều là *(x + i)
Truyền mảng vào hàm là truyền con trỏĐúngMảng phân rã trước khi truyền
sizeof của mảng và con trỏ giống nhauSaiChỉ 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ọnKhi 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ử

  1. 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ị.
  2. In sizeof của a, &a, a[0] &a[0], giải thích bốn con số.
  3. In a + 1&a + 1, tính chênh lệch byte và giải thích vì sao khác nhau.
  4. Gọi sizeof trên tham số mảng bên trong hàm, đọc cảnh báo của trình biên dịch.
  5. Thử gán a = b giữa hai mảng và a++, chép lại hai thông báo lỗi.
  6. Tạo hai file, một file định nghĩa int a[100] và file kia khai báo extern int *a. Chạy và quan sát hậu quả.
  7. 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ự.
  • a&a cù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 + n là hợp lệ để so sánh nhưng không được đọc. Biểu thức a - 1 thì không hợp lệ.