Bài 34.1: Bài tập thực hành Struct cơ bản trong C++

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

Trong lập trình, chúng ta thường xuyên cần làm việc với các dữ liệu có liên quan đến nhau nhưng lại có kiểu dữ liệu khác nhau. Ví dụ, thông tin về một cuốn sách có thể bao gồm: tiêu đề (chuỗi ký tự), tác giả (chuỗi ký tự), năm xuất bản (số nguyên), giá tiền (số thực). Nếu chúng ta chỉ dùng các biến riêng lẻ, việc quản lý và truyền các dữ liệu này giữa các phần của chương trình sẽ trở nên rắc rối và khó khăn.

May mắn thay, C++ cung cấp một công cụ mạnh mẽ để giải quyết vấn đề này: Struct. Struct (viết tắt của structure - cấu trúc) cho phép chúng ta nhóm các biến có kiểu dữ liệu khác nhau lại với nhau dưới một cái tên duy nhất. Hãy cùng đi sâu vào tìm hiểu và thực hành với Struct cơ bản nhé!

Struct là gì?

Struct là một kiểu dữ liệu do người dùng định nghĩa (user-defined data type). Nó hoạt động như một bản thiết kế (blueprint) hoặc khuôn mẫu (template) cho phép bạn tạo ra các đối tượng (object) chứa nhiều thành viên (members). Các thành viên này có thể là các biến với bất kỳ kiểu dữ liệu nào (int, float, double, bool, string, thậm chí là các struct khác).

Mục đích chính của struct là tổ chức dữ liệu một cách logic, giúp code của bạn dễ đọc, dễ hiểu và dễ bảo trì hơn. Thay vì làm việc với hàng tá biến riêng lẻ, bạn chỉ cần làm việc với một đối tượng struct duy nhất.

Khai báo (Định nghĩa) một Struct

Để tạo một struct, chúng ta sử dụng từ khóa struct, theo sau là tên của struct và một cặp dấu ngoặc nhọn {} chứa các thành viên của nó. Mỗi thành viên được khai báo giống như một biến thông thường và kết thúc bằng dấu chấm phẩy ;. Toàn bộ khối khai báo struct cũng phải kết thúc bằng dấu chấm phẩy ;.

Đây là cú pháp cơ bản:

struct TenStruct {
    KieuDuLieu1 ten_thanh_vien1;
    KieuDuLieu2 ten_thanh_vien2;
    // ... có thể có nhiều thành viên khác
    KieuDuLieuN ten_thanh_vienN;
}; // Kết thúc bằng dấu chấm phẩy!

Hãy thử định nghĩa một struct đơn giản để lưu trữ tọa độ của một điểm 2D:

#include <iostream>

struct Diem {
    int x;
    int y;
};

int main() {
    return 0;
}

Trong ví dụ này, chúng ta đã tạo ra một kiểu dữ liệu mới có tên là Diem. Kiểu dữ liệu này bao gồm hai thành viên: xy, cả hai đều có kiểu là int. Lưu ý rằng việc định nghĩa struct không tạo ra bất kỳ biến nào ngay lập tức; nó chỉ tạo ra một khuôn mẫu.

Tạo đối tượng (Biến) từ Struct

Sau khi đã định nghĩa struct, chúng ta có thể tạo ra các đối tượng (hay còn gọi là biến) của struct đó. Mỗi đối tượng sẽ có bản sao riêng của tất cả các thành viên được khai báo trong struct.

Để tạo một đối tượng struct, bạn chỉ cần khai báo biến với tên struct là kiểu dữ liệu:

#include <iostream>

struct Diem {
    int x;
    int y;
};

int main() {
    using namespace std;

    Diem diem1;
    Diem diem2 = {10, 20};
    Diem diem3 {5, 15};
    Diem diem4 = diem2;

    cout << "Diem 1 da duoc tao." << endl;
    cout << "Diem 2 duoc khoi tao la (" << diem2.x << ", " << diem2.y << ")." << endl;
    cout << "Diem 3 duoc khoi tao la (" << diem3.x << ", " << diem3.y << ")." << endl;
    cout << "Diem 4 duoc sao chep tu Diem 2 la (" << diem4.x << ", " << diem4.y << ")." << endl;

    return 0;
}

Output:

Diem 1 da duoc tao.
Diem 2 duoc khoi tao la (10, 20).
Diem 3 duoc khoi tao la (5, 15).
Diem 4 duoc sao chep tu Diem 2 la (10, 20).

Trong ví dụ trên, diem1, diem2, diem3, và diem4 đều là các đối tượng của struct Diem. Mỗi đối tượng này có bản sao riêng của xy.

Truy cập các Thành viên của Struct

Để làm việc với dữ liệu bên trong một đối tượng struct (tức là truy cập các thành viên của nó), chúng ta sử dụng toán tử dấu chấm (.).

