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>

using namespace std;

struct DC {
    string so;
    string duong;
    string tp;
};

struct NV {
    int ma;
    string ten;
    DC dc;
};

int main() {
    NV a;
    a.ma = 101;
    a.ten = "Nguyen Van A";
    a.dc.so = "123";
    a.dc.duong = "Le Loi";
    a.dc.tp = "Ha Noi";

    cout << "Thong tin NV:" << endl;
    cout << "Ma: " << a.ma << endl;
    cout << "Ten: " << a.ten << endl;
    cout << "Dia chi: " << a.dc.so << " "
              << a.dc.duong << ", " << a.dc.tp << endl;

    return 0;
}
Thong tin NV:
Ma: 101
Ten: Nguyen Van A
Dia chi: 123 Le Loi, Ha Noi

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>

using namespace std;

struct SV {
    int ma;
    string ten;
    int tuoi;

    void in() {
        cout << "Ma SV: " << ma << endl;
        cout << "Ho ten: " << ten << endl;
        cout << "Tuoi: " << tuoi << endl;
    }

    void tang() {
        tuoi++;
        cout << ten << " vua them 1 tuoi, hien tai la: " << tuoi << endl;
    }
};

int main() {
    SV s;
    s.ma = 12345;
    s.ten = "Tran Thi B";
    s.tuoi = 20;

    cout << "Lan 1:" << endl;
    s.in();

    s.tang();

    cout << "\nLan 2 sau khi tang tuoi:" << endl;
    s.in();

    return 0;
}
Lan 1:
Ma SV: 12345
Ho ten: Tran Thi B
Tuoi: 20
Tran Thi B vua them 1 tuoi, hien tai la: 21

Lan 2 sau khi tang tuoi:
Ma SV: 12345
Ho ten: Tran Thi B
Tuoi: 21

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>

using namespace std;

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

int main() {
    SP* p = new SP;

    p->ma = 201;
    p->ten = "Sach Lap Trinh C++";
    p->gia = 150000.0;

    cout << "Thong tin SP (qua con tro):" << endl;
    cout << "Ma: " << p->ma << endl;
    cout << "Ten: " << p->ten << endl;
    cout << "Gia: " << p->gia << endl;

    delete p;
    p = nullptr;

    return 0;
}
Thong tin SP (qua con tro):
Ma: 201
Ten: Sach Lap Trinh C++
Gia: 150000

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>

using namespace std;

struct SV {
    int ma;
    string ten;
    int tuoi;

    void in() const {
        cout << "  - Ma SV: " << ma << ", Ho ten: " << ten << ", Tuoi: " << tuoi << endl;
    }
};

int main() {
    vector<SV> dsSV;

    SV s1 = {1001, "Nguyen Van A", 20};
    SV s2 = {1002, "Tran Thi B", 21};
    SV s3 = {1003, "Pham Van C", 19};

    dsSV.push_back(s1);
    dsSV.push_back(s2);
    dsSV.push_back(s3);

    cout << "Danh sach SV:" << endl;

    for (const SV& s : dsSV) {
        s.in();
    }

    return 0;
}
Danh sach SV:
  - Ma SV: 1001, Ho ten: Nguyen Van A, Tuoi: 20
  - Ma SV: 1002, Ho ten: Tran Thi B, Tuoi: 21
  - Ma SV: 1003, Ho ten: Pham Van C, Tuoi: 19

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>

using namespace std;

struct HP {
    string ma;
    string ten;
    int tc;
    double diem;
};

void inHP(const HP& h) {
    cout << "Ma HP: " << h.ma
              << ", Ten HP: " << h.ten
              << ", So TC: " << h.tc
              << ", Diem: " << h.diem << endl;
}

void capNhatDiem(HP& h, double dMoi) {
    h.diem = dMoi;
    cout << "Da cap nhat diem cho " << h.ten << " thanh " << h.diem << endl;
}

HP taoHP(string ma, string ten, int tc, double diem) {
    HP h;
    h.ma = ma;
    h.ten = ten;
    h.tc = tc;
    h.diem = diem;
    return h;
}

int main() {
    HP ds = {"MH001", "Dai So Tuyen Tinh", 3, 7.5};

    cout << "Thong tin ban dau:" << endl;
    inHP(ds);

    capNhatDiem(ds, 8.8);

    cout << "\nThong tin sau cap nhat:" << endl;
    inHP(ds);

    HP lt = taoHP("MH002", "Lap Trinh C++ Nang Cao", 4, 9.0);

    cout << "\nHoc Phan moi duoc tao:" << endl;
    inHP(lt);

    return 0;
}
Thong tin ban dau:
Ma HP: MH001, Ten HP: Dai So Tuyen Tinh, So TC: 3, Diem: 7.5
Da cap nhat diem cho Dai So Tuyen Tinh thanh 8.8

Thong tin sau cap nhat:
Ma HP: MH001, Ten HP: Dai So Tuyen Tinh, So TC: 3, Diem: 8.8

Hoc Phan moi duoc tao:
Ma HP: MH002, Ten HP: Lap Trinh C++ Nang Cao, So TC: 4, Diem: 9

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.