Bài 34.2: Bài tập thực hành Struct nâng cao trong C++

Chào mừng trở lại với chuỗi bài viết về C++! Sau khi đã làm quen với những khái niệm cơ bản của Struct, trong bài viết này, chúng ta sẽ tập trung vào việc thực hành các ứng dụng nâng cao hơn của Struct thông qua các ví dụ cụ thể. Đây là cơ hội tuyệt vời để củng cố kiến thức và khám phá sức mạnh thực sự của việc tổ chức dữ liệu bằng Struct trong các tình huống phức tạp hơn.

Chúng ta sẽ cùng nhau đi qua một số bài tập minh họa, bao gồm:

  1. Sử dụng Struct lồng nhau (Nested Struct).
  2. Thêm hàm thành viên (member functions) vào Struct.
  3. Làm việc với con trỏ tới Struct và cấp phát động.
  4. Sử dụng Struct trong các cấu trúc dữ liệu như mảng hoặc vector.
  5. Truyền và trả về Struct từ hàm.

Hãy bắt đầu nào!

1. Struct Lồng Nhau (Nested Struct)

Đôi khi, dữ liệu của một đối tượng có thể bao gồm các thông tin chi tiết mà bản thân chúng cũng có cấu trúc riêng. Lúc này, nested struct là một giải pháp thanh lịch.

Bài tập minh họa: Định nghĩa một Struct DiaChi và sử dụng nó làm thành viên bên trong một Struct NhanVien.

#include <iostream>
#include <string>

// Định nghĩa Struct DiaChi
struct DiaChi {
    string soNha;
    string duong;
    string thanhPho;
};

// Định nghĩa Struct NhanVien, lồng Struct DiaChi bên trong
struct NhanVien {
    int maSo;
    string ten;
    DiaChi diaChi; // Sử dụng Struct DiaChi làm thành viên
};

int main() {
    // Tạo một đối tượng NhanVien
    NhanVien nv1;

    // Gán giá trị, bao gồm cả các thành viên của Struct DiaChi lồng bên trong
    nv1.maSo = 101;
    nv1.ten = "Nguyen Van A";
    nv1.diaChi.soNha = "123";
    nv1.diaChi.duong = "Le Loi";
    nv1.diaChi.thanhPho = "Ha Noi";

    // In thông tin nhân viên
    cout << "Thong tin Nhan vien:" << endl;
    cout << "Ma so: " << nv1.maSo << endl;
    cout << "Ten: " << nv1.ten << endl;
    cout << "Dia chi: " << nv1.diaChi.soNha << " "
              << nv1.diaChi.duong << ", " << nv1.diaChi.thanhPho << endl;

    return 0;
}

Giải thích:

  • Chúng ta định nghĩa DiaChi trước.
  • Bên trong NhanVien, chúng ta khai báo một thành viên có kiểu là DiaChi với tên diaChi.
  • Khi truy cập các thành viên của DiaChi thông qua đối tượng NhanVien, chúng ta sử dụng toán tử chấm (.) hai lần: nv1.diaChi.soNha. Điều này thể hiện việc truy cập thành viên soNha của struct diaChi, là thành viên của struct nv1.
  • Cách tiếp cận này giúp tổ chức dữ liệu một cách rõ ràngcó cấu trúc hơn.

2. Thêm Hàm Thành Viên (Member Functions) vào Struct

Trong C++, Struct có thể chứa hàm thành viên giống như Class. Điều này cho phép bạn kết hợp dữ liệuhành vi liên quan đến dữ liệu đó vào cùng một đơn vị.

Bài tập minh họa: Định nghĩa một Struct SinhVien có các thông tin cơ bản và thêm một hàm thành viên để in thông tin của sinh viên đó.

#include <iostream>
#include <string>

// Định nghĩa Struct SinhVien với các thành viên dữ liệu và một hàm thành viên
struct SinhVien {
    int maSV;
    string hoTen;
    int tuoi;