Cú pháp là: ten_doi_tuong.ten_thanh_vien

Hãy xem cách truy cập và gán giá trị cho các thành viên của đối tượng Diem:

#include <iostream>

struct Diem {
    int x;
    int y;
};

int main() {
    using namespace std;

    Diem diem1;

    diem1.x = 5;
    diem1.y = 8;

    cout << "Toa do diem 1: (" << diem1.x << ", " << diem1.y << ")" << endl;

    Diem diem2 = {10, 20};

    cout << "Toa do diem 2: (" << diem2.x << ", " << diem2.y << ")" << endl;

    diem2.x = 100;
    cout << "Toa do diem 2 sau khi thay doi x: (" << diem2.x << ", " << diem2.y << ")" << endl;

    return 0;
}

Output:

Toa do diem 1: (5, 8)
Toa do diem 2: (10, 20)
Toa do diem 2 sau khi thay doi x: (100, 20)

Như bạn thấy, chúng ta dễ dàng đọc hoặc ghi dữ liệu vào các thành viên xy của từng đối tượng Diem bằng cách sử dụng toán tử ..

Struct với các Kiểu dữ liệu Thành viên Đa dạng

Một struct có thể chứa các thành viên với bất kỳ kiểu dữ liệu nào, bao gồm cả string, double, bool, v.v. Điều này cho phép chúng ta tạo ra các cấu trúc dữ liệu phức tạp và thực tế hơn.

Hãy định nghĩa một struct Sach để lưu thông tin sách:

#include <iostream>
#include <string>

struct Sach {
    string tieuDe;
    string tacGia;
    int namXB;
    double giaTien;
    bool conHang;
};

int main() {
    using namespace std;

    Sach s1;

    s1.tieuDe = "Lap trinh C++ co ban";
    s1.tacGia = "FullhouseDev";
    s1.namXB = 2024;
    s1.giaTien = 250.0;
    s1.conHang = true;

    cout << "Thong tin sach 1:" << endl;
    cout << "Tieu de: " << s1.tieuDe << endl;
    cout << "Tac gia: " << s1.tacGia << endl;
    cout << "Nam xuat ban: " << s1.namXB << endl;
    cout << "Gia tien: " << s1.giaTien << " nghin dong" << endl;
    cout << "Con hang: " << (s1.conHang ? "Co" : "Khong") << endl;

    Sach s2 = {"Toan roi rac", "Mot giao su gioi", 2020, 180.5, false};

    cout << "\nThong tin sach 2:" << endl;
    cout << "Tieu de: " << s2.tieuDe << endl;
    cout << "Tac gia: " << s2.tacGia << endl;
    cout << "Nam xuat ban: " << s2.namXB << endl;
    cout << "Gia tien: " << s2.giaTien << " nghin dong" << endl;
    cout << "Con hang: " << (s2.conHang ? "Co" : "Khong") << endl;

    return 0;
}

Output:

Thong tin sach 1:
Tieu de: Lap trinh C++ co ban
Tac gia: FullhouseDev
Nam xuat ban: 2024
Gia tien: 250 nghin dong
Con hang: Co

Thong tin sach 2:
Tieu de: Toan roi rac
Tac gia: Mot giao su gioi
Nam xuat ban: 2020
Gia tien: 180.5 nghin dong
Con hang: Khong

Ví dụ này cho thấy struct Sach chứa các thành viên với kiểu string, int, double, và bool. Chúng ta khởi tạo và truy cập các thành viên này một cách dễ dàng bằng toán tử ..

Struct Lồng nhau (Nested Structs)

Một thành viên của struct cũng có thể là một struct khác. Điều này rất hữu ích khi bạn muốn tổ chức dữ liệu theo các cấp độ phức tạp hơn.

Ví dụ, thông tin về một sinh viên có thể bao gồm tên, mã số, và địa chỉ. Địa chỉ lại có thể là một struct riêng với các thành viên như số nhà, tên đường, thành phố, quốc gia.

#include <iostream>
#include <string>

struct DiaChi {
    int soNha;
    string duong;
    string thanhPho;
    string quocGia;
};

struct SinhVien {
    int maSV;
    string ten;
    double gpa;
    DiaChi dcLL;
};

int main() {
    using namespace std;

    SinhVien sv1;

    sv1.maSV = 12345;
    sv1.ten = "Pham Van B";
    sv1.gpa = 3.9;

    sv1.dcLL.soNha = 15;
    sv1.dcLL.duong = "Le Loi";
    sv1.dcLL.thanhPho = "Ha Noi";
    sv1.dcLL.quocGia = "Viet Nam";

    cout << "Thong tin sinh vien:" << endl;
    cout << "Ma so: " << sv1.maSV << endl;
    cout << "Ho ten: " << sv1.ten << endl;
    cout << "GPA: " << sv1.gpa << endl;
    cout << "Dia chi: "
              << sv1.dcLL.soNha << " "
              << sv1.dcLL.duong << ", "
              << sv1.dcLL.thanhPho << ", "
              << sv1.dcLL.quocGia << endl;

    return 0;
}

