← Lập trình C nâng cao

Bài 4 · Nâng cao · 24 phút· Cập nhật 11/06/2026

Con trỏ & mảng

Biên soạn bởi Nguyễn Anh Tuấn

Con trỏ và mảng trong C (pointer & array): tên mảng là địa chỉ phần tử đầu, số học con trỏ, a[i] = *(a+i) và bẫy mảng suy biến (decay) khi truyền vào hàm.

Điểm mấu chốt: tên mảng là địa chỉ của phần tử đầu tiên. Viết a tương đương &a[0]. Vì thế gán một mảng cho con trỏ không cần dấu &:

ten-mang.c

#include <stdio.h>

int main() {
    int a[4] = {10, 20, 30, 40};
    int *p = a;                  // a la dia chi a[0]; khong can &

    printf("%d\n", *p);          // 10  - phan tu dau
    printf("%d\n", a == &a[0]);  // 1   - dung, bang nhau
    return 0;
}

Kết quả khi chạy

10
1
  • a ≡ &a[0] - tên mảng là địa chỉ phần tử đầu.
  • int *p = a; hợp lệ và KHÔNG cần & (mảng đã là địa chỉ).
  • Với biến thường vẫn phải &x; chỉ mảng mới đặc biệt thế này.

Phép lập chỉ số a[i] chỉ là đường tắt cho số học con trỏ: *(a + i). Kéo k để di chuyển con trỏ p = a + k dọc mảng và xem ba cách viết luôn cho cùng một ô:

int a[4] = { 10, 20, 30, 40 };
▼ p
a[0]
10
0x100
a[1]
20
0x104
a[2]
30
0x108
a[3]
40
0x10C
p = a + 0 = 0x100 (= 0x100 + 0×4 byte)
a[0] ≡ *(a + 0) ≡ *p = 10

Tăng k lên 1, địa chỉ nhảy 4 byte (= sizeof int) chứ không phải 1 - con trỏ “biết” kích thước phần tử. Đó là lý do a[i]*(a + i) luôn cho cùng một ô.

index-vs-ptr.c

int a[4] = {10, 20, 30, 40};

printf("%d\n", a[2]);       // 30
printf("%d\n", *(a + 2));   // 30  - giong het a[2]

Kết quả khi chạy

30
30

Điểm tinh tế: p + 1 không cộng 1 vào địa chỉ - nó nhảy sizeof(phần tử) byte. Con trỏ biết kiểu nó trỏ tới, nên luôn rơi đúng phần tử kế tiếp:

ptr-math.c

int a[4] = {10, 20, 30, 40};
int *p = a;            // p tro a[0]  (vd 0x100)

printf("%d\n", *p);    // 10
p = p + 1;             // nhay 4 byte (sizeof int) -> a[1] (0x104)
printf("%d\n", *p);    // 20

Kết quả khi chạy

10
20
  • p + i → địa chỉ + i × sizeof(*p) byte (KHÔNG phải + i byte).
  • int* nhảy 4, double* nhảy 8, char* nhảy 1 mỗi bước.
  • Nhờ vậy a + i luôn trỏ đúng a[i] dù phần tử to nhỏ thế nào.

Hai cách duyệt mảng tương đương: bằng chỉ số a[i] hoặc bằng con trỏ chạy p++ từ a tới a + n. Đây cũng đúng cách duyệt một chuỗi bằng con trỏ char:

duyet-con-tro.c

#include <stdio.h>

int main() {
    int a[4] = {10, 20, 30, 40};
    for (int *p = a; p < a + 4; p++) {  // p chay tu a[0] toi a[3]
        printf("%d ", *p);
    }
    printf("\n");
    return 0;
}

Kết quả khi chạy

10 20 30 40

Mảng và con trỏ rất giống nhưng không đồng nhất. Khác biệt lộ ra ở sizeof:

sizeof.c

int a[4];
int *p = a;

printf("%zu\n", sizeof(a));   // 16  - ca mang (4 * 4 byte)
printf("%zu\n", sizeof(p));   // 8   - chi 1 con tro

Kết quả khi chạy

16
8

Mảng “suy biến” khi truyền vào hàm

