Bỏ qua điều hướng, tới nội dung chính
Học C
Bài 9.416 phút đọc

Duyệt mảng

Sau bài này bạn sẽ làm được

  • Duyệt mảng bằng chỉ số và bằng con trỏ
  • Biết vì sao macro đếm phần tử vô dụng bên trong hàm
  • Viết hàm in mảng có định dạng dễ đọc
  • Chọn kiểu chỉ số phù hợp cho vòng lặp

Gần như mọi thao tác trên mảng đều bắt đầu bằng một vòng lặp duyệt qua các phần tử. Bài này gom lại những cách viết chuẩn, cùng một cái bẫy về macro đếm phần tử mà rất nhiều người mắc.

#Ba cách duyệt

ba-cach.c
#define N 5

int a[N] = { 1, 2, 3, 4, 5 };

/* Cách 1: theo chỉ số. Dễ đọc nhất, dùng được ở mọi nơi. */
for (size_t i = 0; i < N; ++i)
    printf("%d ", a[i]);

/* Cách 2: theo con trỏ. Ngắn hơn, hợp khi không cần biết vị trí. */
for (int *p = a; p < a + N; ++p)
    printf("%d ", *p);

/* Cách 3: hai con trỏ đầu và cuối. Kiểu của thư viện chuẩn. */
int *dau = a;
int *cuoi = a + N;

while (dau < cuoi)
    printf("%d ", *dau++);
CáchƯu điểmKhi nào chọn
Theo chỉ sốBiết được vị trí, dễ truy cập nhiều mảng cùng lúcMặc định. Khi cần in vị trí hoặc so hai mảng song song
Theo con trỏKhông cần phép nhân để tính địa chỉ mỗi lượtKhi chỉ quan tâm giá trị, và khi làm việc với chuỗi
Hai con trỏDiễn đạt được khoảng, gọn khi thu hẹp từ hai đầuĐảo mảng, phân hoạch, tìm cặp

#Macro đếm phần tử

#define SO_PHAN_TU(x) (sizeof (x) / sizeof (x)[0])

int a[10];

printf("%zu\n", SO_PHAN_TU(a));    /* 10, đúng */
Hàm tự đoán độ dài
void xu_ly(int a[])
{
    for (size_t i = 0; i < SO_PHAN_TU(a); ++i)
        printf("%d ", a[i]);
}

xu_ly(a);
Tính ở nơi mảng còn là mảng
void xu_ly(const int *a, size_t n)
{
    for (size_t i = 0; i < n; ++i)
        printf("%d ", a[i]);
}

xu_ly(a, SO_PHAN_TU(a));   /* macro dùng ở main, nơi a vẫn là mảng */

#Hàm in mảng

Bạn sẽ cần hàm này ở mọi bài còn lại của Chương 9. Viết cho tử tế một lần rồi chép lại dùng.

in-mang.c
void in_mang(const char *nhan, const int *a, size_t n)
{
    printf("%s[", nhan);

    for (size_t i = 0; i < n; ++i)
        printf("%d%s", a[i], i + 1 < n ? ", " : "");

    printf("]\n");
}
terminal
./in-mang
truoc: [5, 1, 4, 2, 8]
sau:   [1, 2, 4, 5, 8]
rong:  []

Ba chi tiết đáng chú ý. Thứ nhất, biểu thức i + 1 < n ? ", " : "" đặt dấu phẩy vào giữa các phần tử chứ không đặt sau phần tử cuối. Thứ hai, hàm nhận const int * nên trình biên dịch bảo đảm nó không sửa dữ liệu của bạn. Thứ ba, với n bằng 0 vòng lặp không chạy lần nào và bạn nhận được cặp ngoặc rỗng, đúng như mong đợi.

Bản in theo bảng cho mảng dài

void in_bang(const int *a, size_t n, size_t cot)
{
    for (size_t i = 0; i < n; ++i) {
        printf("%6d", a[i]);

        if ((i + 1) % cot == 0 || i + 1 == n)
            putchar('\n');
    }
}
terminal
./in-bang
     1     2     3     4     5     6     7     8
     9    10    11    12    13    14    15    16
    17    18    19    20