Output:

Thong tin sinh vien:
Ma so: 12345
Ho ten: Pham Van B
GPA: 3.9
Dia chi: 15 Le Loi, Ha Noi, Viet Nam

Để truy cập các thành viên của struct lồng nhau (DiaChi bên trong SinhVien), chúng ta sử dụng toán tử chấm hai lần: sv1.diaChiLienLac.soNha.

Mảng các Struct

Bạn có thể tạo một mảng mà mỗi phần tử của mảng đó là một đối tượng struct. Điều này rất tiện lợi khi bạn cần quản lý một danh sách các đối tượng cùng loại.

Ví dụ, một mảng lưu danh sách các cuốn sách:

#include <iostream>
#include <string>

struct Sach {
    string tieuDe;
    string tacGia;
    int namXB;
};

int main() {
    using namespace std;

    Sach dsTV[3];

    dsTV[0] = {"Sach 1", "Tac gia A", 2022};
    dsTV[1] = {"Sach 2", "Tac gia B", 2023};
    dsTV[2] = {"Sach 3", "Tac gia C", 2021};

    cout << "Danh sach cac cuon sach trong thu vien:" << endl;
    for (int i = 0; i < 3; ++i) {
        cout << "Sach " << i + 1 << ":" << endl;
        cout << "  Tieu de: " << dsTV[i].tieuDe << endl;
        cout << "  Tac gia: " << dsTV[i].tacGia << endl;
        cout << "  Nam XB: " << dsTV[i].namXB << endl;
    }

    return 0;
}

Output:

Danh sach cac cuon sach trong thu vien:
Sach 1:
  Tieu de: Sach 1
  Tac gia: Tac gia A
  Nam XB: 2022
Sach 2:
  Tieu de: Sach 2
  Tac gia: Tac gia B
  Nam XB: 2023
Sach 3:
  Tieu de: Sach 3
  Tac gia: Tac gia C
  Nam XB: 2021

Khi làm việc với mảng các struct, chúng ta sử dụng toán tử [] để truy cập phần tử mảng, sau đó sử dụng toán tử . để truy cập thành viên của struct tại phần tử đó: danhSachThuVien[i].tieuDe.

Truyền Struct vào Hàm

Giống như bất kỳ kiểu dữ liệu nào khác, bạn có thể truyền các đối tượng struct vào hàm. Có ba cách chính để làm điều này:

  1. Truyền theo giá trị (Pass by value): Hàm nhận một bản sao của đối tượng struct. Mọi thay đổi bên trong hàm sẽ chỉ ảnh hưởng đến bản sao đó, không ảnh hưởng đến đối tượng gốc. Đây là cách mặc định.
  2. Truyền theo tham chiếu (Pass by reference): Hàm nhận một tham chiếu đến đối tượng gốc. Mọi thay đổi bên trong hàm sẽ ảnh hưởng trực tiếp đến đối tượng gốc. Sử dụng toán tử &.
  3. Truyền theo tham chiếu hằng (Pass by const reference): Tương tự như truyền theo tham chiếu, nhưng đối tượng được truyền vào là không thể thay đổi bên trong hàm. Đây là cách hiệu quả và an toàn khi bạn chỉ cần đọc dữ liệu của struct trong hàm mà không muốn tạo bản sao lớn và không muốn vô tình thay đổi dữ liệu gốc. Sử dụng toán tử const &.

Hãy xem ví dụ với struct HinhTron:

#include <iostream>

struct HinhTron {
    double r; // r = banKinh
};

void inR(HinhTron h) {
    cout << "Trong ham inR: Ban kinh = " << h.r << endl;
    h.r = 100.0;
    cout << "Trong ham inR: Ban kinh sau khi thay doi = " << h.r << endl;
}

void tangR(HinhTron& h) {
    cout << "Trong ham tangR: Ban kinh truoc tang = " << h.r << endl;
    h.r += 1.0;
    cout << "Trong ham tangR: Ban kinh sau khi tang = " << h.r << endl;
}

void inChiTiet(const HinhTron& h) {
     cout << "Trong ham inChiTiet: Ban kinh = " << h.r << endl;
}

int main() {
    using namespace std;

    HinhTron hA = {5.0};
    HinhTron hB = {7.0};
    HinhTron hC = {10.0};

    cout << "--- Truyen theo gia tri ---" << endl;
    inR(hA);
    cout << "Sau khi goi inR, ban kinh hinh A: " << hA.r << endl;

    cout << "\n--- Truyen theo tham chieu ---" << endl;
    tangR(hB);
    cout << "Sau khi goi tangR, ban kinh hinh B: " << hB.r << endl;

    cout << "\n--- Truyen theo tham chieu hang ---" << endl;
    inChiTiet(hC);
    cout << "Sau khi goi inChiTiet, ban kinh hinh C: " << hC.r << endl;

    return 0;
}

