[C++ OOP Thực Chiến] Bài 27: Toán tử số đối (Non-member function) - Góc nhìn từ bên ngoài pháo đài
Chào anh em! Ở [Bài 25], chúng ta đã nạp chồng toán tử số đối (Unary Minus -) bằng cách viết hàm trực tiếp vào bên trong Class. Lợi thế là chúng ta chọc thẳng được vào biến private (tử số, mẫu số).
Nhưng trong kỹ thuật phần mềm, có một nguyên tắc thiết kế: Class càng ít hàm càng tốt. Những hàm nào không bắt buộc phải can thiệp sâu vào "nội tạng" của Class thì nên đẩy ra ngoài thành hàm tự do (Non-member function) để Class nhẹ gánh.
Hôm nay, với sự trợ giúp của bộ đôi Getter/Setter học ở [Bài 26], chúng ta sẽ đẩy logic đảo dấu phân số ra thế giới bên ngoài.
1. Sự khác biệt cốt lõi: Con trỏ this biến mất!
Khi bạn viết hàm operator- ở BÊN TRONG Class (Member Function):
- Hàm không có tham số:
PhanSo operator-() const; - Lý do: Nó tự lấy dữ liệu của chính nó thông qua con trỏ ngầm định
this.
Khi bạn dọn nhà ra BÊN NGOÀI Class (Non-member Function):
- Hàm bắt buộc phải có 1 tham số:
PhanSo operator-(const PhanSo& ps); - Lý do: Đứng ở ngoài đường, C++ không biết bạn đang muốn đảo dấu phân số nào, nên bạn phải chỉ đích danh (truyền Object vào) cho nó.
2. Xuyên thủng pháo đài bằng Getter
Vì hàm của chúng ta bây giờ là "người dưng nước lã" (hàm tự do toàn cục), nó sẽ bị luật private của Class PhanSo chặn đứng nếu cố tình gọi ps.tuSo.
Giải pháp? Chúng ta có 2 cách:
- Dùng
friend(Đã học ở Bài 22). Nhưng dùngfriendnhiều quá sẽ phá vỡ tính Đóng gói. - Dùng Getter (Đã học ở Bài 26) - Đây là cách chuẩn "thanh niên nghiêm túc", đi vào nhà bằng cửa chính có lính gác.
3. Code Demo: Đảo dấu phân số từ bên ngoài
Hãy cùng xem kiến trúc code khi tách biệt hoàn toàn Data (Class) và Logic tính toán (Global Function):
#include <iostream>
#include <cmath>
using namespace std;
// --- PHÁO ĐÀI DỮ LIỆU ---
class PhanSo {
private:
int tuSo;
int mauSo;
void rutGon() {
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(); }
// Cánh cửa đọc dữ liệu (GETTER)
int getTuSo() const { return tuSo; }
int getMauSo() const { return mauSo; }
void inPhanSo() const {
if (mauSo == 1) cout << tuSo << "\n";
else if (tuSo == 0) cout << "0\n";
else cout << tuSo << "/" << mauSo << "\n";
}
// BÊN TRONG CLASS KHÔNG CÒN HÀM OPERATOR NÀO CẢ!
};
// --- LOGIC TÍNH TOÁN (NẰM HOÀN TOÀN BÊN NGOÀI) ---
// Nạp chồng toán tử 1 ngôi bằng hàm tự do (Non-member)
// Truyền Hằng tham chiếu (const &) để tránh copy vô ích và bảo vệ bản gốc
PhanSo operator-(const PhanSo& ps) {
// Không thể gọi ps.tuSo, bắt buộc phải dùng Getter
int tuMoi = -ps.getTuSo(); // Đảo dấu tử số
int mauGiuaNguyen = ps.getMauSo();
// Trả về Object mới
return PhanSo(tuMoi, mauGiuaNguyen);
}
// ------------------------------------------------
int main() {
cout << "--- DAO CHIEU TU BEN NGOAI PHAO DAI ---\n";
PhanSo ps1(5, 7); // 5/7
cout << "Phan so goc: "; ps1.inPhanSo();
// Gọi phép thuật: C++ sẽ tự động map cú pháp này vào hàm operator- ở trên
PhanSo ps1_dao = -ps1;
cout << "Sau khi dao dau (-ps1): "; ps1_dao.inPhanSo();
return 0;
}
Nhận xét: Mã nguồn bên trong Class PhanSo giờ đây cực kỳ gọn gàng. Nó làm đúng trách nhiệm duy nhất (Single Responsibility) là lưu trữ dữ liệu và bảo vệ tính toàn vẹn (rút gọn). Còn các phép toán được đẩy ra ngoài, tự do phát triển mà không làm phình to Class.
Tạm kết & Gợi mở
Anh em đã hoàn thành xuất sắc việc thao túng toán tử 1 ngôi bằng cả 2 góc nhìn: Từ bên trong (Member) và từ bên ngoài (Non-member). Cả 2 cách đều có cú pháp gọi ở hàm main giống hệt nhau (-ps1), sự khác biệt chỉ nằm ở tư duy tổ chức code của người Kỹ sư.
Đã đến lúc chúng ta đối mặt với "Trùm cuối" của hệ thống toán tử: Toán tử 2 ngôi (Binary Operator).
Làm sao để dạy C++ hiểu được phép tính: ps1 + ps2?
Nó khác gì so với toán tử 1 ngôi? Và khi nạp chồng phép cộng +, chúng ta nên dùng Member Function hay Non-member Function thì hệ thống sẽ tối ưu hơn?
Tất cả bí mật sẽ được giải đáp trong Bài 28: Toán tử + dùng member và non-member function - Trận chiến của những lối thiết kế. Đừng quên Upvote để tiếp lửa cho series nhé!
All rights reserved