Mảng một chiều trong C++


Trong C++, mảng là cấu trúc dữ liệu cơ bản dùng để lưu trữ nhiều giá trị có cùng kiểu trong một khối bộ nhớ liên tiếp. Mỗi phần tử trong mảng được đánh số thứ tự, bắt đầu từ chỉ số 0. Việc sử dụng mảng cho phép lập trình viên xử lý dữ liệu hàng loạt như danh sách điểm, mảng ký tự, bảng số liệu… một cách hiệu quả về hiệu năng và tổ chức mã nguồn.

Mảng trong C++ có thể được khai báo tĩnh hoặc động. Khi khai báo tĩnh, số lượng phần tử phải được xác định tại thời điểm biên dịch:

int a[5];

Câu lệnh trên tạo một mảng gồm 5 số nguyên, với các chỉ số từ a[0] đến a[4]. Giá trị của các phần tử chưa được khởi tạo nên không được sử dụng trước khi gán giá trị. C++ không tự động khởi tạo mảng thành 0 như nhiều ngôn ngữ bậc cao khác.

Có thể khởi tạo mảng trực tiếp:

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

Hoặc khởi tạo một phần, phần còn lại sẽ được mặc định là 0:

int c[5] = {1, 2}; // c[2] → c[4] bằng 0

Khi khai báo mảng với dấu [] rỗng và cung cấp danh sách khởi tạo, trình biên dịch sẽ tự suy ra kích thước:

int d[] = {10, 20, 30}; // mảng có 3 phần tử

Truy cập phần tử mảng dùng chỉ số:

cout << b[0]; // in 1
b[2] = 100;   // gán lại giá trị tại chỉ số 2

Tuy nhiên, nếu truy cập ra ngoài phạm vi khai báo, ví dụ b[10], hành vi của chương trình là không xác định(undefined behavior). Không có lỗi biên dịch, nhưng kết quả có thể sai hoặc dẫn đến crash, vì trình biên dịch không kiểm tra chỉ số.

Mảng trong C++ thực chất là con trỏ đến vùng nhớ liên tiếp, nên khi ta viết:

int x[3] = {7, 8, 9};

Thì x chính là địa chỉ của phần tử đầu tiên &x[0]. Cú pháp x[i] tương đương với *(x + i) – toán tử [] thực chất là cú pháp ngắn cho phép truy cập theo địa chỉ bù.

Điều này giải thích vì sao có thể duyệt mảng như sau:

for (int i = 0; i < 3; i++) {
    cout << *(x + i) << " ";
}

Mặc dù mảng trong C++ bản chất là con trỏ, nhưng có những khác biệt quan trọng so với con trỏ thuần:

  • Kích thước của mảng được cố định và lưu tại thời điểm biên dịch.
  • Không thể gán mảng này cho mảng khác trực tiếp bằng toán tử =.
  • Khi truyền mảng vào hàm, mảng sẽ mất kích thước gốc, chỉ còn là con trỏ.

Vì vậy, khi truyền mảng vào hàm, ta thường phải truyền kèm độ dài:

void inMang(int a[], int n) {
    for (int i = 0; i < n; i++) {
        cout << a[i] << " ";
    }
}

Hoặc dùng cú pháp con trỏ tương đương:

void inMang(int* a, int n);

Trong mọi trường hợp, hai dạng int a[] và int* a trong định nghĩa hàm là tương đương nhau. Điều này cũng cho thấy mảng không mang theo thông tin kích thước khi được truyền đi, khác với std::vector.

Nếu muốn dùng mảng động – kích thước xác định tại thời gian chạy – ta dùng toán tử new:

int* p = new int[n]; // cấp phát mảng động

Sau khi dùng xong, phải giải phóng bộ nhớ để tránh rò rỉ:

delete[] p;

Cần phân biệt rõ giữa mảng tĩnh (cấp phát trên stack) và mảng động (cấp phát trên heap). Mảng tĩnh bị giới hạn kích thước bởi stack frame, còn mảng động thì linh hoạt nhưng đòi hỏi người lập trình quản lý bộ nhớ thủ công.

Một điểm cần chú ý là trong C++, không có cách nội tại để xác định số phần tử của mảng nếu chỉ có con trỏ. Tuy nhiên, với mảng tĩnh trong cùng scope, có thể dùng:

int a[10];
int n = sizeof(a) / sizeof(a[0]); // = 10

Lưu ý rằng sizeof(a) chỉ trả về đúng kích thước toàn mảng khi a là mảng khai báo trong hàm, không phải tham số hàm hay con trỏ.

Việc thao tác trên mảng thường gắn liền với các thuật toán xử lý dữ liệu cơ bản: duyệt, tính tổng, tìm max/min, đếm, lọc, v.v. Ví dụ:

int a[] = {5, 2, 9, 1, 3};
int n = sizeof(a)/sizeof(a[0]);
int tong = 0;

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

Trong môi trường C++ hiện đại, người ta thường khuyên sử dụng std::vector thay vì mảng thuần, vì vector an toàn hơn, tự quản lý bộ nhớ, và hỗ trợ nhiều thao tác linh hoạt. Tuy nhiên, hiểu sâu về mảng là điều bắt buộc khi học lập trình hệ thống, xử lý dữ liệu ở cấp độ thấp, hoặc khi hiệu suất và kiểm soát bộ nhớ là ưu tiên hàng đầu.