Bài 3.3: Bài tập thực hành kiểu dữ liệu phức tạp trong C++

Chào mừng các bạn đã quay trở lại với series blog C++ của FullhouseDev! Sau khi làm quen với các kiểu dữ liệu cơ bản như số nguyên, số thực, ký tự,... chúng ta nhanh chóng nhận ra rằng chúng không đủ để biểu diễn các thông tin phức tạp trong thế giới thực. Làm sao để lưu trữ danh sách điểm của học sinh? Làm sao để mô tả một đối tượng bao gồm nhiều loại thông tin khác nhau (ví dụ: một cuốn sách có tên, tác giả, năm xuất bản)?

Đó chính là lúc các kiểu dữ liệu phức tạp (complex data types) hoặc còn gọi là kiểu dữ liệu có cấu trúc (structured data types) phát huy sức mạnh. Trong bài thực hành này, chúng ta sẽ cùng nhau khám phá và làm quen với một số kiểu dữ liệu phức tạp thông dụng nhất trong C++, bao gồm: Mảng (Arrays), Cấu trúc (Structs), và hai người bạn cực kỳ hữu ích từ Thư viện chuẩn C++ (Standard Library) là vectorstring.

Nắm vững các kiểu dữ liệu này sẽ là nền tảng vững chắc để bạn xây dựng các chương trình phức tạp và mạnh mẽ hơn rất nhiều!


1. Mảng (Arrays): Ngôi nhà chung của các phần tử cùng loại

Hãy tưởng tượng bạn cần lưu trữ điểm số của 5 môn học. Việc tạo 5 biến riêng biệt (diemMon1, diemMon2, ...) có vẻ không hiệu quả. Đây là lúc mảng tỏa sáng. Mảng là một tập hợp các phần tử cùng kiểu dữ liệu được lưu trữ liên tiếp trong bộ nhớ. Chúng ta có thể truy cập từng phần tử thông qua chỉ số (index).

Đặc điểm chính của mảng trong C++ (mảng C-style truyền thống):

  • Kích thước cố định khi khai báo.
  • Các phần tử được đánh số từ 0 đến kích thước - 1.
  • Truy cập phần tử bằng toán tử [].

Ví dụ 1: Khai báo và sử dụng mảng cơ bản

#include <iostream>

int main() {
    int diem[5]; 
    diem[0] = 85;
    diem[1] = 92;
    diem[2] = 78;
    diem[3] = 95;
    diem[4] = 88;

    cout << "Diem mon thu 3 (chi so 2): " << diem[2] << endl; 

    double nhiet[] = {25.5, 27.0, 26.3, 28.1}; 
    cout << "So luong ngay trong mang nhiet do: " << sizeof(nhiet) / sizeof(nhiet[0]) << endl;

    return 0;
}

Output:

Diem mon thu 3 (chi so 2): 78
So luong ngay trong mang nhiet do: 4

Giải thích:

  • int diemSo[5]; khai báo một mảng tên là diemSo có thể chứa 5 số nguyên.
  • diemSo[index] = value; gán giá trị value cho phần tử tại vị trí index. Nhớ là chỉ số bắt đầu từ 0!
  • cout << diemSo[2]; in ra giá trị của phần tử thứ 3 (tại chỉ số 2).
  • double nhietDo[] = { ... }; là cách khai báo và khởi tạo mảng ngay lập tức. Trình biên dịch sẽ tự động tính kích thước dựa trên số lượng phần tử bạn cung cấp.
  • sizeof(nhietDo) / sizeof(nhietDo[0]) là một kỹ thuật (hơi "thủ công") để tính số lượng phần tử trong mảng C-style khi nó được khai báo trong cùng một scope.

Ví dụ 2: Duyệt qua mảng bằng vòng lặp

Để làm việc với tất cả (hoặc nhiều) phần tử trong mảng, chúng ta thường dùng vòng lặp.

#include <iostream>

int main() {
    int sl[4] = {100, 150, 75, 200}; 

    cout << "So luong san pham theo tung quy:" << endl;
    for (int i = 0; i < 4; ++i) {
        cout << "- Quy " << (i + 1) << ": " << sl[i] << endl;
    }

    cout << "\nSo luong san pham (dung range-based for):" << endl;
    for (int x : sl) {
        cout << x << " ";
    }
    cout << endl;

    return 0;
}