Định dạng %6d căn phải trong sáu ký tự nên các cột thẳng hàng. Bài 3.2 đã nói về độ rộng trường trong printf.

#Chọn kiểu cho biến chỉ số

KiểuƯu điểmNhược điểm
size_tĐúng kiểu mà sizeof trả về, không bao giờ tràn với mảng thậtKhông dấu nên dễ mắc bẫy trừ ra số khổng lồ
intCó dấu nên duyệt ngược tự nhiên, quen thuộcCảnh báo so sánh khác dấu, tràn khi mảng lớn hơn hai tỷ phần tử
ptrdiff_tCó dấu, đủ rộng, đúng kiểu của hiệu hai con trỏÍt người dùng nên trông lạ
/* Chỉ số dùng size_t, nhưng kết quả tìm kiếm dùng kiểu có dấu */
long tim(const int *a, size_t n, int x)
{
    for (size_t i = 0; i < n; ++i)
        if (a[i] == x)
            return (long)i;

    return -1;      /* âm một là quy ước không tìm thấy */
}

#Bốn mẫu duyệt thường gặp

Mẫu 1: tích lũy

long long tong = 0;

for (size_t i = 0; i < n; ++i)
    tong += a[i];

Mẫu 2: đếm theo điều kiện

size_t so_chan = 0;

for (size_t i = 0; i < n; ++i)
    if (a[i] % 2 == 0)
        so_chan++;

Mẫu 3: lọc và dồn tại chỗ

loc.c
/* Xóa mọi phần tử bằng x, trả về số phần tử còn lại.
   Hai chỉ số: doc chạy hết mảng, ghi chỉ tiến khi giữ lại phần tử. */
size_t xoa_gia_tri(int *a, size_t n, int x)
{
    size_t ghi = 0;

    for (size_t doc = 0; doc < n; ++doc)
        if (a[doc] != x)
            a[ghi++] = a[doc];

    return ghi;
}

Mẫu hai chỉ số này rất đáng nhớ. Nó giải quyết được xóa phần tử, loại bỏ trùng lặp, dồn số 0 về cuối, và nhiều bài khác, luôn trong một lượt duyệt duy nhất mà không cần mảng phụ.

Mẫu 4: dừng sớm

int co_so_am = 0;

for (size_t i = 0; i < n; ++i)
    if (a[i] < 0) {
        co_so_am = 1;
        break;              /* không cần duyệt tiếp */
    }

Tự làm thử

  1. Cài in_mang như trong bài, thử với mảng rỗng, một phần tử và mười phần tử.
  2. Viết cùng một vòng lặp tính tổng bằng cả ba cách duyệt, xác nhận kết quả giống nhau.
  3. Gọi macro đếm phần tử bên trong một hàm và ở main, in cả hai kết quả rồi giải thích chênh lệch.
  4. Cài xoa_gia_tri theo mẫu hai chỉ số, dùng nó để loại bỏ mọi số 0 khỏi mảng.
  5. Viết hàm loại bỏ phần tử trùng lặp liên tiếp bằng đúng mẫu hai chỉ số, trong một lượt duyệt.
  6. Viết hàm in_bang nhận số cột từ tham số, in mảng một trăm phần tử theo tám cột và theo mười cộ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

  • Ba cách duyệt là theo chỉ số, theo con trỏ và bằng hai con trỏ khoảng. Chọn theo độ dễ đọc, không theo tốc độ.
  • Macro sizeof(x) / sizeof(x)[0] chỉ đúng khi x còn là mảng thật, tức là ở nơi khai báo, không phải bên trong hàm.
  • Hàm nhận mảng luôn cần thêm tham số độ dài, và nên có const nếu không sửa.
  • Dùng size_t cho chỉ số vòng lặp tiến, dùng kiểu có dấu cho giá trị trả về có thể là âm một.
  • Mẫu hai chỉ số đọc và ghi giải quyết được xóa, lọc và dồn phần tử trong một lượt duyệt.