Khi truyền mảng vào hàm, nó suy biến (decay) thành con trỏ: void f(int arr[]) thực chất là void f(int *arr). Bên trong, sizeof(arr) = 8 (con trỏ), không phải kích thước mảng - nên bạn luôn phải truyền thêm số phần tử n để hàm biết khi nào dừng.

Câu hỏi thường gặp

Vì bản thân tên mảng a ĐÃ là một địa chỉ - địa chỉ của phần tử đầu a[0]. Viết a tương đương &a[0]. Với một biến thường thì cần &x, nhưng mảng thì không.

Ôn lại: Bài Mảng & chuỗi - khoá C cơ bản →

Có. Trình biên dịch DỊCH a[i] thành *(a + i). Vì phép cộng giao hoán nên *(a + i) = *(i + a), tức i[a] cũng hợp lệ (đừng viết thế, nhưng nó cho thấy a[i] thuần tuý là số học con trỏ).

Nhảy sizeof(*p) byte - kích thước MỘT phần tử. int* nhảy 4, double* nhảy 8, char* nhảy 1. Con trỏ “biết” kiểu nó trỏ tới, nên a + i luôn rơi đúng vào phần tử thứ i dù mỗi phần tử to nhỏ khác nhau.

Giá trị (con số địa chỉ) giống nhau, nhưng KIỂU khác: aint * (trỏ tới một int), còn &aint (*)[4] (trỏ tới CẢ mảng). Hệ quả: a + 1 nhảy 4 byte (một int), nhưng &a + 1 nhảy 16 byte (cả mảng). Chi tiết nhỏ, nhưng giải thích nhiều lỗi khó hiểu.

a là một MẢNG thật - trình biên dịch biết nó gồm 4 int nên sizeof(a) = 16. p chỉ là một con trỏ, sizeof(p) = 8 (kích thước một địa chỉ trên 64-bit). Đừng dùng sizeof để đếm phần tử qua một con trỏ!

Mảng “suy biến” (decay) thành con trỏ tới phần tử đầu. void f(int arr[]) thực chất là void f(int *arr). Bên trong hàm, sizeof(arr) = 8 (con trỏ), KHÔNG phải kích thước mảng → bạn phải truyền thêm số phần tử n.

Bài Con trỏ & hàm →

Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.

Tick những điều em tự tin làm được sau khi học bài này. 0/6

Trả lời vài câu để chắc rằng em đã nắm bài.

Câu 1/3 Điểm: 0

Với int a[4]; biểu thức a[i] tương đương cách viết nào bằng con trỏ?

  1. 1

    Địa chỉ của p

    Dùng công cụ ở Bước 2 với k = 2 (mảng bắt đầu ở 0x100). p = a + 2 có địa chỉ gì? *p bằng bao nhiêu?

    Hoàn thành khi: 0x108 (= 0x100 + 2×4) và *p = 30. Cộng 2 nhảy 8 byte, không phải 2.

  2. 2

    Viết lại bằng con trỏ

    Viết a[3] dưới dạng dùng dấu * (dereference).

    Hoàn thành khi: *(a + 3) - hoàn toàn tương đương a[3].

  3. 3

    Bước nhảy

    p + 1 nhảy bao nhiêu byte nếu pint*? Nếu pdouble* (double = 8 byte)?

    Hoàn thành khi: int*: 4 byte. double*: 8 byte. Luôn bằng sizeof phần tử p trỏ tới.

  4. 4

    Duyệt bằng con trỏ

    Viết vòng for duyệt int a[5] bằng CON TRỎ (không dùng a[i]) để in từng phần tử.

    Hoàn thành khi: for (int *p = a; p < a + 5; p++) printf("%d ", *p);

  5. 5

    sizeof mảng

    Cho int a[10]; (int = 4 byte). sizeof(a) bằng bao nhiêu? Còn với int *p = a; thì sizeof(p)?

    Hoàn thành khi: sizeof(a) = 40 (10×4). sizeof(p) = 8 (một con trỏ). Khác nhau!

  6. 6

    Vì sao cần n?

    Trong void in(int arr[], int n), vì sao bắt buộc có tham số n?

    Hoàn thành khi: arr đã suy biến thành con trỏ - hàm không biết mảng dài bao nhiêu. Phải truyền n để biết khi nào dừng.