[C++ OOP Thực Chiến] Bài 34: Toán tử so sánh - Cán cân công lý của OOP
Chào anh em! Đến thời điểm này, Class PhanSo của chúng ta đã có thể tính toán (+, -, *, /) và in ấn (<<, >>) y hệt một biến int nguyên thủy.Nhưng nếu bạn ném một mảng các PhanSo vào hàm std::sort() của C++, compiler sẽ khóc thét. Tại sao? Vì nó không biết và thằng nào lớn hơn để mà xếp hạng!Đã đến lúc chúng ta nạp chồng các toán tử quan hệ: ==, !=, <, >, <=, >=.
1. Logic So Sánh: Lợi thế tuyệt đối của việc "Rút gọn ngầm"
Trong toán học, để so sánh 2 phân số, chúng ta thường phải quy đồng mẫu số hoặc nhân chéo:$\frac{a}{b} = \frac{c}{d} \iff a \times d = b \times c$Tuy nhiên, hãy nhớ lại thiết kế xuất sắc của chúng ta từ [Bài 20]. Hàm Constructor luôn tự động gọi rutGon() ngay khi Object ra đời. Mọi mẫu số âm đều bị đẩy lên tử số, và phân số luôn ở dạng tối giản.Điều này mang lại một lợi thế cực lớn về mặt hiệu năng (Performance) khi so sánh bằng (==):Thay vì phải nhân chéo tốn CPU, và trong RAM của chúng ta thực chất ĐỀU LÀ . Chúng ta chỉ cần so sánh trực tiếp: tuSo == tuSo và mauSo == mauSo là xong!Còn với phép lớn hơn/nhỏ hơn, ta áp dụng nhân chéo một cách an toàn (vì mẫu số luôn dương):
2. Lựa chọn vũ khí: Lại là Hàm bạn (Friend Function)
Giống hệt như phép cộng ở [Bài 29], toán tử so sánh là toán tử 2 ngôi. Nếu chúng ta dùng Member Function, biểu thức 5 == ps1 sẽ bị lỗi compiler ngay lập tức vì số 5 không thể làm chủ nhà.
Do đó, Hàm bạn (Friend) với 2 tham số ngang hàng tiếp tục là chân ái để bảo toàn tính giao hoán. Đặc biệt, kiểu trả về của các hàm này bắt buộc phải là bool (Đúng/Sai).
3. Code Demo: Định nghĩa Cán cân công lý
Hãy cùng xem cách triển khai cực kỳ gọn gàng và chuẩn xác:
#include <iostream>
#include <cmath>
using namespace std;
class PhanSo {
private:
int tuSo;
int mauSo;
void rutGon() {
if (mauSo == 0) mauSo = 1;
if (mauSo < 0) { tuSo = -tuSo; mauSo = -mauSo; }
int a = abs(tuSo), b = abs(mauSo);
while (b != 0) { int temp = b; b = a % b; a = temp; }
int ucln = (a == 0) ? 1 : a;
tuSo /= ucln; mauSo /= ucln;
}
public:
PhanSo(int tu = 0, int mau = 1) : tuSo(tu), mauSo(mau) { rutGon(); }
friend ostream& operator<<(ostream& os, const PhanSo& ps) {
if (ps.mauSo == 1) os << ps.tuSo;
else if (ps.tuSo == 0) os << "0";
else os << ps.tuSo << "/" << ps.mauSo;
return os;
}
// --- KHAI BÁO CÁC TOÁN TỬ SO SÁNH (HÀM BẠN) ---
friend bool operator==(const PhanSo& a, const PhanSo& b);
friend bool operator<(const PhanSo& a, const PhanSo& b);
friend bool operator>(const PhanSo& a, const PhanSo& b);
};
// --- ĐỊNH NGHĨA HÀM TỰ DO ---
// Toán tử Bằng
bool operator==(const PhanSo& a, const PhanSo& b) {
// Nhờ đã rút gọn ngầm, chỉ cần so sánh trực tiếp!
return (a.tuSo == b.tuSo) && (a.mauSo == b.mauSo);
}
// Toán tử Nhỏ hơn (Nhân chéo)
bool operator<(const PhanSo& a, const PhanSo& b) {
return (a.tuSo * b.mauSo) < (b.tuSo * a.mauSo);
}
// Toán tử Lớn hơn
bool operator>(const PhanSo& a, const PhanSo& b) {
return (a.tuSo * b.mauSo) > (b.tuSo * a.mauSo);
}
// Mẹo của Senior: Đã có == và <, ta có thể suy ra các phép còn lại
// mà không cần chọc vào private nữa (Không cần làm friend)
bool operator!=(const PhanSo& a, const PhanSo& b) { return !(a == b); }
bool operator<=(const PhanSo& a, const PhanSo& b) { return (a < b) || (a == b); }
bool operator>=(const PhanSo& a, const PhanSo& b) { return !(a < b); }
// ------------------------------------------------
int main() {
cout << "--- HE THONG SO SANH PHAN SO ---\n";
PhanSo ps1(2, 4); // Rút gọn thành 1/2
PhanSo ps2(1, 2); // 1/2
PhanSo ps3(3, 4); // 3/4
cout << "ps1: " << ps1 << " | ps2: " << ps2 << " | ps3: " << ps3 << "\n\n";
// 1. So sánh 2 Object
if (ps1 == ps2) cout << "[X] ps1 bang ps2!\n";
if (ps1 < ps3) cout << "[X] ps1 nho hon ps3!\n";
// 2. So sánh Object với số nguyên (Tính giao hoán nhờ Friend)
if (ps1 < 1) cout << "[X] ps1 nho hon 1!\n";
// Số nguyên nằm bên trái vẫn chạy mượt mà!
if (1 > ps1) cout << "[X] 1 lon hon ps1!\n";
return 0;
}
Nhận xét:
Kỹ thuật "tái sử dụng" logic ở nhóm toán tử !=, <=, >= là một thói quen cực tốt của các Kỹ sư dạn dày kinh nghiệm. Nó giúp bạn giảm thiểu code lặp lại (DRY - Don't Repeat Yourself) và hạn chế bug nếu sau này logic so sánh lõi bị thay đổi.
Tạm kết & Gợi mở
Chúc mừng anh em! Với mảnh ghép So sánh này, Class PhanSo của chúng ta đã chính thức hoàn thiện 100%. Nó mạnh mẽ, an toàn, thông minh và hòa nhập hoàn toàn vào hệ sinh thái chuẩn của C++.
Hành trình từ Bài 1 đến Bài 34, chúng ta đã xoay sở, nhào nặn và tối ưu hóa MỘT CLASS DUY NHẤT.
Nhưng trong một hệ thống phần mềm thực tế, bạn hiếm khi làm việc với 1 Class đơn độc. Hãy tưởng tượng bạn đang code một con Game. Bạn có Class QuaiVat (có máu, tốc chạy, hàm di chuyển). Rồi Sếp yêu cầu bạn tạo thêm Class BossLửa, Class BossNước. Chẳng lẽ bạn lại mở file mới ra và copy-paste lại đống code di chuyển, máu me của QuaiVat sang cho bọn Boss?
Việc copy-paste code là tội ác tày trời trong lập trình. Để giải quyết bài toán "Tái sử dụng cấp độ hệ thống", OOP mang đến cho chúng ta một khái niệm vĩ đại: Kế thừa (Inheritance).
Hẹn gặp lại anh em ở Bài 35: Đặc tính cơ bản của kế thừa đơn - Sự truyền ngôi của các Class! Đừng quên lưu lại project Phân số này làm hành trang đi phỏng vấn nhé!
All rights reserved