    // Hàm thành viên để in thông tin sinh viên
    void inThongTin() {
        cout << "Ma SV: " << maSV << endl;
        cout << "Ho ten: " << hoTen << endl;
        cout << "Tuoi: " << tuoi << endl;
    }

    // Hàm thành viên khác: Tăng tuổi
    void tangTuoi() {
        tuoi++;
        cout << hoTen << " vua them 1 tuoi, hien tai la: " << tuoi << endl;
    }
};

int main() {
    // Tạo một đối tượng SinhVien
    SinhVien sv1;

    // Gán giá trị cho các thành viên dữ liệu
    sv1.maSV = 12345;
    sv1.hoTen = "Tran Thi B";
    sv1.tuoi = 20;

    // Gọi hàm thành viên để in thông tin
    cout << "Lan 1:" << endl;
    sv1.inThongTin();

    // Gọi hàm thành viên de tang tuoi
    sv1.tangTuoi();

    // Goi lai ham de kiem tra tuoi da thay doi
    cout << "\nLan 2 sau khi tang tuoi:" << endl;
    sv1.inThongTin();


    return 0;
}

Giải thích:

  • Bên trong định nghĩa SinhVien, chúng ta khai báo các hàm inThongTin()tangTuoi().
  • Các hàm thành viên này có thể truy cập trực tiếp các thành viên dữ liệu (maSV, hoTen, tuoi) của chính đối tượng mà chúng được gọi.
  • Để gọi hàm thành viên, chúng ta sử dụng toán tử chấm (.) trên đối tượng Struct: sv1.inThongTin(); hoặc sv1.tangTuoi();.
  • Việc này giúp đóng gói dữ liệu và các thao tác liên quan đến dữ liệu đó, làm cho code trở nên có tổ chứcdễ bảo trì hơn.

3. Làm Việc với Con Trỏ tới Struct và Cấp Phát Động

Sử dụng con trỏ tới Struct cho phép chúng ta làm việc với các đối tượng Struct thông qua địa chỉ bộ nhớ của chúng. Kết hợp với cấp phát động (new), chúng ta có thể tạo ra các đối tượng Struct trong bộ nhớ Heap, điều này cực kỳ quan trọng khi xử lý dữ liệu có kích thước không xác định trước hoặc cần tồn tại ngoài phạm vi của một hàm.

Bài tập minh họa: Tạo một đối tượng Struct SanPham bằng cách cấp phát động và truy cập các thành viên của nó bằng con trỏ.

#include <iostream>
#include <string>

// Định nghĩa Struct SanPham
struct SanPham {
    int maSP;
    string tenSP;
    double gia;
};

int main() {
    // Khai báo một con trỏ tới Struct SanPham
    SanPham* conTroSP;

    // Cấp phát động bộ nhớ cho một đối tượng SanPham
    conTroSP = new SanPham;

    // Truy cập và gán giá trị cho các thành viên bằng toán tử mũi tên (->)
    conTroSP->maSP = 201;
    conTroSP->tenSP = "Sach Lap Trinh C++";
    conTroSP->gia = 150000.0;

    // In thông tin sản phẩm thông qua con trỏ
    cout << "Thong tin San pham (qua con tro):" << endl;
    cout << "Ma SP: " << conTroSP->maSP << endl;
    cout << "Ten SP: " << conTroSP->tenSP << endl;
    cout << "Gia: " << conTroSP->gia << endl;

    // Quan trọng: Giải phóng bộ nhớ đã cấp phát động khi không còn sử dụng
    delete conTroSP;
    conTroSP = nullptr; // Tranh dangling pointer

    return 0;
}

