Bài 11.1: Khái niệm và sử dụng mảng 1 chiều trong C++

Chào mừng trở lại với series blog C++ của FullhouseDev! 👋

Trong cuộc sống hay trong lập trình, chúng ta thường xuyên phải làm việc với những tập hợp các dữ liệu cùng loại. Ví dụ: một danh sách điểm thi của học sinh, nhiệt độ trung bình các ngày trong tuần, hoặc đơn giản là một chuỗi các số nguyên. Việc lưu trữ và quản lý từng giá trị một bằng các biến riêng lẻ sẽ rất cồng kềnh và phi thực tế khi số lượng dữ liệu lớn.

Đây chính là lúc mảng (array) tỏa sáng! Mảng cho phép chúng ta gom các biến cùng kiểu lại với nhau dưới một cái tên duy nhất. Trong bài viết này, chúng ta sẽ tập trung vào loại mảng cơ bản và phổ biến nhất: mảng 1 chiều.

Hãy cùng "mở khóa" sức mạnh của mảng 1 chiều trong C++ nhé!

Mảng 1 Chiều là gì?

Tưởng tượng bạn có một dãy các "ngăn kéo" được đánh số liên tiếp, mỗi ngăn chỉ dùng để chứa một loại đồ vật duy nhất (ví dụ: chỉ quần áo, hoặc chỉ sách). Mảng 1 chiều trong C++ cũng tương tự như vậy!

Một mảng 1 chiều là một tập hợp các phần tử có cùng kiểu dữ liệu, được lưu trữ liên tiếp trong bộ nhớ máy tính và được truy cập thông qua một chỉ số (index).

  • Cùng kiểu dữ liệu: Tất cả các phần tử trong mảng phải có kiểu dữ liệu giống nhau (int, float, double, char, string...).
  • Lưu trữ liên tiếp: Các phần tử được xếp "sát nhau" trong bộ nhớ, giúp việc truy cập nhanh chóng và hiệu quả.
  • Chỉ số (Index): Mỗi phần tử trong mảng có một vị trí duy nhất, được xác định bằng chỉ số của nó. Đặc biệt quan trọng: Trong C++, chỉ số của mảng luôn bắt đầu từ 0.

Ví dụ: Một mảng gồm 5 số nguyên sẽ có các phần tử tại các chỉ số 0, 1, 2, 3, 4.

Khai báo Mảng 1 Chiều

Để sử dụng mảng, trước hết bạn cần "khai báo" nó. Khi khai báo, bạn cần cho C++ biết:

  1. Kiểu dữ liệu của các phần tử trong mảng.
  2. Tên mà bạn muốn đặt cho mảng.
  3. Kích thước (số lượng phần tử tối đa) của mảng.

Cú pháp khai báo mảng 1 chiều cơ bản là:

kieu_du_lieu ten_mang[kich_thuoc];

Trong đó:

  • kieu_du_lieu: Kiểu dữ liệu của các phần tử (ví dụ: int, float, char, double).
  • ten_mang: Tên định danh bạn đặt cho mảng (tuân theo quy tắc đặt tên biến).
  • kich_thuoc: Số lượng phần tử mà mảng có thể chứa. Lưu ý quan trọng: Với mảng C-style cơ bản này, kich_thuoc thường phải là một hằng số nguyên dương hoặc một biểu thức có thể tính toán ra một hằng số nguyên dương tại thời điểm biên dịch (compile-time).

Ví dụ minh họa:

#include <iostream>

int main() {
    int diem[5];
    double nhiet[10];
    char ten_ky_tu[20];

    cout << "Da khai bao xong cac mang." << endl;

    return 0;
}

Output:

Da khai bao xong cac mang.

Giải thích: Đoạn code trên chỉ đơn thuần thông báo cho trình biên dịch về sự tồn tại của các mảng diemSo, nhietDo, ten với kích thước và kiểu dữ liệu xác định. Lúc này, các phần tử bên trong mảng có thể chứa những giá trị "rác" (garbage values) từ bộ nhớ.