Output:

So luong san pham theo tung quy:
- Quy 1: 100
- Quy 2: 150
- Quy 3: 75
- Quy 4: 200

So luong san pham (dung range-based for):
100 150 75 200

Giải thích:

  • Vòng lặp for (int i = 0; i < 4; ++i) là cách phổ biến để duyệt mảng dựa trên chỉ số. i đi từ 0 đến 3, tương ứng với các chỉ số của mảng soLuongSp.
  • Vòng lặp for (int soLuong : soLuongSp) (range-based for loop) là cách hiện đại và ngắn gọn hơn. Trong mỗi lần lặp, biến soLuong sẽ tự động nhận giá trị của từng phần tử trong mảng soLuongSp theo thứ tự.

Ví dụ 3: Bài tập nhỏ với mảng - Tính tổng và trung bình

Hãy tính tổng điểm và điểm trung bình từ mảng diemSo ở Ví dụ 1.

#include <iostream>

int main() {
    int d[5] = {85, 92, 78, 95, 88};
    int tong = 0;

    for (int x : d) {
        tong += x;
    }

    double tb = static_cast<double>(tong) / (sizeof(d) / sizeof(d[0])); 

    cout << "Tong diem: " << tong << endl;
    cout << "Diem trung binh: " << tb << endl;

    return 0;
}

Output:

Tong diem: 438
Diem trung binh: 87.6

Giải thích:

  • Chúng ta dùng vòng lặp để cộng dồn giá trị của từng phần tử vào biến tongDiem.
  • Để tính trung bình, chúng ta chia tongDiem cho số lượng phần tử (là 5). Việc sử dụng static_cast<double> là quan trọng để đảm bảo phép chia là phép chia số thực, tránh kết quả bị cắt cụt (ví dụ: 440 / 5 = 88, nhưng nếu không ép kiểu và một trong hai là số nguyên, kết quả sẽ là số nguyên). Cách dùng sizeof(diemSo)/sizeof(diemSo[0]) giúp code linh hoạt hơn nếu bạn thay đổi kích thước mảng.

2. Cấu trúc (Structs): Gói gọn dữ liệu khác loại

Mảng rất tốt cho dữ liệu cùng loại, nhưng làm sao để biểu diễn một thứ gì đó có nhiều thuộc tính với các kiểu dữ liệu khác nhau? Ví dụ, thông tin về một người có thể bao gồm Tên (chuỗi), Tuổi (số nguyên), Chiều cao (số thực), Tình trạng hôn nhân (boolean),...

Cấu trúc (struct) trong C++ cho phép chúng ta nhóm các biến thành viên (member variables) lại với nhau dưới một tên duy nhất, tạo thành một kiểu dữ liệu "mới" do người dùng định nghĩa.

Đặc điểm chính của Struct:

  • Tập hợp các thành viên với kiểu dữ liệu bất kỳ.
  • Truy cập các thành viên bằng toán tử . (dot operator).
  • Mặc định, các thành viên trong struct là public (có thể truy cập từ bên ngoài).

Ví dụ 4: Định nghĩa và sử dụng một Struct đơn giản

Hãy tạo một cấu trúc để biểu diễn một điểm trong mặt phẳng 2D.

#include <iostream>

struct Diem {
    int x; 
    int y; 
}; 

int main() {
    Diem p1; 
    p1.x = 10;
    p1.y = 20;

    cout << "Toa do cua diem p1: (" << p1.x << ", " << p1.y << ")" << endl;

    Diem p2 = {30, 40}; 
    cout << "Toa do cua diem p2: (" << p2.x << ", " << p2.y << ")" << endl;

    return 0;
}

Output:

Toa do cua diem p1: (10, 20)
Toa do cua diem p2: (30, 40)

Giải thích:

  • struct Point { ... }; định nghĩa một kiểu dữ liệu mới tên là Point.
  • Bên trong {} là các biến thành viên: int x;int y;.
  • Point p1; tạo một biến (một instance) có kiểu Point.
  • p1.xp1.y dùng toán tử . để truy cập các thành viên xy của biến p1.
  • Point p2 = {30, 40}; là cách khởi tạo nhanh các thành viên khi tạo biến struct.