Output:

--- Truyen theo gia tri ---
Trong ham inR: Ban kinh = 5
Trong ham inR: Ban kinh sau khi thay doi = 100
Sau khi goi inR, ban kinh hinh A: 5

--- Truyen theo tham chieu ---
Trong ham tangR: Ban kinh truoc tang = 7
Trong ham tangR: Ban kinh sau khi tang = 8
Sau khi goi tangR, ban kinh hinh B: 8

--- Truyen theo tham chieu hang ---
Trong ham inChiTiet: Ban kinh = 10
Sau khi goi inChiTiet, ban kinh hinh C: 10
  • Truyền theo giá trị phù hợp cho các struct nhỏ hoặc khi bạn muốn hàm làm việc trên bản sao riêng.
  • Truyền theo tham chiếu phù hợp khi hàm cần thay đổi dữ liệu của đối tượng gốc.
  • Truyền theo tham chiếu hằng là lựa chọn tốt nhất khi hàm chỉ cần đọc dữ liệu, đặc biệt với các struct lớn, vì nó tránh việc tạo bản sao tốn kém và ngăn chặn việc thay đổi dữ liệu gốc một cách vô ý.

Tại sao nên dùng Struct?

Việc sử dụng struct mang lại nhiều lợi ích:

  • Tổ chức code: Gói gọn dữ liệu liên quan lại với nhau, giúp code mạch lạc và dễ quản lý hơn.
  • Đọc hiểu: Khi nhìn vào một đối tượng struct, bạn biết ngay nó đại diện cho một thực thể nào đó (ví dụ: một Điểm, một Cuốn sách, một Sinh viên) và chứa những thông tin gì.
  • Dễ bảo trì: Nếu bạn cần thêm một thuộc tính mới cho một thực thể (ví dụ: thêm ngày sinh vào struct SinhVien), bạn chỉ cần chỉnh sửa định nghĩa struct ở một nơi duy nhất.
  • Truyền dữ liệu hiệu quả: Thay vì truyền nhiều đối số riêng lẻ vào hàm, bạn có thể truyền một đối tượng struct duy nhất (đặc biệt là truyền bằng tham chiếu hằng để tránh sao chép).

Kết bài

Qua bài thực hành này, chúng ta đã cùng nhau tìm hiểu những kiến thức cơ bản nhất về Struct trong C++: cách định nghĩa, tạo đối tượng, truy cập thành viên, làm việc với các kiểu dữ liệu thành viên đa dạng, struct lồng nhau, mảng struct và cách truyền struct vào hàm.

Struct là một khái niệm nền tảng và cực kỳ quan trọng trong lập trình C++, giúp bạn tổ chức dữ liệu hiệu quả hơn rất nhiều.

Chúc mừng bạn đã hoàn thành bài học về Struct cơ bản! Hãy dành thời gian thực hành bằng cách tự định nghĩa các struct cho các đối tượng quen thuộc (như Ô tô, Sản phẩm, Tài khoản ngân hàng) và luyện tập tạo, truy cập, cũng như truyền chúng vào các hàm. Việc thực hành sẽ giúp bạn nắm vững kiến thức này.

#include <iostream>
#include <string>
#include <iomanip> // For setw, setfill

struct ThoiGian {
    int h; // gio
    int p; // phut
    int s; // giay
};

void hienThiTG(const ThoiGian& tg) {
    cout << "Thoi gian: "
              << setw(2) << setfill('0') << tg.h << ":"
              << setw(2) << setfill('0') << tg.p << ":"
              << setw(2) << setfill('0') << tg.s
              << endl;
}

void themGiay(ThoiGian& tg, int themS) {
    tg.s += themS;
    tg.p += tg.s / 60;
    tg.s %= 60;
    tg.h += tg.p / 60;
    tg.p %= 60;
    tg.h %= 24;
}

int main() {
    using namespace std;

    ThoiGian bayGio = {14, 30, 45};

    cout << "Bay gio la: ";
    hienThiTG(bayGio);

    themGiay(bayGio, 75);

    cout << "Sau khi them 75 giay: ";
    hienThiTG(bayGio);

    ThoiGian sauMotTieng = {15, 30, 45};
    cout << "Mot tieng sau la: ";
    hienThiTG(sauMotTieng);

    return 0;
}

Output:

Bay gio la: Thoi gian: 14:30:45
Sau khi them 75 giay: Thoi gian: 14:31:00
Mot tieng sau la: Thoi gian: 15:30:45

Comments

There are no comments at the moment.