Khởi tạo Mảng 1 Chiều

Sau khi khai báo, bạn thường muốn "đổ" các giá trị ban đầu vào mảng. Việc này gọi là khởi tạo mảng. Có nhiều cách để khởi tạo:

1. Khởi tạo ngay lúc khai báo (sử dụng danh sách khởi tạo {})

Đây là cách phổ biến và tiện lợi nhất để gán giá trị ban đầu cho mảng. Bạn liệt kê các giá trị trong cặp ngoặc nhọn {} và phân cách bằng dấu phẩy.

  • Khởi tạo đầy đủ: Liệt kê đủ số phần tử bằng kích thước đã khai báo.

    #include <iostream>
    
    int main() {
        int diem[] = {85, 90, 78, 92, 88};
        double gia[3] = {15.5, 20.0, 12.75};
    
        cout << "Mang diem co kich thuoc tu dong suy dien tu 5 phan tu." << endl;
        cout << "Mang gia duoc khai bao ro kich thuoc 3 va khoi tao." << endl;
    
        return 0;
    }
    

    Output:

    Mang diem co kich thuoc tu dong suy dien tu 5 phan tu.
    Mang gia duoc khai bao ro kich thuoc 3 va khoi tao.

    Giải thích:

    • Đối với diemThi, chúng ta không cần chỉ định kích thước trong []. Trình biên dịch C++ sẽ tự động đếm số phần tử trong {} (ở đây là 5) và gán kích thước đó cho mảng.
    • Đối với giaCa, chúng ta khai báo rõ kích thước là 3 và cung cấp đúng 3 giá trị.
  • Khởi tạo một phần: Liệt kê ít hơn số phần tử so với kích thước đã khai báo. Các phần tử còn lại sẽ được tự động khởi tạo về giá trị mặc định của kiểu dữ liệu (0 cho số nguyên/thực, false cho bool, '\0' cho char, nullptr cho con trỏ...).

    #include <iostream>
    
    int main() {
        int a[5] = {1, 2};
        float b[4] = {3.14f};
        char c[6] = {'a', 'b', 'c'};
    
        cout << "Cac mang da duoc khoi tao mot phan." << endl;
    
        return 0;
    }
    

    Output:

    Cac mang da duoc khoi tao mot phan.

    Giải thích: Các phần tử không được liệt kê trong danh sách khởi tạo sẽ nhận giá trị 0 (hoặc tương đương với 0 đối với kiểu dữ liệu đó).

  • Khởi tạo tất cả về 0 (hoặc giá trị mặc định): Nếu bạn chỉ muốn tất cả các phần tử có giá trị mặc định ban đầu, bạn có thể dùng {} trống hoặc {0}.

    #include <iostream>
    
    int main() {
        int a[10] = {};
        double b[5] = {0.0};
    
        cout << "Cac mang da duoc khoi tao ve gia tri mac dinh." << endl;
    
        return 0;
    }
    

    Output:

    Cac mang da duoc khoi tao ve gia tri mac dinh.

    Giải thích: Dùng {} hoặc {0} là cách gọn gàng để đảm bảo tất cả các phần tử ban đầu có giá trị 0 (hoặc giá trị mặc định tương đương).

2. Khởi tạo (gán giá trị) sau khi khai báo

Bạn có thể gán giá trị cho từng phần tử riêng lẻ sau khi mảng đã được khai báo. Để làm điều này, bạn cần sử dụng chỉ số của phần tử đó.

Cú pháp: ten_mang[chi_so] = gia_tri;

Ví dụ minh họa:

#include <iostream>

int main() {
    int diem[5];

    diem[0] = 95;
    diem[1] = 87;
    diem[2] = 91;
    diem[3] = 76;
    diem[4] = 93;

    cout << "Da gan gia tri cho tung phan tu cua mang diem." << endl;

    return 0;
}

Output:

Da gan gia tri cho tung phan tu cua mang diem.

Giải thích: Chúng ta truy cập vào từng "ngăn kéo" của mảng thông qua chỉ số của nó (bắt đầu từ 0) và gán một giá trị mới vào đó.

Truy cập Phần tử trong Mảng

Điểm mạnh của mảng là khả năng truy cập trực tiếp vào bất kỳ phần tử nào thông qua chỉ số của nó. Bạn có thể dùng chỉ số để:

  • Đọc giá trị của phần tử đó.
  • Thay đổi giá trị của phần tử đó.

Cú pháp: ten_mang[chi_so]

Ví dụ minh họa:

#include <iostream>

int main() {
    int diem[] = {85, 90, 78, 92, 88};

    cout << "Diem cua hoc sinh thu nhat (index 0): " << diem[0] << endl;
    cout << "Diem cua hoc sinh thu ba (index 2): " << diem[2] << endl;
    cout << "Diem cua hoc sinh cuoi cung (index 4): " << diem[4] << endl;

    diem[1] = 95;

    cout << "Diem cua hoc sinh thu hai sau khi cap nhat: " << diem[1] << endl;

    return 0;
}

Output:

Diem cua hoc sinh thu nhat (index 0): 85
Diem cua hoc sinh thu ba (index 2): 78
Diem cua hoc sinh cuoi cung (index 4): 88
Diem cua hoc sinh thu hai sau khi cap nhat: 95

Giải thích: Chúng ta sử dụng diemThi[chỉ_số] để tham chiếu đến phần tử mong muốn. diemThi[0] là phần tử đầu tiên, diemThi[2] là phần tử thứ ba, v.v. Sau đó, chúng ta có thể sử dụng biểu thức này giống như một biến thông thường để đọc giá trị (như khi in ra màn hình) hoặc gán giá trị mới.

Lưu ý cực kỳ quan trọng: Hãy luôn đảm bảo chỉ số bạn sử dụng nằm trong phạm vi hợp lệ của mảng, tức là từ 0 đến kich_thuoc - 1. Truy cập vào chỉ số ngoài phạm vi này (gọi là array out of bounds) là một lỗi nghiêm trọng, có thể dẫn đến chương trình bị crash hoặc cho kết quả không mong muốn (hành vi không xác định - undefined behavior).

Duyệt (Iterate) qua Mảng

Để xử lý tất cả hoặc một phần các phần tử trong mảng, bạn cần "duyệt" qua nó. Vòng lặp là công cụ lý tưởng cho việc này. Hai cách phổ biến nhất để duyệt mảng 1 chiều là sử dụng vòng lặp for truyền thống với chỉ số hoặc vòng lặp range-based for (từ C++11).

1. Duyệt bằng vòng lặp for với chỉ số

Cách này cho phép bạn truy cập cả chỉ số lẫn giá trị của từng phần tử.

#include <iostream>

int main() {
    int a[] = {2, 4, 6, 8, 10};
    int n = 5;

    cout << "Cac phan tu cua mang a (su dung for index):" << endl;

    for (int i = 0; i < n; ++i) {
        cout << "Phan tu tai index " << i << ": " << a[i] << endl;
    }

    return 0;
}

Output:

Cac phan tu cua mang a (su dung for index):
Phan tu tai index 0: 2
Phan tu tai index 1: 4
Phan tu tai index 2: 6
Phan tu tai index 3: 8
Phan tu tai index 4: 10

Giải thích:

  • Chúng ta dùng một biến đếm i bắt đầu từ 0.
  • Vòng lặp tiếp tục chạy miễn là i nhỏ hơn kichThuoc. Điều này đảm bảo i sẽ chạy từ 0 đến kichThuoc - 1, bao phủ tất cả các chỉ số hợp lệ của mảng.
  • Trong mỗi lần lặp, chúng ta sử dụng soChan[i] để truy cập phần tử tại chỉ số hiện tại.
2. Duyệt bằng vòng lặp range-based for (từ C++11)

