Menu

Mảng trong C++: khai báo, chỉ số và những lỗi thường gặp

Giải thích mảng thô trong C++: cách khai báo và khởi tạo, truy cập bằng chỉ số an toàn, duyệt mảng theo kích thước, cái bẫy mảng suy biến thành con trỏ, và vì sao std::array và vector thường tốt hơn.

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ột dãy giá trị có kích thước cố định

Mảng (array) là một khối bộ nhớ liền nhau chứa một số lượng cố định các giá trị cùng kiểu. Một biến int lưu một con số, còn một mảng int lưu nhiều con số nằm sát nhau, mỗi số truy cập được bằng một chỉ số nguyên.

Bạn khai báo mảng bằng cách ghi kiểu phần tử, tên và kích thước trong cặp ngoặc vuông. Thêm một danh sách trong ngoặc nhọn để gán giá trị:

Chỉ số bắt đầu từ 0: phần tử đầu tiên là scores[0], và một mảng kích thước 4 có các chỉ số hợp lệ từ 0 đến 3. Kích thước phải là hằng số biết được lúc biên dịch: bạn không thể viết int n = readInput(); int a[n]; trong C++ chuẩn (đó là phần mở rộng không khả chuyển). Khi cần kích thước được quyết định lúc chạy, hãy dùng vector.

Khởi tạo mảng

Có vài cách để gán giá trị cho mảng, cùng một số cách viết tắt đáng biết:

Điều cần ghi nhớ: khi bạn cung cấp ít giá trị khởi tạo hơn kích thước, các phần tử còn lại được khởi tạo theo giá trị (bằng 0 với các kiểu số) chứ không chứa giá trị rác. Nhưng một mảng không có giá trị khởi tạo nào, như int e[4]; khai báo cục bộ, chứa các giá trị không xác định, và đọc chúng trước khi gán là undefined behavior.

Duyệt mảng

Vì các phần tử nằm liền nhau, bạn duyệt mảng bằng một vòng lặp chỉ số đơn giản. Để không vượt giới hạn, hãy điều khiển vòng lặp bằng độ dài thật của mảng thay vì một con số viết cứng:

sizeof(scores) là tổng số byte của cả mảng; chia cho sizeof(scores[0]) (kích thước một phần tử) sẽ ra số phần tử. Từ C++17 có cách viết gọn hơn, std::size(scores), dễ đọc hơn và sẽ không biên dịch nếu bạn lỡ truyền vào một con trỏ. Còn đơn giản hơn nữa khi bạn chỉ cần các giá trị: vòng for dạng range bỏ qua hoàn toàn phép tính chỉ số.

Cái bẫy truy cập ngoài phạm vi

C++ không kiểm tra giới hạn với arr[i]. Truy cập vượt quá phần tử cuối không ném ngoại lệ hay cảnh báo gì: nó đọc hoặc ghi vào vùng nhớ nào tình cờ nằm ở đó. Đây là bug mảng phổ biến nhất, và là một trường hợp undefined behavior kinh điển:

int a[3] = {1, 2, 3};
a[3] = 99;          // OOPS - valid indices are 0..2, not 3
cout << a[5];       // garbage, crash, or corruption - undefined behavior

Lỗi lệch một đơn vị (off-by-one) thường ẩn trong điều kiện vòng lặp. Viết i <= n thay vì i < n sẽ chạy thừa một bước và chạm vào arr[n], phần tử không tồn tại:

for (int i = 0; i <= n; i++)   // BUG: when i == n, arr[i] is out of bounds
    cout << arr[i];

Cách khắc phục là kỷ luật ở phần trước: lặp với i < size, không bao giờ dùng <=, và tính kích thước từ chính mảng thay vì gõ lại một con số cố định, thứ sẽ lệch đi khi bạn thêm phần tử.

Mảng suy biến: con trỏ ẩn

Hành vi khó lường nhất của mảng trong C++ là suy biến (decay): khi bạn truyền mảng vào một hàm, nó âm thầm chuyển thành con trỏ trỏ tới phần tử đầu tiên. Thông tin kích thước bị mất, nên sizeof bên trong hàm đo kích thước của con trỏ, không phải của mảng.

Lưu ý int arr[] và int* arr là như nhau khi làm tham số hàm: cặp ngoặc vuông chỉ để trang trí. Vì số phần tử đã mất, bạn phải tự truyền độ dài kèm theo mảng:

int sum(const int* arr, int n) {
    int total = 0;
    for (int i = 0; i < n; i++) total += arr[i];
    return total;
}

Khuôn mẫu "truyền một con trỏ và một độ dài rồi cầu mong chúng khớp nhau" này chính là sự bất tiện đẩy phần lớn code C++ sang dùng std::array và std::vector, những kiểu tự mang theo kích thước và không bao giờ suy biến.

Đôi nét về mảng nhiều chiều

Bạn có thể lồng các cặp ngoặc vuông để tạo một lưới. Mảng 2 chiều thực chất là mảng của các mảng, được sắp xếp theo từng hàng trong bộ nhớ:

Truy cập bằng grid[row][col]. Những lưu ý về giới hạn và suy biến vẫn áp dụng, thậm chí còn tệ hơn, vì truyền mảng 2 chiều vào hàm đòi hỏi ghi rõ mọi chiều trừ chiều đầu tiên (void f(int g[][3])). Với bất cứ thứ gì lớn hơn một lưới nhỏ cố định, một vector chứa các vector sẽ ít lỗi hơn nhiều.

Tiếp theo: vector

Mảng thô nhanh và dễ đoán, nhưng kích thước cố định, không an toàn về giới hạn và hành vi suy biến thành con trỏ khiến chúng bất tiện trong code hằng ngày. Tiếp theo, chúng ta sẽ làm quen với std::vector: một mảng có thể thay đổi kích thước, tự lớn lên khi cần, nhớ kích thước của chính nó và kết hợp trực tiếp với các thuật toán STL, cho bạn gần như mọi thứ mảng có mà ít cách tự bắn vào chân mình hơn nhiều.

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

Khai báo và khởi tạo mảng trong C++ như thế nào?

Viết kiểu phần tử, tên mảng và kích thước trong cặp ngoặc vuông, rồi tùy chọn thêm một danh sách trong ngoặc nhọn: int scores[4] = {90, 75, 100, 60};. Bạn có thể bỏ kích thước khi đã cung cấp giá trị khởi tạo: int scores[] = {90, 75, 100, 60}; để trình biên dịch tự đếm giúp bạn.

Làm sao lấy độ dài mảng trong C++?

Với một mảng thật sự còn trong phạm vi, dùng std::size(arr) (C++17) hoặc sizeof(arr) / sizeof(arr[0]). Cách này không còn đúng khi mảng đã suy biến thành con trỏ (chẳng hạn bên trong một hàm nhận int arr[]), lúc đó sizeof cho kích thước của con trỏ chứ không phải của mảng.

Chuyện gì xảy ra khi truy cập mảng ngoài phạm vi trong C++?

Đó là undefined behavior. C++ không kiểm tra giới hạn với arr[i], nên đọc hoặc ghi vượt quá cuối mảng có thể làm chương trình crash, trả về giá trị rác hoặc âm thầm làm hỏng vùng nhớ lân cận. Luôn giữ chỉ số trong khoảng từ 0 đến size - 1.

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