Ví dụ 5: Struct phức tạp hơn

Hãy tạo một struct để lưu thông tin cơ bản về một sinh viên.

#include <iostream>
#include <string> 

struct SV {
    string ma; 
    string ten;      
    int tuoi;               
    double dtb;   
};

int main() {
    SV sv1;

    sv1.ma = "SV001";
    sv1.ten = "Nguyen Van A";
    sv1.tuoi = 20;
    sv1.dtb = 8.5;

    cout << "Thong tin sinh vien:" << endl;
    cout << "  Ma SV: " << sv1.ma << endl;
    cout << "  Ho Ten: " << sv1.ten << endl;
    cout << "  Tuoi: " << sv1.tuoi << endl;
    cout << "  Diem TB: " << sv1.dtb << endl;

    return 0;
}

Output:

Thong tin sinh vien:
  Ma SV: SV001
  Ho Ten: Nguyen Van A
  Tuoi: 20
  Diem TB: 8.5

Giải thích:

  • Struct SinhVien nhóm 4 thông tin khác loại: string, int, double.
  • Chúng ta tạo biến sv1 kiểu SinhVien và gán giá trị cho từng thành viên bằng toán tử ..

Ví dụ 6: Mảng của Structs

Chúng ta hoàn toàn có thể kết hợp các kiểu dữ liệu phức tạp với nhau. Một trường hợp phổ biến là tạo một mảng chứa các biến struct.

#include <iostream>
#include <string>

struct SP {
    string ten;
    double gia;
    int tonKho;
};

int main() {
    SP ds[3]; 

    ds[0] = {"Laptop", 1200.0, 15}; 

    ds[1].ten = "Chuot";
    ds[1].gia = 25.5;
    ds[1].tonKho = 50;

    ds[2] = {"Ban Phim", 75.0, 30};

    cout << "Danh sach san pham:" << endl;
    for (int i = 0; i < 3; ++i) {
        cout << "- Ten: " << ds[i].ten 
                  << ", Gia: " << ds[i].gia 
                  << ", Ton kho: " << ds[i].tonKho << endl;
    }

    return 0;
}

Output:

Danh sach san pham:
- Ten: Laptop, Gia: 1200, Ton kho: 15
- Ten: Chuot, Gia: 25.5, Ton kho: 50
- Ten: Ban Phim, Gia: 75, Ton kho: 30

Giải thích:

  • SanPham danhSachSP[3]; khai báo một mảng có 3 phần tử, mỗi phần tử là một biến kiểu SanPham.
  • Để truy cập thành viên của một struct nằm trong mảng, chúng ta sử dụng kết hợp toán tử [] (truy cập phần tử mảng) và toán tử . (truy cập thành viên struct): danhSachSP[i].ten.

3. vector: Mảng động linh hoạt

Nhược điểm lớn nhất của mảng C-style là kích thước cố định. Bạn cần biết trước mình sẽ lưu bao nhiêu phần tử. Trong nhiều trường hợp, số lượng dữ liệu có thể thay đổi trong quá trình chạy chương trình. Đây là lúc vector từ Thư viện chuẩn C++ trở thành giải pháp lý tưởng.

vector là một container (bộ chứa) kiểu mảng động. Nó có thể tự động thay đổi kích thước khi bạn thêm hoặc bớt phần tử.

Đặc điểm chính của vector:

  • Kích thước động, có thể thay đổi lúc runtime.
  • Cung cấp nhiều hàm tiện ích (thêm, xóa, lấy kích thước,...).
  • Các phần tử cũng được lưu trữ liền kề trong bộ nhớ (giống mảng).
  • Yêu cầu #include <vector>.

Ví dụ 7: Tạo và thêm phần tử vào vector

#include <iostream>
#include <vector> 

int main() {
    vector<int> v; 

    v.push_back(10);
    v.push_back(25);
    v.push_back(5);

    cout << "Kich thuoc vector sau khi them: " << v.size() << endl; 

    v.push_back(42);
    cout << "Kich thuoc vector sau khi them nua: " << v.size() << endl;

    return 0;
}

Output:

Kich thuoc vector sau khi them: 3
Kich thuoc vector sau khi them nua: 4

Giải thích:

  • vector<int> soNguyen; khai báo một vector có thể chứa các số nguyên. Ban đầu nó rỗng.
  • .push_back(value) là hàm để thêm value vào cuối vector. Vector sẽ tự động "lớn lên" để chứa phần tử mới.
  • .size() là hàm trả về số lượng phần tử hiện có trong vector.

Ví dụ 8: Truy cập và duyệt vector

Bạn có thể truy cập phần tử của vector giống như mảng truyền thống bằng toán tử [], hoặc dùng hàm .at() (an toàn hơn vì nó kiểm tra giới hạn).

#include <iostream>
#include <vector>

int main() {
    vector<double> v = {22.1, 23.5, 21.9, 24.0, 25.2};

    cout << "Nhiet do ngay thu 2 (chi so 1): " << v[1] << endl; 
    cout << "Nhiet do ngay thu 4 (chi so 3): " << v.at(3) << endl; 

    cout << "\nDanh sach nhiet do (for):" << endl;
    for (int i = 0; i < v.size(); ++i) { 
        cout << v[i] << " ";
    }
    cout << endl;

    cout << "Danh sach nhiet do (range-based for):" << endl;
    for (double t : v) {
        cout << t << " ";
    }
    cout << endl;

    return 0;
}

Output:

Nhiet do ngay thu 2 (chi so 1): 23.5
Nhiet do ngay thu 4 (chi so 3): 24
Danh sach nhiet do (for):
22.1 23.5 21.9 24 25.2 
Danh sach nhiet do (range-based for):
22.1 23.5 21.9 24 25.2

Giải thích:

  • nhietDo[1]nhietDo.at(3) đều truy cập phần tử. .at(index) sẽ ném ra một ngoại lệ nếu index nằm ngoài phạm vi hợp lệ, giúp phát hiện lỗi sớm hơn là chỉ truy cập bằng [] (có thể gây ra lỗi runtime không mong muốn).
  • Vòng lặp for truyền thống dùng nhietDo.size() để biết khi nào dừng.
  • Range-based for loop hoạt động rất tốt với vector.

4. string: Làm việc với văn bản dễ dàng

Làm việc với chuỗi ký tự là một tác vụ rất phổ biến trong lập trình. C-style strings (mảng ký tự kết thúc bằng '\0') có thể hơi phức tạp và dễ gây lỗi (ví dụ: tràn bộ đệm). string từ Thư viện chuẩn C++ cung cấp một cách an toàn và tiện lợi để xử lý văn bản.

Đặc điểm chính của string:

  • Quản lý bộ nhớ tự động.
  • Cung cấp nhiều hàm tiện ích (nối chuỗi, tìm kiếm, lấy độ dài, so sánh,...).
  • Có thể thay đổi kích thước.
  • Yêu cầu #include <string>.

Ví dụ 9: Khai báo và thao tác với string

#include <iostream>
#include <string> 

int main() {
    string s1 = "FullhouseDev";
    string s2 = "Xin chao, ";

    string s3 = s2 + s1 + "!"; 

    cout << s3 << endl;

    cout << "Do dai cua chuoi thongDiep: " << s3.length() << endl; 

    cout << "Ky tu dau tien: " << s3[0] << endl;
    cout << "Ky tu cuoi cung: " << s3[s3.length() - 1] << endl;

    return 0;
}

Output:

Xin chao, FullhouseDev!
Do dai cua chuoi thongDiep: 21
Ky tu dau tien: X
Ky tu cuoi cung: !

Giải thích:

  • string ten = "FullhouseDev"; tạo và khởi tạo một chuỗi.
  • Toán tử + được nạp chồng (overload) cho string để nối chuỗi.
  • .length() (hoặc .size()) trả về số ký tự trong chuỗi.
  • Bạn có thể truy cập từng ký tự bằng toán tử [], giống như mảng ký tự.

Ví dụ 10: Đọc chuỗi từ nhập liệu

Khi đọc chuỗi từ bàn phím, cần lưu ý cách cin >> hoạt động và khi nào nên dùng getline.

#include <iostream>
#include <string>
#include <limits> 

