Bài 35.5: Bài tập thực hành lập trình hướng đối tượng trong C++

Chào mừng trở lại với chuỗi bài học C++ của FullhouseDev!

Sau khi đã cùng nhau tìm hiểu về các khái niệm cốt lõi trong Lập trình Hướng đối tượng (OOP) với C++, từ lớp (class), đối tượng (object), đóng gói (encapsulation), kế thừa (inheritance) đến đa hình (polymorphism)... giờ là lúc chúng ta xắn tay áo lên và thực hành! Lý thuyết rất quan trọng, nhưng chỉ thông qua việc viết codegiải quyết vấn đề thực tế, bạn mới có thể thực sự nắm vững và áp dụng OOP một cách hiệu quả.

Bài viết này, Bài 35.5, sẽ là nơi chúng ta cùng nhau thực hiện các bài tập thực hành để củng cố kiến thức về OOP trong C++. Không có cách nào tốt hơn để học ngoài việc làm!

Hãy cùng bắt đầu ngay với các bài tập nhé!


Bài tập 1: Xây dựng lớp SinhVien đơn giản

Yêu cầu:

Tạo một lớp có tên là SinhVien. Lớp này sẽ có các thuộc tính (dữ liệu thành viên) riêng tư (private) bao gồm:

  • maSV (chuỗi - string)
  • tenSV (chuỗi - string)
  • diemTrungBinh (số thực - double)

Lớp cần có các phương thức công khai (public) để:

  • Thiết lập giá trị cho các thuộc tính (setters).
  • Lấy giá trị của các thuộc tính (getters).
  • Hiển thị thông tin đầy đủ của sinh viên.

Giải thích:

Bài tập này giúp bạn ôn lại cách khai báo lớp, định nghĩa các thành viên dữ liệu (data members)thành viên hàm (member functions), cũng như áp dụng nguyên tắc đóng gói (encapsulation) bằng cách giữ dữ liệu ở chế độ private và cung cấp các hàm public để tương tác với dữ liệu đó.

Code minh họa:

#include <iostream>
#include <string>
#include <iomanip> // Để định dạng output số thực

class SinhVien {
private:
    string maSV;
    string tenSV;
    double diemTrungBinh;

public:
    // Hàm tạo (Constructor) - Tùy chọn nhưng rất tiện lợi
    // Khởi tạo đối tượng ngay khi được tạo
    SinhVien(string ma, string ten, double diem) : maSV(ma), tenSV(ten), diemTrungBinh(diem) {
        cout << "Da tao sinh vien: " << tenSV << endl;
    }

    // Hàm hủy (Destructor) - Tùy chọn
    // Được gọi khi đối tượng bị hủy
    ~SinhVien() {
        // cout << "Da huy sinh vien: " << tenSV << endl;
        // Trong ví dụ đơn giản này, destructor không cần làm gì đặc biệt
    }

    // Setters (Thiết lập giá trị)
    void setMaSV(const string& ma) {
        maSV = ma;
    }

    void setTenSV(const string& ten) {
        tenSV = ten;
    }

    void setDiemTrungBinh(double diem) {
        if (diem >= 0 && diem <= 10) { // Thêm kiểm tra ràng buộc đơn giản
            diemTrungBinh = diem;
        } else {
            cerr << "Loi: Diem khong hop le!" << endl;
        }
    }

    // Getters (Lấy giá trị)
    string getMaSV() const { // 'const' nghĩa là hàm này không thay đổi trạng thái của đối tượng
        return maSV;
    }

    string getTenSV() const {
        return tenSV;
    }

    double getDiemTrungBinh() const {
        return diemTrungBinh;
    }

    // Phương thức hiển thị thông tin
    void hienThongTin() const {
        cout << "--- Thong tin Sinh vien ---" << endl;
        cout << "Ma SV: " << maSV << endl;
        cout << "Ten SV: " << tenSV << endl;
        // Sử dụng fixed và setprecision để hiển thị 2 chữ số thập phân
        cout << "Diem TB: " << fixed << setprecision(2) << diemTrungBinh << endl;
        cout << "--------------------------" << endl;
    }
}; // Kết thúc khai báo lớp bằng dấu chấm phẩy

int main() {
    // Tạo một đối tượng SinhVien bằng constructor
    SinhVien sv1("SV001", "Nguyen Van A", 8.75);

    // Hiển thị thông tin ban đầu
    sv1.hienThongTin();

    // Sử dụng setters để cập nhật thông tin
    sv1.setDiemTrungBinh(9.0);

    // Sử dụng getters và phương thức hiển thị để kiểm tra
    cout << "\nCap nhat diem cho " << sv1.getTenSV() << endl;
    sv1.hienThongTin();

    // Tạo thêm một đối tượng khác
    SinhVien sv2("SV002", "Tran Thi B", 7.5);
    sv2.hienThongTin();

    return 0;
} // Khi ra khỏi phạm vi này, sv1 và sv2 sẽ bị hủy, destructor (nếu có nội dung) sẽ được gọi.

Giải thích code:

  • Chúng ta khai báo lớp SinhVien với các thành viên dữ liệu private (maSV, tenSV, diemTrungBinh) để đóng gói dữ liệu, ngăn chặn truy cập trực tiếp từ bên ngoài lớp.
  • Các hàm public như setMaSV, getTenSV, hienThongTin là giao diện để thế giới bên ngoài tương tác với đối tượng SinhVien. Đây chính là nguyên tắc đóng gói trong OOP.
  • Hàm tạo SinhVien(string, string, double) giúp khởi tạo các thuộc tính của đối tượng ngay khi nó được tạo ra, làm cho code ngắn gọn và an toàn hơn.
  • Hàm hủy ~SinhVien() được gọi tự động khi đối tượng bị phá hủy (ví dụ: khi nó ra khỏi phạm vi), hữu ích cho việc giải phóng tài nguyên.
  • Trong main(), chúng ta tạo ra các đối tượng (instance) của lớp SinhVien (sv1, sv2) và gọi các phương thức (methods) của chúng (hienThongTin, setDiemTrungBinh).

Bài tập 2: Tương tác giữa các đối tượng - Lớp SachThuVien

Yêu cầu:

  1. Tạo một lớp Sach đơn giản với các thuộc tính như tieuDe (string) và tacGia (string), và một phương thức hiển thị thông tin sách.
  2. Tạo một lớp ThuVien. Lớp này sẽ chứa một tập hợp các đối tượng Sach. Sử dụng vector để lưu trữ danh sách sách.
  3. Lớp ThuVien cần có các phương thức để:
    • Thêm một cuốn sách vào thư viện.
    • Hiển thị thông tin của tất cả các cuốn sách trong thư viện.

Giải thích:

Bài tập này minh họa mối quan hệ thành phần (composition) trong OOP, nơi một đối tượng của lớp này (ThuVien) có chứa (has-a) các đối tượng của lớp khác (Sach). Bạn sẽ thấy cách các đối tượng tương tác với nhau thông qua việc gọi các phương thức của nhau.

Code minh họa:

#include <iostream>
#include <string>
#include <vector> // Để sử dụng vector

// Lớp Sach đơn giản
class Sach {
private:
    string tieuDe;
    string tacGia;

public:
    // Hàm tạo
    Sach(string td, string tg) : tieuDe(td), tacGia(tg) {}

    // Getters (cần thiết để lớp ThuVien lấy thông tin hiển thị)
    string getTieuDe() const { return tieuDe; }
    string getTacGia() const { return tacGia; }

    // Phương thức hiển thị thông tin Sach
    void hienThongTin() const {
        cout << "- \"" << tieuDe << "\" by " << tacGia << endl;
    }
}; // Kết thúc khai báo lớp Sach

// Lớp ThuVien chứa các đối tượng Sach
class ThuVien {
private:
    // Sử dụng vector để lưu trữ nhiều đối tượng Sach
    vector<Sach> danhSachSach; // Đây là mối quan hệ composition (ThuVien "có" các đối tượng Sach)

public:
    // Phương thức thêm sách mới vào thư viện
    void themSach(const Sach& sachMoi) { // Truyền đối tượng Sach theo tham chiếu const để hiệu quả và không làm thay đổi đối tượng gốc
        danhSachSach.push_back(sachMoi); // Thêm sách vào vector
        cout << "Da them sach: \"" << sachMoi.getTieuDe() << "\"" << endl;
    }

    // Phương thức hiển thị tất cả sách trong thư viện
    void hienThiTatCaSach() const {
        cout << "\n--- Cac sach trong thu vien ---" << endl;
        if (danhSachSach.empty()) {
            cout << "Thu vien hien tai rong." << endl;
        } else {
            // Duyệt qua từng đối tượng Sach trong vector và gọi phương thức hienThongTin() của từng đối tượng đó
            for (const auto& sach : danhSachSach) {
                sach.hienThongTin();
            }
        }
        cout << "------------------------------" << endl;
    }
}; // Kết thúc khai báo lớp ThuVien

int main() {
    // Tạo các đối tượng Sach
    Sach sach1("Nha gia kim", "Paulo Coelho");
    Sach sach2("De men phieu luu ky", "To Hoai");
    Sach sach3("Hoang tu be", "Antoine de Saint-Exupery");

    // Tạo một đối tượng ThuVien
    ThuVien myThuVien;

    // Thêm các đối tượng Sach vào đối tượng ThuVien
    myThuVien.themSach(sach1);
    myThuVien.themSach(sach2);
    myThuVien.themSach(sach3);

    // Hiển thị danh sách sách trong thư viện
    myThuVien.hienThiTatCaSach();

    return 0;
}

Giải thích code:

  • Lớp Sach là một lớp đơn giản như bài tập 1.
  • Lớp ThuVien có một thành viên dữ liệu là vector<Sach> danhSachSach. Đây là cách thể hiện mối quan hệ thành phần (composition): một ThuVien được tạo thành từ hoặc chứa nhiều Sach.
  • Phương thức themSach của ThuVien nhận một đối tượng Sach làm tham số và thêm nó vào vector danhSachSach sử dụng push_back().
  • Phương thức hienThiTatCaSach lặp qua vector danhSachSach. Với mỗi đối tượng Sach trong vector, nó gọi phương thức hienThongTin() của chính đối tượng Sach đó để hiển thị thông tin. Điều này cho thấy sự tương tác giữa các đối tượng thuộc các lớp khác nhau.

Bài tập 3: Áp dụng Kế thừa - Hệ thống Hình học cơ bản

Yêu cầu:

  1. Thiết kế một lớp cơ sở (base class) có tên là Hinh. Lớp này có thể có các thuộc tính chung như mauSac (string) và một phương thức hiển thị màu sắc.
  2. Thiết kế hai lớp kế thừa (derived classes) là HinhTronHinhChuNhat, kế thừa từ lớp Hinh.
  3. Lớp HinhTron cần có thêm thuộc tính banKinh (double) và phương thức tính diện tích riêng.
  4. Lớp HinhChuNhat cần có thêm thuộc tính chieuDaichieuRong (double) và phương thức tính diện tích riêng.
  5. Minh họa việc tạo đối tượng của các lớp kế thừa và truy cập các thuộc tính, phương thức từ cả lớp cơ sở và lớp kế thừa.

Giải thích:

Bài tập này giúp bạn hiểu và áp dụng nguyên tắc kế thừa (inheritance). Các lớp con (HinhTron, HinhChuNhat) kế thừa các đặc điểm chung (mauSac, hienThiMau) từ lớp cha (Hinh), đồng thời bổ sung thêm các đặc điểm riêng của chúng (thuộc tính kích thước, phương thức tính diện tích). Kế thừa giúp tái sử dụng code và tạo ra cấu trúc phân cấp trong chương trình.

Code minh họa:

#include <iostream>
#include <string>
#include <cmath> // Để sử dụng M_PI cho hình tròn
#include <iomanip> // Để định dạng output

// Định nghĩa M_PI nếu chưa có (một số trình biên dịch cần define này)
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif

// Lớp cơ sở (Base Class)
class Hinh {
protected: // Sử dụng protected để lớp con có thể truy cập thuộc tính này
    string mauSac;

public:
    // Hàm tạo lớp cơ sở
    Hinh(string mau) : mauSac(mau) {
        cout << "Constructor Hinh: Tao hinh mau " << mauSac << endl;
    }

    // Phương thức chung của lớp cơ sở
    void hienThiMau() const {
        cout << "Hinh nay co mau: " << mauSac << endl;
    }

    // Hàm hủy (nên để virtual nếu có kế thừa)
    virtual ~Hinh() {
        cout << "Destructor Hinh" << endl;
    }
};

// Lớp kế thừa (Derived Class) HinhTron
// Kế thừa public từ lớp Hinh
class HinhTron : public Hinh {
private:
    double banKinh;

public:
    // Hàm tạo lớp HinhTron
    // Phải gọi hàm tạo của lớp cơ sở (Hinh(mau))
    HinhTron(string mau, double r) : Hinh(mau), banKinh(r) {
         cout << "Constructor HinhTron: Ban kinh " << banKinh << endl;
    }

    // Phương thức riêng của HinhTron
    double tinhDienTich() const {
        return M_PI * banKinh * banKinh;
    }

     // Override hàm hủy nếu cần thiết (không bắt buộc trong ví dụ này nhưng là practice tốt)
    ~HinhTron() override {
        cout << "Destructor HinhTron" << endl;
    }
};

// Lớp kế thừa (Derived Class) HinhChuNhat
// Kế thừa public từ lớp Hinh
class HinhChuNhat : public Hinh {
private:
    double chieuDai;
    double chieuRong;

public:
    // Hàm tạo lớp HinhChuNhat
    // Phải gọi hàm tạo của lớp cơ sở (Hinh(mau))
    HinhChuNhat(string mau, double cd, double cr) : Hinh(mau), chieuDai(cd), chieuRong(cr) {
        cout << "Constructor HinhChuNhat: Dai " << chieuDai << ", Rong " << chieuRong << endl;
    }

    // Phương thức riêng của HinhChuNhat
    double tinhDienTich() const {
        return chieuDai * chieuRong;
    }

    ~HinhChuNhat() override {
        cout << "Destructor HinhChuNhat" << endl;
    }
};

int main() {
    cout << fixed << setprecision(2); // Định dạng output số thực

    // Tạo đối tượng của lớp HinhTron
    HinhTron hinhTr("Do", 5.0);

    // Tạo đối tượng của lớp HinhChuNhat
    HinhChuNhat hinhCN("Xanh Duong", 4.0, 6.0);

    cout << endl; // Xuống dòng cho dễ nhìn

    // Gọi phương thức kế thừa từ lớp cơ sở
    hinhTr.hienThiMau();
    hinhCN.hienThiMau();

    cout << endl; // Xuống dòng

    // Gọi phương thức riêng của từng lớp kế thừa
    cout << "Dien tich Hinh Tron (" << hinhTr.getMauSac() << "): " << hinhTr.tinhDienTich() << endl;
    cout << "Dien tich Hinh Chu Nhat (" << hinhCN.getMauSac() << "): " << hinhCN.tinhDienTich() << endl;

    cout << endl; // Xuống dòng

    cout << "Ket thuc main..." << endl;
    // Các đối tượng hinhTr và hinhCN sẽ bị hủy khi ra khỏi scope
    return 0;
} // Destructor sẽ được gọi tự động theo thứ tự ngược lại của constructor (derived trước, base sau)

Giải thích code:

  • Lớp Hinh là lớp cơ sở với thuộc tính protected mauSac. protected cho phép các lớp kế thừa truy cập trực tiếp thuộc tính này, khác với private.
  • Lớp HinhTronHinhChuNhat kế thừa từ Hinh bằng cú pháp : public Hinh. public inheritance nghĩa là các thành viên public của Hinh sẽ trở thành public của lớp con, protected sẽ vẫn là protected.
  • Trong hàm tạo của lớp con (HinhTron, HinhChuNhat), chúng ta sử dụng cú pháp : Hinh(mau) để gọi hàm tạo của lớp cơ sở. Việc này đảm bảo phần thuộc tính của lớp cha trong đối tượng lớp con được khởi tạo đúng cách.
  • Các lớp con bổ sung các thuộc tính và phương thức riêng của chúng (banKinh, chieuDai, chieuRong, tinhDienTich).
  • Trong main(), chúng ta tạo đối tượng của các lớp con. Ta có thể gọi các phương thức của lớp cơ sở (hienThiMau()) trên đối tượng của lớp con, chứng tỏ chúng đã kế thừa phương thức đó. Đồng thời, ta gọi các phương thức riêng của từng lớp con (tinhDienTich()).
  • Việc sử dụng virtual cho destructor của lớp cơ sở là practice tốt trong C++ khi làm việc với kế thừa, để đảm bảo destructor của lớp con được gọi đúng khi làm việc với con trỏ lớp cha.

Bài tập ví dụ: C++ Bài 24.B2: Quản lý doanh thu bán hàng (OOP)

Quản lý doanh thu bán hàng (OOP)

Đề bài

Công ty FullHouse Dev muốn quản lý doanh thu bán hàng của các nhân viên. Quy tắc đánh giá như sau:

  • Doanh thu = Số lượng sản phẩm bán × Giá mỗi sản phẩm

Hãy nhập thông tin doanh thu của các nhân viên và tìm ra nhân viên có doanh thu cao nhất.

Input Format

  • Dòng đầu ghi số nhân viên (không quá 100 nhân viên)
  • Mỗi nhân viên ghi trên 4 dòng:
    • Tên nhân viên
    • Số lượng sản phẩm bán
    • Giá mỗi sản phẩm
    • Mã nhân viên

Output Format

Ghi ra thông tin của nhân viên có doanh thu cao nhất gồm các thông tin:

  • Mã nhân viên
  • Tên nhân viên
  • Số lượng sản phẩm bán
  • Giá mỗi sản phẩm
  • Doanh thu

Ví dụ

Dữ liệu vào:
3
Nguyen Van A
50
200000
NV001
Tran Thi B
70
150000
NV002
Le Van C
30
100000
NV003
Dữ liệu ra:
NV002 Tran Thi B 70 150000 10500000
Giải thích ví dụ mẫu:
  • Đầu vào: Nguyễn Văn A 50 200000 NV001
  • Đầu ra: NV002 Trần Thị B 70 150000 10500000

Dòng này hiển thị thông tin chi tiết về nhân viên NV002, bao gồm mã nhân viên, tên, số lượng sản phẩm bán, giá mỗi sản phẩm và doanh thu. Đây là nhân viên có doanh thu cao nhất.

Cách tính doanh thu cho Trần Thị B: Doanh thu = 70 × 150000 = 10500000 Tuyệt vời! Đây là hướng dẫn chi tiết để giải bài tập này bằng C++ sử dụng lập trình hướng đối tượng (OOP), tập trung vào các bước và ý tưởng cốt lõi mà không cung cấp code hoàn chỉnh:

Bước 1: Xác định đối tượng (Object)

Bài toán quản lý doanh thu cho các nhân viên. Rõ ràng, đối tượng trung tâm ở đây chính là "Nhân viên". Chúng ta cần tạo một lớp (class) để biểu diễn một nhân viên bán hàng.

Bước 2: Thiết kế lớp (Class) Employee

Lớp Employee sẽ chứa các thuộc tính (attributes) cần thiết để lưu trữ thông tin của một nhân viên và các phương thức (methods) để thực hiện các hành động liên quan.

  • Thuộc tính (Private Members): Để đảm bảo tính đóng gói (encapsulation), các dữ liệu của nhân viên nên được khai báo là private.

    • Tên nhân viên: Kiểu dữ liệu string.
    • Số lượng sản phẩm bán: Kiểu dữ liệu số nguyên (ví dụ int).
    • Giá mỗi sản phẩm: Kiểu dữ liệu số nguyên (ví dụ int).
    • Mã nhân viên: Kiểu dữ liệu string.
  • Phương thức (Public Members): Cần có các phương thức để tương tác với đối tượng Employee.

    • Constructor: Một phương thức khởi tạo để tạo đối tượng Employee từ các thông tin đầu vào. Có thể tạo một constructor nhận tất cả các thông tin (tên, số lượng, giá, mã) làm tham số.
    • Phương thức tính doanh thu: Một phương thức để tính toán doanh thu dựa trên số lượng và giá. Phương thức này nên trả về kết quả. Kiểu dữ liệu trả về cho doanh thu nên là long long để tránh tràn số, vì số lượng và giá có thể lớn khiến tích vượt quá giới hạn của int. Phương thức này không làm thay đổi trạng thái của đối tượng, nên đánh dấu là const.
    • Phương thức lấy thông tin (Getters): Cần các phương thức public để truy cập các thuộc tính private khi cần in kết quả (ví dụ: getId(), getName(), getQuantity(), getPrice(), getRevenue()). Phương thức getRevenue() có thể gọi lại phương thức tính doanh thu bên trong. Đánh dấu các getter là const.

Bước 3: Đọc dữ liệu và lưu trữ

Trong hàm main(), bạn sẽ thực hiện việc đọc dữ liệu và tạo các đối tượng Employee.

  • Đọc số lượng nhân viên n.
  • Sử dụng một container để lưu trữ các đối tượng Employee. vector<Employee> là lựa chọn phù hợp và linh hoạt.
  • Lặp lại n lần để đọc thông tin cho từng nhân viên:
    • Đọc tên (getline vì tên có thể chứa khoảng trắng).
    • Đọc số lượng (cin >>).
    • Đọc giá (cin >>).
    • Quan trọng: Sau khi đọc số nguyên bằng cin >>, còn lại ký tự newline trong bộ đệm. Cần xử lý nó trước khi gọi getline tiếp theo (ví dụ: cin.ignore(numeric_limits<streamsize>::max(), '\n'); hoặc đơn giản hơn là cin.ignore(); nếu chỉ cần bỏ qua 1 ký tự newline).
    • Đọc mã nhân viên (getline).
    • Tạo một đối tượng Employee mới sử dụng constructor với dữ liệu vừa đọc.
    • Thêm đối tượng này vào vector.

Bước 4: Tìm nhân viên có doanh thu cao nhất

Sau khi đã có một vector chứa tất cả các đối tượng Employee:

  • Khởi tạo biến lưu trữ doanh thu cao nhất tìm được (max_revenue, kiểu long long) và chỉ số (hoặc con trỏ/iterator) của nhân viên tương ứng (max_index).
  • Lặp qua vector:
    • Đối với mỗi nhân viên trong vector, gọi phương thức tính doanh thu của họ.
    • So sánh doanh thu hiện tại với max_revenue.
    • Nếu doanh thu hiện tại lớn hơn max_revenue, cập nhật max_revenue và lưu lại chỉ số (hoặc thông tin khác) của nhân viên này. Có thể bắt đầu với nhân viên đầu tiên làm giả định ban đầu cho max_revenuemax_index.

Bước 5: In kết quả

Sau khi vòng lặp kết thúc, bạn đã xác định được nhân viên có doanh thu cao nhất (thông qua max_index).

  • Truy cập đối tượng Employee tại max_index trong vector.
  • Sử dụng các phương thức getter của đối tượng này để lấy ra Mã nhân viên, Tên nhân viên, Số lượng sản phẩm bán, Giá mỗi sản phẩm và Doanh thu.
  • In các thông tin này ra màn hình theo đúng định dạng yêu cầu, cách nhau bởi dấu cách.

Gợi ý về việc sử dụng std:

  • Sử dụng cin, cout cho nhập/xuất cơ bản.
  • Sử dụng string cho tên và mã.
  • Sử dụng vector để lưu trữ danh sách nhân viên.
  • Sử dụng getline để đọc chuỗi có khoảng trắng.
  • Sử dụng cin.ignore() để xử lý ký tự newline sau khi đọc số.
  • Sử dụng long long cho kiểu dữ liệu doanh thu.
  • Sử dụng const cho các phương thức getter và phương thức tính doanh thu vì chúng không làm thay đổi trạng thái của đối tượng.

Làm thêm nhiều bài tập miễn phí tại đây

Comments

There are no comments at the moment.