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>

// Định nghĩa struct Diem
struct Diem {
    int x; // Thành viên lưu hoành độ
    int y; // Thành viên lưu tung độ
}; // Dấu chấm phẩy cuối cùng là bắt buộc

int main() {
    // struct Diem đã được định nghĩa và có thể sử dụng như một kiểu dữ liệu mới
    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() {
    // Tạo một đối tượng (biến) có kiểu Diem
    Diem diem1;

    // Tạo một đối tượng Diem và khởi tạo ngay lập tức
    // Sử dụng danh sách khởi tạo (initializer list)
    Diem diem2 = {10, 20};

    // Bạn cũng có thể tạo và khởi tạo mà không cần dấu '='
    Diem diem3 {5, 15}; // Cách hiện đại hơn trong C++11 trở lên

    // Tạo một đối tượng Diem và sao chép từ một đối tượng khác
    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;
}

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() {
    Diem diem1; // Tạo đối tượng

    // Gán giá trị cho các thành viên của diem1
    diem1.x = 5;
    diem1.y = 8;

    // Truy cập và in giá trị của các thành viên
    cout << "Toa do diem 1: (" << diem1.x << ", " << diem1.y << ")" << endl;

    Diem diem2 = {10, 20}; // Khởi tạo ngay

    // Truy cập và in giá trị của diem2
    cout << "Toa do diem 2: (" << diem2.x << ", " << diem2.y << ")" << endl;

    // Thay đổi giá trị của một thành viên của diem2
    diem2.x = 100;
    cout << "Toa do diem 2 sau khi thay doi x: (" << diem2.x << ", " << diem2.y << ")" << endl;

    return 0;
}

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> // Cần cho string

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

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

    // Gán thông tin cho quyenSach1
    quyenSach1.tieuDe = "Lap trinh C++ co ban";
    quyenSach1.tacGia = "FullhouseDev";
    quyenSach1.namXuatBan = 2024;
    quyenSach1.giaTien = 250.0; // Nghin dong :)
    quyenSach1.conHang = true;

    // In thông tin sách
    cout << "Thong tin quyen sach 1:" << endl;
    cout << "Tieu de: " << quyenSach1.tieuDe << endl;
    cout << "Tac gia: " << quyenSach1.tacGia << endl;
    cout << "Nam xuat ban: " << quyenSach1.namXuatBan << endl;
    cout << "Gia tien: " << quyenSach1.giaTien << " nghin dong" << endl;
    cout << "Con hang: " << (quyenSach1.conHang ? "Co" : "Khong") << endl;

    // Tạo và khởi tạo ngay một cuốn sách khác
    Sach quyenSach2 = {"Toan roi rac", "Mot giao su gioi", 2020, 180.5, false};

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


    return 0;
}

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>

// Định nghĩa struct cho DiaChi
struct DiaChi {
    int soNha;
    string tenDuong;
    string thanhPho;
    string quocGia;
};

// Định nghĩa struct SinhVien sử dụng DiaChi
struct SinhVien {
    int maSoSinhVien;
    string hoTen;
    double diemGPA;
    DiaChi diaChiLienLac; // Thành viên có kiểu là struct DiaChi
};

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

    // Gán thông tin cơ bản cho sv1
    sv1.maSoSinhVien = 12345;
    sv1.hoTen = "Pham Van B";
    sv1.diemGPA = 3.9;

    // Gán thông tin địa chỉ cho thành viên diaChiLienLac (là một struct)
    sv1.diaChiLienLac.soNha = 15; // Truy cập thành viên của struct lồng nhau
    sv1.diaChiLienLac.tenDuong = "Le Loi";
    sv1.diaChiLienLac.thanhPho = "Ha Noi";
    sv1.diaChiLienLac.quocGia = "Viet Nam";

    // In thông tin sinh viên và địa chỉ
    cout << "Thong tin sinh vien:" << endl;
    cout << "Ma so: " << sv1.maSoSinhVien << endl;
    cout << "Ho ten: " << sv1.hoTen << endl;
    cout << "GPA: " << sv1.diemGPA << endl;
    cout << "Dia chi: "
              << sv1.diaChiLienLac.soNha << " "
              << sv1.diaChiLienLac.tenDuong << ", "
              << sv1.diaChiLienLac.thanhPho << ", "
              << sv1.diaChiLienLac.quocGia << endl;

    return 0;
}

Để 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 namXuatBan;
};

