0

[C++ OOP Thực Chiến] Bài 20: Bài tập thực chiến - Xây dựng hệ thống Phân số (Phần 1)

Chào anh em! Chào mừng đến với project thực chiến đầu tiên.

Mục tiêu của chúng ta là tạo ra một Class PhanSo hoạt động mượt mà y như các kiểu dữ liệu gốc (int, float) của C++. Trong phần 1 này, chúng ta sẽ thiết kế bộ khung, xử lý các hàm khởi tạo (Constructor) và xây dựng logic tự động rút gọn phân số – một tính năng bắt buộc phải có để chống tràn bộ nhớ số học.

1. Thiết kế "Bản vẽ" (Class Architecture)

Một phân số luôn cấu tạo bởi 2 thành phần cốt lõi: Tử số và Mẫu số. Dưới góc độ Đóng gói (Encapsulation), 2 thành phần này chứa đầy rủi ro: Nếu ai đó ở ngoài gán Mẫu số = 0, chương trình của chúng ta sẽ dính lỗi Divide by Zero và sập server ngay lập tức.

Vì vậy, mọi dữ liệu phải là private.

class PhanSo {
private:
    int tuSo;
    int mauSo;

    // Các hàm tiện ích dùng nội bộ (Người ngoài không cần biết)
    int timUCLN(int a, int b); // Tìm Ước chung lớn nhất
    void rutGon();             // Logic tự động rút gọn

public:
    // Các Constructor
    PhanSo();
    PhanSo(int tu, int mau);

    // Giao diện (Interface)
    void inPhanSo() const;
};

2. Logic nghiệp vụ (Business Logic): Tự động rút gọn

Thay vì bắt người dùng phải tự gọi hàm rút gọn mỗi lần tạo phân số, chúng ta sẽ áp dụng tư duy tự động hóa: Object khi sinh ra phải tự biết tối ưu hóa chính nó.

Chúng ta sẽ cài cắm hàm rutGon() chạy ngầm ngay bên trong Constructor. Để rút gọn, ta cần tìm Ước chung lớn nhất (UCLN) bằng thuật toán Euclid cực kỳ nhanh chóng.

#include <iostream>
#include <cmath>

using namespace std;

class PhanSo {
private:
    int tuSo;
    int mauSo;

    // Helper method: Tìm Ước chung lớn nhất (UCLN)
    int timUCLN(int a, int b) {
        a = abs(a); // Trị tuyệt đối để xử lý số âm
        b = abs(b);
        while (b != 0) {
            int temp = b;
            b = a % b;
            a = temp;
        }
        return a;
    }

    // Helper method: Tự động rút gọn và chuẩn hóa dấu
    void rutGon() {
        if (mauSo == 0) {
            cout << "[FATAL ERROR] Mau so khong the bang 0!\n";
            exit(1); // Ép dừng chương trình nếu dữ liệu nát
        }

        // Chuẩn hóa: Nếu mẫu âm thì chuyển dấu âm lên tử số (-1 / -2 -> 1 / 2)
        if (mauSo < 0) {
            tuSo = -tuSo;
            mauSo = -mauSo;
        }

        int ucln = timUCLN(tuSo, mauSo);
        tuSo /= ucln;
        mauSo /= ucln;
    }

public:
    // 1. DEFAULT CONSTRUCTOR
    PhanSo() : tuSo(0), mauSo(1) {}

    // 2. PARAMETERIZED CONSTRUCTOR
    PhanSo(int tu, int mau) : tuSo(tu), mauSo(mau) {
        rutGon(); // Chạy ngầm logic tối ưu ngay khi Object ra đời!
    }

    // Phương thức const: Chỉ in, không sửa đổi dữ liệu
    void inPhanSo() const {
        if (mauSo == 1) {
            cout << tuSo << "\n"; // In số nguyên nếu mẫu là 1
        } else if (tuSo == 0) {
            cout << "0\n";
        } else {
            cout << tuSo << "/" << mauSo << "\n";
        }
    }
};

3. Lắp ráp và Test hệ thống (main)

Bây giờ, hãy thử tạo ra các Object Phân số với những dữ liệu cực kỳ "xấu xí" để xem bộ khiên bảo vệ (Constructor + Hàm rút gọn ngầm) của chúng ta hoạt động tốt thế nào:

int main() {
    cout << "--- KHOI TAO PHAN SO ---\n";

    // 1. Tạo phân số bình thường
    PhanSo ps1(2, 4);
    cout << "ps1 (2/4) = "; 
    ps1.inPhanSo(); // Kỳ vọng: 1/2

    // 2. Tạo phân số với mẫu âm
    PhanSo ps2(5, -15);
    cout << "ps2 (5/-15) = "; 
    ps2.inPhanSo(); // Kỳ vọng: -1/3 (Dấu âm tự chuyển lên trên)

    // 3. Tạo phân số là số nguyên
    PhanSo ps3(100, 25);
    cout << "ps3 (100/25) = "; 
    ps3.inPhanSo(); // Kỳ vọng: 4

    // 4. Default Constructor
    PhanSo ps4;
    cout << "ps4 mac dinh = "; 
    ps4.inPhanSo(); // Kỳ vọng: 0

    return 0;
}

Nhận xét: Tuyệt vời! Object của chúng ta thông minh đến mức dù bạn cố tình ném vào rác (mẫu âm, phân số chưa tối giản), nó vẫn tự động dọn dẹp và đưa về trạng thái sạch sẽ nhất ngay từ lúc khởi tạo. Đây chính là tiêu chuẩn code của một Senior.

Tạm kết & Gợi mở

Chúng ta đã dựng xong bộ khung dữ liệu vô cùng vững chắc cho Class PhanSo.

Tuy nhiên, trong quá trình khởi tạo, chúng ta còn có thể gặp phải những trường hợp phức tạp hơn. Làm sao để khởi tạo linh hoạt khi dữ liệu đầu vào không chỉ là hai số nguyên rời rạc, mà có thể là một chuỗi dạng String (ví dụ: "3/4") truyền từ API xuống?

Để giải quyết bài toán này và hoàn thiện toàn diện kỹ năng, hẹn gặp lại anh em ở Bài 21: Phương thức khởi tạo có tham số truyền vào 2. Nhớ Upvote và thực hành gõ lại code để tay quen phím nhé!


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí