Menu

Mảng trong C: khai báo, khởi tạo và duyệt mảng

Cách khai báo và khởi tạo mảng trong C, truy cập phần tử bằng chỉ số bắt đầu từ 0, tính độ dài mảng bằng sizeof, duyệt các phần tử bằng vòng lặp, và vì sao đọc vượt quá cuối mảng là undefined behavior chứ không phải một thông báo lỗi.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

Mảng (array) là một khối có kích thước cố định gồm các phần tử cùng kiểu, nằm nối tiếp nhau trong bộ nhớ. Cách bố trí liền kề đó giải thích mọi thứ: nhờ nó mà truy cập theo chỉ số diễn ra tức thì, mảng và con trỏ gắn bó mật thiết với nhau, và C có thể đưa bạn địa chỉ của phần tử thứ 0 rồi để bạn tìm các phần tử còn lại bằng phép tính số học.

Khai báo mảng

Kiểu phần tử, tên, và kích thước trong cặp ngoặc vuông:

int    scores[5];       // 5 ints
double prices[100];     // 100 doubles
char   initials[3];     // 3 chars

Kích thước là số phần tử, và phải biết trước ngay tại chỗ khai báo, thường là một số cụ thể hoặc một hằng số tạo bằng #define. Bộ nhớ được dành sẵn ngay lập tức; không có gì được cấp phát thêm về sau.

Một mảng cục bộ vừa khai báo chứa giá trị rác, không phải các số 0:

Dòng thứ hai có thể in ra các số 0, có thể in ra những số rất lớn, và có thể khác nhau giữa các lần chạy hoặc giữa bản build debug và bản build release. Đọc những giá trị này là undefined behavior; hãy khởi tạo trước khi đọc.

Khởi tạo mảng

Có vài cách viết, cách nào cũng hữu ích:

Cách viết {0} là cách bạn nên nhớ: mọi phần tử bạn không liệt kê đều được khởi tạo bằng 0, nên chỉ một số 0 là đủ để đưa cả mảng về 0. Designated initializer ([4] = 99) cho phép bạn đặt giá trị cho từng vị trí cụ thể và để phần còn lại bằng 0, rất tiện cho các bảng tra cứu phần lớn là trống.

Có một việc bạn không thể làm: gán lại cả mảng sau khi đã khai báo:

int a[3] = {1, 2, 3};
int b[3];
b = a;                    // error: an array is not assignable
memcpy(b, a, sizeof a);   // this is how you copy one

Chỉ số bắt đầu từ 0

Phần tử đầu tiên là arr[0] và phần tử cuối là arr[n-1]. Không có arr[n].

Chạy vòng lặp cuối cùng và nhìn các địa chỉ: mỗi địa chỉ cách địa chỉ trước đúng sizeof(int) byte. Các phần tử thật sự nằm nối đuôi nhau:

 index      0         1         2         3         4
         +--------+--------+--------+--------+--------+
 value   |   88   |   95   |   75   |   60   |  100   |
         +--------+--------+--------+--------+--------+
 offset   +0       +4       +8       +12      +16      bytes
          ^
          &scores[0], which is also what `scores` decays to

Đánh chỉ số từ 0 không phải lựa chọn tùy tiện: nó biến chỉ số thành độ lệch tính từ đầu mảng, đúng thứ mà phép tính địa chỉ cần. scores[i] được định nghĩa là *(scores + i); xem thêm phần con trỏ và mảng.

Tính độ dài mảng

Lúc chạy, C không lưu độ dài mảng ở đâu cả, nhưng trình biên dịch biết độ dài đó, và sizeof cho bạn lấy nó ra:

sizeof(arr) / sizeof(arr[0]) là cách viết quen thuộc. Chia cho arr[0] thay vì cho sizeof(double) viết cứng giúp dòng code vẫn đúng khi bạn đổi kiểu phần tử.

Cái bẫy: cách này chỉ đúng tại nơi mảng được khai báo. Truyền mảng vào một hàm thì tham số là con trỏ, nên sizeof lại đo kích thước con trỏ, thường là 8 byte, cho ra "độ dài" bằng 1 hoặc 2. Đó là lý do mọi hàm nhận mảng trong C đều nhận thêm số phần tử:

Duyệt mảng bằng vòng lặp

Dạng chuẩn là một vòng lặp for chạy từ 0 khi i < n:

Chú ý i < n, không phải i <= n. Với n phần tử, các chỉ số hợp lệ đi từ 0 đến n - 1, nên <= chạy thừa một lượt và đọc vượt quá cuối mảng. Lấy giá trị khởi đầu của hottest và coldest từ temps[0] thay vì từ 0 cũng là một thói quen đáng học theo: khởi tạo giá trị lớn nhất bằng 0 sẽ âm thầm sai khi dữ liệu toàn số âm.