int main() {
    // Khai báo một mảng gồm 3 đối tượng Sach
    Sach danhSachThuVien[3];

    // Gán dữ liệu cho từng phần tử trong mảng
    danhSachThuVien[0] = {"Sach 1", "Tac gia A", 2022};
    danhSachThuVien[1] = {"Sach 2", "Tac gia B", 2023};
    danhSachThuVien[2] = {"Sach 3", "Tac gia C", 2021};

    // Duyệt qua mảng và in thông tin từng cuốn sách
    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: " << danhSachThuVien[i].tieuDe << endl;
        cout << "  Tac gia: " << danhSachThuVien[i].tacGia << endl;
        cout << "  Nam XB: " << danhSachThuVien[i].namXuatBan << endl;
    }

    return 0;
}

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 banKinh;
};

// Truyền theo giá trị: Hàm nhận bản sao
void inBanKinh(HinhTron ht) {
    cout << "Trong ham inBanKinh: Ban kinh = " << ht.banKinh << endl;
    ht.banKinh = 100.0; // Thay đổi bản sao, không ảnh hưởng bản gốc
    cout << "Trong ham inBanKinh: Ban kinh sau khi thay doi = " << ht.banKinh << endl;
}

// Truyền theo tham chiếu: Hàm làm việc trực tiếp trên bản gốc
void tangBanKinh(HinhTron& ht) {
    cout << "Trong ham tangBanKinh: Ban kinh truoc tang = " << ht.banKinh << endl;
    ht.banKinh += 1.0; // Thay đổi bản gốc
    cout << "Trong ham tangBanKinh: Ban kinh sau khi tang = " << ht.banKinh << endl;
}

// Truyền theo tham chiếu hằng: Đọc dữ liệu hiệu quả mà không cho phép thay đổi
void inChiTietHinhTron(const HinhTron& ht) {
     cout << "Trong ham inChiTietHinhTron: Ban kinh = " << ht.banKinh << endl;
     // ht.banKinh = 20.0; // Lỗi biên dịch! Không thể thay đổi khi là const reference
}


int main() {
    HinhTron hinhA = {5.0};
    HinhTron hinhB = {7.0};
    HinhTron hinhC = {10.0};

    cout << "--- Truyen theo gia tri ---" << endl;
    inBanKinh(hinhA);
    cout << "Sau khi goi inBanKinh, ban kinh hinh A: " << hinhA.banKinh << endl; // Vẫn là 5.0

    cout << "\n--- Truyen theo tham chieu ---" << endl;
    tangBanKinh(hinhB);
    cout << "Sau khi goi tangBanKinh, ban kinh hinh B: " << hinhB.banKinh << endl; // Bây giờ là 8.0

    cout << "\n--- Truyen theo tham chieu hang ---" << endl;
    inChiTietHinhTron(hinhC);
    cout << "Sau khi goi inChiTietHinhTron, ban kinh hinh C: " << hinhC.banKinh << endl; // Vẫn là 10.0

    return 0;
}
  • 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>

// Ví dụ cuối cùng: Struct ThoiGian
struct ThoiGian {
    int gio;
    int phut;
    int giay;
};

// Hàm hiển thị thời gian (truyền theo tham chiếu hằng)
void hienThiThoiGian(const ThoiGian& tg) {
    cout << "Thoi gian: "
              << (tg.gio < 10 ? "0" : "") << tg.gio << ":"
              << (tg.phut < 10 ? "0" : "") << tg.phut << ":"
              << (tg.giay < 10 ? "0" : "") << tg.giay
              << endl;
}

// Hàm thêm giây vào thời gian (truyền theo tham chiếu để thay đổi)
void themGiay(ThoiGian& tg, int soGiayThem) {
    tg.giay += soGiayThem;
    tg.phut += tg.giay / 60;
    tg.giay %= 60;
    tg.gio += tg.phut / 60;
    tg.phut %= 60;
    tg.gio %= 24; // Giả định thời gian 24h
}


int main() {
    // Tạo đối tượng ThoiGian
    ThoiGian bayGio = {14, 30, 45};

    // Hiển thị thời gian ban đầu
    cout << "Bay gio la: ";
    hienThiThoiGian(bayGio);

    // Thêm 75 giây
    themGiay(bayGio, 75); // 75 giây = 1 phút 15 giây

    // Hiển thị thời gian sau khi thêm
    cout << "Sau khi them 75 giay: ";
    hienThiThoiGian(bayGio);

    // Tạo một đối tượng thời gian khác và hiển thị
    ThoiGian sauMotTieng = {15, 30, 45};
    cout << "Mot tieng sau la: ";
    hienThiThoiGian(sauMotTieng);


    return 0;
}

Comments

There are no comments at the moment.