Cách này đơn giản hơn khi bạn chỉ cần truy cập giá trị của từng phần tử và không cần quan tâm đến chỉ số của nó.

#include <iostream>
#include <string>

int main() {
    string ds_ten[] = {"Alice", "Bob", "Charlie"};

    cout << "Cac ten trong danh sach (su dung range-based for):" << endl;

    for (string t : ds_ten) {
        cout << "Ten: " << t << endl;
    }

    int diem_so[] = {85, 90, 78, 92, 88};
    cout << "\nCac diem so (su dung range-based for voi auto):" << endl;
    for (auto d : diem_so) {
        cout << "Diem: " << d << endl;
    }

    return 0;
}

Output:

Cac ten trong danh sach (su dung range-based for):
Ten: Alice
Ten: Bob
Ten: Charlie

Cac diem so (su dung range-based for voi auto):
Diem: 85
Diem: 90
Diem: 78
Diem: 92
Diem: 88

Giải thích:

  • for (string ten : danhSachTen) đọc là: "Với mỗi string ten trong mảng danhSachTen..."
  • Trong mỗi lần lặp, biến ten sẽ lần lượt nhận giá trị của từng phần tử trong mảng.
  • Cú pháp này gọn gàng và ít bị lỗi "duyệt sai cận" (off-by-one error) so với vòng lặp for truyền thống khi chỉ cần đọc giá trị.
  • Sử dụng auto giúp C++ tự động suy luận kiểu dữ liệu của phần tử.

Lấy Kích thước của Mảng C-style

Một điều quan trọng cần biết là mảng C-style không tự động lưu trữ kích thước của nó. Điều này có nghĩa là nếu bạn truyền mảng vào một hàm, hàm đó sẽ không biết kích thước ban đầu của mảng trừ khi bạn truyền kích thước đó như một tham số riêng.

Tuy nhiên, khi mảng được khai báo trong cùng một phạm vi (scope) mà bạn đang làm việc, bạn có thể tính toán kích thước của nó bằng cách sử dụng toán tử sizeof.

Công thức phổ biến để tính số phần tử:

so_phan_tu = sizeof(ten_mang) / sizeof(ten_mang[0])

  • sizeof(ten_mang): Trả về tổng số byte mà toàn bộ mảng chiếm trong bộ nhớ.
  • sizeof(ten_mang[0]): Trả về số byte mà một phần tử đơn lẻ của mảng chiếm trong bộ nhớ.

Chia tổng số byte cho số byte của một phần tử sẽ ra tổng số phần tử.

Ví dụ minh họa:

#include <iostream>
#include <cstddef>

int main() {
    int a[] = {10, 20, 30, 40, 50, 60};

    size_t n = sizeof(a) / sizeof(a[0]);

    cout << "Tong so byte cua mang a: " << sizeof(a) << endl;
    cout << "So byte cua mot phan tu (int): " << sizeof(a[0]) << endl;
    cout << "Kich thuoc cua mang (so phan tu): " << n << endl;

    cout << "\nCac phan tu trong mang:" << endl;
    for (size_t i = 0; i < n; ++i) {
        cout << a[i] << " ";
    }
    cout << endl;

    double b[] = {22.5, 23.0, 21.8};
    size_t m = sizeof(b) / sizeof(b[0]);

    cout << "\nKich thuoc cua mang b (so phan tu): " << m << endl;

    return 0;
}

Output:

Tong so byte cua mang a: 24
So byte cua mot phan tu (int): 4
Kich thuoc cua mang (so phan tu): 6

Cac phan tu trong mang:
10 20 30 40 50 60 

Kich thuoc cua mang b (so phan tu): 3

Giải thích: Chúng ta sử dụng sizeof để tính toán kích thước động (tại thời điểm biên dịch) dựa trên số phần tử thực tế đã được khởi tạo. Kiểu dữ liệu size_t là kiểu không âm được sử dụng phổ biến cho kích thước và chỉ số trong C++.

Lưu ý: Từ C++17 trở đi, bạn có thể sử dụng size(ten_mang) để lấy kích thước một cách an toàn hơn, nhưng sizeof là cách truyền thống và vẫn hoạt động tốt cho mảng C-style khi nằm trong cùng phạm vi.

Ví dụ Tổng hợp: Nhập và Tính Tổng các Số trong Mảng

Hãy cùng áp dụng những gì đã học để viết một chương trình nhỏ: yêu cầu người dùng nhập một số lượng nhất định các số nguyên, lưu chúng vào mảng, và sau đó tính tổng của các số đó.

#include <iostream>

int main() {
    const int MAX = 100;
    int a[MAX];

    int n;

    cout << "Nhap so luong so nguyen ban muon nhap (toi da " << MAX << "): ";
    cin >> n;

    if (n <= 0 || n > MAX) {
        cout << "So luong khong hop le. Chuong trinh ket thuc." << endl;
        return 1;
    }

    cout << "Moi ban nhap " << n << " so nguyen:" << endl;
    for (int i = 0; i < n; ++i) {
        cout << "Nhap so thu " << (i + 1) << ": ";
        cin >> a[i];
    }

    int t = 0;
    for (int i = 0; i < n; ++i) {
        t += a[i];
    }

    cout << "\nCac so ban da nhap la:";
    for (int i = 0; i < n; ++i) {
        cout << " " << a[i];
    }
    cout << endl;

    cout << "Tong cua cac so trong mang la: " << t << endl;

    return 0;
}

Example Input:

3
10
20
30

Output:

Nhap so luong so nguyen ban muon nhap (toi da 100): 3
Moi ban nhap 3 so nguyen:
Nhap so thu 1: 10
Nhap so thu 2: 20
Nhap so thu 3: 30

Cac so ban da nhap la: 10 20 30 
Tong cua cac so trong mang la: 60

Giải thích:

  • Chúng ta khai báo mảng cacSo với kích thước tối đa cố định (MAX_SIZE = 100).
  • Biến soLuong lưu trữ số lượng phần tử thực tế mà người dùng muốn nhập, có thể nhỏ hơn hoặc bằng MAX_SIZE.
  • Vòng lặp for đầu tiên dùng để đọc dữ liệu từ bàn phím và lưu vào mảng. Chúng ta lặp từ i = 0 đến soLuong - 1. cacSo[i] là nơi dữ liệu được lưu.
  • Vòng lặp for thứ hai dùng để tính tổng. Chúng ta lặp qua các phần tử đã nhập (từ 0 đến soLuong - 1) và cộng giá trị của chúng vào biến tong.
  • Cuối cùng, chúng ta in ra các số đã nhập và tổng của chúng. Lưu ý vòng lặp in cũng chỉ duyệt soLuong phần tử thực tế.

Ví dụ Khác: Tìm Phần tử Lớn nhất trong Mảng

Một bài toán phổ biến khác là tìm giá trị lớn nhất (hoặc nhỏ nhất) trong mảng.

#include <iostream>

int main() {
    int diem[] = {85, 90, 78, 92, 88, 95, 81};
    int n = sizeof(diem) / sizeof(diem[0]);

    int lon_nhat = diem[0];

    for (int i = 1; i < n; ++i) {
        if (diem[i] > lon_nhat) {
            lon_nhat = diem[i];
        }
    }

    cout << "Mang diem thi: ";
    for (int i = 0; i < n; ++i) {
        cout << diem[i] << (i == n - 1 ? "" : ", ");
    }
    cout << endl;

    cout << "Diem cao nhat trong mang la: " << lon_nhat << endl;

    return 0;
}

Output:

Mang diem thi: 85, 90, 78, 92, 88, 95, 81
Diem cao nhat trong mang la: 95

Giải thích:

  • Chúng ta khởi tạo maxDiem bằng giá trị của phần tử đầu tiên của mảng. Điều này đảm bảo rằng maxDiem ban đầu là một giá trị hợp lệ từ mảng.
  • Vòng lặp bắt đầu từ phần tử thứ hai (index 1) vì phần tử đầu tiên đã dùng để khởi tạo maxDiem.
  • Trong mỗi lần lặp, chúng ta so sánh phần tử hiện tại (diemThi[i]) với maxDiem đang có.
  • Nếu phần tử hiện tại lớn hơn, chúng ta cập nhật maxDiem bằng giá trị của phần tử đó.
  • Sau khi duyệt hết mảng, maxDiem sẽ chứa giá trị lớn nhất tìm được.

Bài tập ví dụ: C++ Bài 7.A1: Phần tử lớn nhất(1)

Viết chương trình để tìm phần tử lớn nhất của một mảng số nguyên có \(N\) phần tử cho trước.

INPUT FORMAT

Dòng đầu tiên chứa số nguyên \(N\) \((1 \leq N \leq 10^6)\) là số lương phần tử

Dòng thứ hai chứa \(N\) số nguyên mỗi số cách nhau một dấu cách là các phần tử của mảng.

OUTPUT FORMAT

In ra số nguyên là phần tử lớn nhất của mảng

Ví dụ:

Input
6
1 4 1 6 12 9
Output
12
Giải thích ví dụ mẫu:

Ví dụ:

  • Giải thích: Tìm số lớn nhất trong mảng 1 4 1 6 12 912.
  1. Đọc dữ liệu đầu vào:

    • Đầu tiên, bạn cần đọc số nguyên N từ đầu vào chuẩn (sử dụng cin).
    • Tiếp theo, bạn cần đọc N số nguyên tiếp theo. Vì N có thể lên tới 10^6, bạn nên sử dụng một cấu trúc dữ liệu có thể thay đổi kích thước động để lưu trữ các số này, ví dụ như vector. Bạn có thể khai báo một vector có kích thước N và sau đó đọc N số lần lượt vào các phần tử của vector.
  2. Tìm phần tử lớn nhất:

    • Cách truyền thống là duyệt qua toàn bộ mảng (hoặc vector), giữ một biến tạm để lưu giá trị lớn nhất tìm thấy cho đến nay. Khởi tạo biến này với giá trị của phần tử đầu tiên (hoặc một giá trị rất nhỏ). Sau đó, với mỗi phần tử tiếp theo, so sánh nó với giá trị lớn nhất hiện tại và cập nhật nếu cần.
    • Tuy nhiên, trong C++, thư viện chuẩn (std) cung cấp hàm rất tiện lợi để làm việc này cho một dãy (range). Bạn nên sử dụng hàm max_element, có trong header <algorithm>. Hàm này nhận hai iterator (chỉ vị trí bắt đầu và kết thúc của dãy) và trả về một iterator trỏ đến phần tử lớn nhất trong dãy đó.
    • Với vector, bạn có thể dễ dàng lấy iterator bắt đầu bằng tên_vector.begin() và iterator kết thúc bằng tên_vector.end().
  3. Lấy giá trị và In kết quả:

    • Hàm max_element trả về một iterator. Để lấy được giá trị thực sự của phần tử mà iterator đó trỏ tới, bạn cần sử dụng toán tử giải tham chiếu (*).
    • Cuối cùng, in giá trị lớn nhất tìm được ra màn hình chuẩn (sử dụng cout).

Tóm tắt các bước chính và các thành phần cần sử dụng:

  • Sử dụng cin để đọc N và các phần tử mảng.
  • Sử dụng vector<int> để lưu trữ N số nguyên.
  • Sử dụng max_element (từ header <algorithm>) với vector.begin()vector.end() để tìm iterator tới phần tử lớn nhất.
  • Sử dụng toán tử * để lấy giá trị từ iterator.
  • Sử dụng cout để in kết quả.
  • Cần include các header <iostream>, <vector>, <algorithm>.

Làm thêm nhiều bài tập miễn phí tại đây

Comments

There are no comments at the moment.