int main() {
    string t1;
    string t2;

    cout << "Nhap ten rieng (chi mot tu): ";
    cin >> t1; 
    cout << "Ten rieng ban vua nhap: " << t1 << endl;

    cin.ignore(numeric_limits<streamsize>::max(), '\n'); 

    cout << "Nhap ho va ten day du: ";
    getline(cin, t2); 
    cout << "Ho ten day du ban vua nhap: " << t2 << endl;

    return 0;
}

Output (ví dụ tương tác):

Nhap ten rieng (chi mot tu): An
Ten rieng ban vua nhap: An
Nhap ho va ten day du: Nguyen Van An
Ho ten day du ban vua nhap: Nguyen Van An

Giải thích:

  • cin >> tenRieng; chỉ đọc một từ duy nhất, dừng lại khi gặp khoảng trắng, tab hoặc Enter.
  • getline(cin, tenDayDu); đọc toàn bộ dòng văn bản cho đến khi gặp ký tự Enter.
  • Sau khi dùng cin >>, ký tự Enter vẫn còn lại trong bộ đệm nhập. Nếu không "xóa" nó (bằng cin.ignore(...)), lệnh getline tiếp theo sẽ đọc ngay ký tự Enter đó và kết thúc ngay lập tức mà không chờ bạn nhập gì cả. Lệnh cin.ignore(...) dùng để loại bỏ phần còn lại của dòng trước đó khỏi bộ đệm.

5. Kết hợp các kiểu dữ liệu phức tạp

Sức mạnh thực sự đến khi chúng ta kết hợp các kiểu dữ liệu này. Ví dụ, bạn có thể tạo một vector chứa các đối tượng SinhVien (đã dùng struct ở trên), mỗi đối tượng SinhVien lại chứa string cho tên và mã số.

Ví dụ 11: vector chứa Struct

#include <iostream>
#include <vector>
#include <string>

struct SV {
    string ma;
    string ten;
    int tuoi;
    double dtb;
};

int main() {
    vector<SV> ds;

    SV sv1 = {"SV001", "Nguyen Van A", 20, 8.5};
    ds.push_back(sv1);

    ds.push_back({"SV002", "Tran Thi B", 19, 9.0}); 

    SV sv3;
    sv3.ma = "SV003";
    sv3.ten = "Le Van C";
    sv3.tuoi = 21;
    sv3.dtb = 7.8;
    ds.push_back(sv3);

    cout << "Danh sach sinh vien trong vector:" << endl;
    for (const SV& sv : ds) { 
        cout << "  Ma SV: " << sv.ma 
                  << ", Ho Ten: " << sv.ten 
                  << ", Tuoi: " << sv.tuoi 
                  << ", Diem TB: " << sv.dtb << endl;
    }

    return 0;
}

Output:

Danh sach sinh vien trong vector:
  Ma SV: SV001, Ho Ten: Nguyen Van A, Tuoi: 20, Diem TB: 8.5
  Ma SV: SV002, Ho Ten: Tran Thi B, Tuoi: 19, Diem TB: 9
  Ma SV: SV003, Ho Ten: Le Van C, Tuoi: 21, Diem TB: 7.8

Giải thích:

  • vector<SinhVien> danhSachSinhVien; khai báo một vector mà mỗi phần tử của nó là một struct SinhVien.
  • Chúng ta có thể tạo một biến SinhVien riêng rồi dùng push_back để thêm vào vector, hoặc khởi tạo struct ngay trong lời gọi push_back.
  • Khi duyệt vector, mỗi biến sv trong vòng lặp for (const SinhVien& sv : danhSachSinhVien) là một tham chiếu đến một đối tượng SinhVien trong vector. Việc sử dụng const SinhVien& là một practice tốt: & (tham chiếu) giúp tránh việc copy toàn bộ struct lớn trong mỗi lần lặp (tiết kiệm hiệu năng), và const đảm bảo chúng ta không vô tình thay đổi dữ liệu của sinh viên khi duyệt.

Bài tập thực hành này đã giới thiệu cho bạn những kiểu dữ liệu phức tạp quan trọng nhất trong C++: Mảng (cố định, cùng loại), Cấu trúc (Struct) (gom nhóm khác loại), vector (mảng động, linh hoạt) và string (chuỗi ký tự tiện lợi).

Comments

There are no comments at the moment.