Giải thích:

  • Chúng ta khai báo SanPham* conTroSP; để tạo một con trỏ có thể trỏ tới một đối tượng SanPham.
  • conTroSP = new SanPham; yêu cầu hệ điều hành cấp phát đủ bộ nhớ trên Heap để chứa một đối tượng SanPham và trả về địa chỉ của vùng nhớ đó, gán vào conTroSP.
  • Khi làm việc với con trỏ tới Struct, chúng ta sử dụng toán tử mũi tên (->) thay vì toán tử chấm (.) để truy cập các thành viên: conTroSP->maSP tương đương với (*conTroSP).maSP. Toán tử -> tiện lợi hơn.
  • Sau khi sử dụng xong, việc giải phóng bộ nhớ bằng delete conTroSP;bắt buộc để tránh tình trạng rò rỉ bộ nhớ (memory leak). Gán conTroSP = nullptr; sau khi delete là một thực hành tốt để tránh sử dụng lại con trỏ đã bị giải phóng (dangling pointer).

4. Sử Dụng Struct trong Mảng và Vector

Việc lưu trữ nhiều đối tượng Struct có cùng kiểu trong một tập hợp là nhu cầu rất phổ biến. Mảng và vector là hai lựa chọn hiệu quả cho việc này. Vector thường linh hoạt hơn do khả năng thay đổi kích thước động.

Bài tập minh họa: Tạo một vector chứa các đối tượng SinhVien (từ ví dụ 2) và in thông tin của tất cả sinh viên trong vector.

#include <iostream>
#include <string>
#include <vector> // Cần include vector

// Định nghĩa lại Struct SinhVien cho ví dụ này
struct SinhVien {
    int maSV;
    string hoTen;
    int tuoi;

    // Hàm thành viên để in thông tin sinh viên
    void inThongTin() const { // Thêm const vì hàm này không thay đổi dữ liệu
        cout << "  - Ma SV: " << maSV << ", Ho ten: " << hoTen << ", Tuoi: " << tuoi << endl;
    }
};

int main() {
    // Tạo một vector chứa các đối tượng SinhVien
    vector<SinhVien> danhSachSinhVien;

    // Tạo và thêm các đối tượng SinhVien vào vector
    SinhVien sv1 = {1001, "Nguyen Van A", 20};
    SinhVien sv2 = {1002, "Tran Thi B", 21};
    SinhVien sv3 = {1003, "Pham Van C", 19};

    danhSachSinhVien.push_back(sv1);
    danhSachSinhVien.push_back(sv2);
    danhSachSinhVien.push_back(sv3);

    // In thông tin của tất cả sinh viên trong vector
    cout << "Danh sach Sinh vien:" << endl;

    // Sử dụng range-based for loop (C++11 trở lên) để duyệt vector
    for (const SinhVien& sv : danhSachSinhVien) {
        sv.inThongTin(); // Gọi hàm thành viên trên từng đối tượng
    }

    // Hoặc sử dụng vòng lặp for truyền thống
    // for (size_t i = 0; i < danhSachSinhVien.size(); ++i) {
    //     danhSachSinhVien[i].inThongTin();
    // }

    return 0;
}

Giải thích:

  • Chúng ta khai báo vector<SinhVien> danhSachSinhVien;. Điều này tạo ra một vector có khả năng lưu trữ các đối tượng có kiểu là SinhVien.
  • Sử dụng push_back() để thêm các đối tượng SinhVien vào cuối vector.
  • Chúng ta sử dụng một vòng lặp for dựa trên phạm vi (range-based for loop) để duyệt qua từng đối tượng SinhVien trong vector.
  • Đối với mỗi đối tượng sv trong vector, chúng ta gọi hàm thành viên sv.inThongTin().
  • Lưu ý const SinhVien& sv: Sử dụng tham chiếu const (const &) khi duyệt qua các phần tử của vector là một thực hành tốt để tránh việc tạo bản sao không cần thiết của từng đối tượng SinhVien (tiết kiệm hiệu năng) và đảm bảo rằng vòng lặp không làm thay đổi dữ liệu của các đối tượng đó. Thêm const vào cuối hàm inThongTin() cũng củng cố điều này, cho biết hàm không thay đổi trạng thái của đối tượng.

5. Truyền và Trả Về Struct từ Hàm

Làm việc với Struct thường bao gồm việc truyền chúng vào các hàm để xử lý hoặc nhận chúng làm giá trị trả về từ hàm. Có nhiều cách để truyền Struct tới hàm, mỗi cách có ưu nhược điểm riêng.

Bài tập minh họa: Viết các hàm để in thông tin Struct, cập nhật thông tin Struct và tạo mới một Struct rồi trả về.

#include <iostream>
#include <string>

// Định nghĩa Struct HocPhan
struct HocPhan {
    string maHP;
    string tenHP;
    int soTinChi;
    double diem;
};

// Hàm in thông tin HocPhan - truyền bằng tham chiếu const (hiệu quả, không thay đổi)
void inThongTinHocPhan(const HocPhan& hp) {
    cout << "Ma HP: " << hp.maHP
              << ", Ten HP: " << hp.tenHP
              << ", So TC: " << hp.soTinChi
              << ", Diem: " << hp.diem << endl;
}

// Hàm cap nhat diem cho HocPhan - truyen bang tham chieu (co the thay doi du lieu goc)
void capNhatDiem(HocPhan& hp, double diemMoi) {
    hp.diem = diemMoi;
    cout << "Da cap nhat diem cho " << hp.tenHP << " thanh " << hp.diem << endl;
}

// Hàm tao mot HocPhan moi - tra ve mot doi tuong Struct
HocPhan taoHocPhan(string ma, string ten, int tc, double diem) {
    HocPhan moi;
    moi.maHP = ma;
    moi.tenHP = ten;
    moi.soTinChi = tc;
    moi.diem = diem;
    return moi; // Trả về đối tượng Struct
}

int main() {
    // Tao mot doi tuong HocPhan ban dau
    HocPhan daiSo = {"MH001", "Dai So Tuyen Tinh", 3, 7.5};

    // In thong tin ban dau (truyen bang tham chieu const)
    cout << "Thong tin ban dau:" << endl;
    inThongTinHocPhan(daiSo);

    // Cap nhat diem (truyen bang tham chieu)
    capNhatDiem(daiSo, 8.8);

    // Kiem tra thong tin sau cap nhat (truyen bang tham chieu const)
    cout << "\nThong tin sau cap nhat:" << endl;
    inThongTinHocPhan(daiSo); // Diem da thay doi

    // Tao mot HocPhan moi (tra ve doi tuong Struct)
    HocPhan lapTrinh = taoHocPhan("MH002", "Lap Trinh C++ Nang Cao", 4, 9.0);

    // In thong tin HocPhan moi
    cout << "\nHoc Phan moi duoc tao:" << endl;
    inThongTinHocPhan(lapTrinh);

    return 0;
}

Giải thích:

  • inThongTinHocPhan(const HocPhan& hp): Hàm này nhận một đối tượng HocPhan bằng tham chiếu const (const &). Điều này có nghĩa là hàm nhận địa chỉ của đối tượng gốc chứ không tạo bản sao (hiệu quả hơn khi Struct lớn), và từ khóa const đảm bảo rằng hàm không thể thay đổi dữ liệu của đối tượng hp gốc. Đây là cách khuyến khích nhất để truyền Struct vào hàm chỉ để đọc dữ liệu.
  • capNhatDiem(HocPhan& hp, double diemMoi): Hàm này nhận một đối tượng HocPhan bằng tham chiếu (&). Điều này cho phép hàm truy cập và thay đổi dữ liệu của đối tượng hp gốc được truyền vào. Bất kỳ thay đổi nào bên trong hàm này sẽ ảnh hưởng đến đối tượng bên ngoài hàm.
  • HocPhan taoHocPhan(...): Hàm này tạo một đối tượng HocPhan cục bộ (HocPhan moi;) và sau đó sử dụng return moi; để trả về bản sao của đối tượng đó. C++ có các cơ chế tối ưu hóa (như Return Value Optimization - RVO hoặc Named RVO - NRVO) để giảm thiểu hoặc loại bỏ việc sao chép trong nhiều trường hợp, nhưng về mặt logic, hàm trả về một bản sao.

Comments

There are no comments at the moment.