Truy cập ngoài phạm vi là undefined behavior

Đây là phần của C khiến người đến từ các ngôn ngữ khác bất ngờ. Không có kiểm tra giới hạn mảng. Không lúc biên dịch, không lúc chạy. arr[10] trên một mảng năm phần tử không phải lỗi, mà là một phép tính địa chỉ mà trình biên dịch thực hiện không một lời cảnh báo.

int arr[5] = {1, 2, 3, 4, 5};
arr[7] = 99;     // writes 8 bytes past the end of the array
int x = arr[-1]; // reads before the start

Chuyện xảy ra tiếp theo là undefined behavior, và triệu chứng của nó đa dạng một cách khó chịu:

  • chương trình trông như vẫn chạy đúng, vì vùng nhớ bị chạm vào đang không dùng;
  • một biến khác thay đổi mà không rõ lý do;
  • chương trình crash với lỗi segmentation fault, có khi rất lâu sau đó;
  • hành vi khác nhau giữa -O0 và -O2.

"Trông như vẫn chạy đúng" là trường hợp nguy hiểm nhất, vì nó có nghĩa là bug được phát hành cùng sản phẩm. Ba cách phòng tránh:

  1. Lặp với i < n. Phần lớn lỗi tràn mảng là lỗi lệch một đơn vị (off-by-one) trong điều kiện vòng lặp.
  2. Kiểm tra các chỉ số đến từ đầu vào.
  1. Build kèm sanitizer khi phát triển. gcc -fsanitize=address -g prog.c biến hầu hết lỗi tràn mảng thành một báo cáo ngay lập tức, nêu rõ tệp, dòng và mảng liên quan.

Mảng chứa các kiểu khác

Cùng một cú pháp dùng được cho mọi kiểu phần tử, kể cả struct:

Dòng char word[6] = "hello"; đáng để dừng lại một chút: một chuỗi trong C chỉ là một mảng char có byte có nghĩa cuối cùng là '\0'. Ký tự kết thúc đó là lý do mảng cần sáu ô cho năm chữ cái.

Sơ lược về mảng có độ dài thay đổi

C99 cho phép tạo mảng có kích thước là một giá trị lúc chạy:

int n = get_count();
int buffer[n];        // a variable-length array

Có hai điều cần lưu ý. Kích thước được cố định ngay khi mảng được tạo, nên VLA không phải một danh sách có thể mở rộng. Và bộ nhớ được lấy từ stack, nên một giá trị n lớn hoặc do kẻ tấn công kiểm soát có thể làm tràn stack và khiến chương trình crash. Đó là lý do nhiều dự án, kể cả nhân Linux, cấm hẳn VLA. Ngoài ra, từ C11 việc hỗ trợ VLA là tùy chọn với các trình biên dịch.

Khi kích thước thật sự chỉ biết được lúc chạy, hãy cấp phát động:

Chú ý rằng arr[i] viết giống hệt nhau dù arr là mảng hay con trỏ tới vùng nhớ được cấp phát: cú pháp chỉ số không phân biệt. Chi tiết đầy đủ có trong bài bộ nhớ động.

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

Khai báo mảng trong C như thế nào?

Ghi kiểu phần tử, tên mảng và kích thước trong cặp ngoặc vuông: int scores[5]; dành chỗ cho năm số int. Kích thước phải là hằng số đã biết ngay tại chỗ khai báo (trừ mảng có độ dài thay đổi, VLA), và các phần tử chưa được khởi tạo cho đến khi bạn gán giá trị.

Làm sao tính độ dài mảng trong C?

sizeof(arr) / sizeof(arr[0]), tức tổng số byte của mảng chia cho kích thước một phần tử. Cách này chỉ đúng trong phạm vi nơi mảng được khai báo: khi truyền mảng vào hàm, mảng đã suy biến thành con trỏ và sizeof lại đo kích thước con trỏ.

Chuyện gì xảy ra khi truy cập vượt quá cuối mảng trong C?

Không gì ngăn bạn lại cả. C không kiểm tra giới hạn mảng, nên arr[10] trên một mảng 5 phần tử sẽ đọc hoặc ghi vào vùng nhớ nào đang nằm ở đó. Đó là undefined behavior: chương trình có thể in ra giá trị rác, làm hỏng một biến khác hoặc bị crash, và cũng có thể trông như vẫn chạy đúng cho đến một ngày nào đó.

Làm sao khởi tạo toàn bộ phần tử của mảng bằng 0 trong C?

int arr[100] = {0};: mọi phần tử bạn không liệt kê đều được khởi tạo bằng 0, nên chỉ một số 0 là đủ để cả mảng bằng 0. Từ C23, int arr[100] = {}; cũng dùng được. Nếu không khởi tạo gì, mảng cục bộ chứa giá trị rác không xác định.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu