[C++ OOP Thực Chiến] Bài 33: Toán tử hậu tố (Postfix) - Tham số "bù nhìn" và cú lừa lịch sử!
Chào anh em! Ở [Bài 32], chúng ta đã viết thành công hàm operator++() cho toán tử Tiền tố (++ps).
Tuy nhiên, luật của C++ không cho phép tồn tại 2 hàm có cùng tên và cùng danh sách tham số. Nếu chúng ta lại viết thêm một hàm operator++() nữa cho Hậu tố (ps++), compiler sẽ gào lên ngay lập tức.
Làm sao để C++ biết được khi nào bạn gọi Tiền tố, khi nào bạn gọi Hậu tố? Hãy cùng xem cách các nhà thiết kế C++ "hack" hệ thống nhé!
1. Cú "hack" mang tên: Tham số bù nhìn (Dummy Parameter)
Để tạo ra sự khác biệt, ngôn ngữ C++ quy định một luật ngầm cực kỳ dị biệt: Toán tử Hậu tố bắt buộc phải nhận vào một tham số kiểu int vô nghĩa.
Cú pháp khai báo:
- Tiền tố:
PhanSo& operator++() - Hậu tố:
PhanSo operator++(int)
Cái tham số int kia sinh ra KHÔNG ĐỂ LÀM GÌ CẢ. Bạn không cần truyền giá trị cho nó, cũng không cần đặt tên biến cho nó trong hàm. Nó chỉ đứng đó làm "bù nhìn" để compiler phân biệt được: "À, có chữ int ở đây thì đích thị là đang gọi Hậu tố ps++!".
2. Luật chơi của Hậu tố: "Tính trước, Tăng sau"
Khi bạn viết int a = ps++;, C++ sẽ thực hiện 3 bước:
- Nhớ lại giá trị hiện tại của
ps. - Tăng
pslên 1. - Lấy cái giá trị CŨ đã nhớ ở bước 1 gán cho
a.
Để làm được điều này trong code, hàm Hậu tố của chúng ta phải:
- Tạo ra một bản sao (clone) của Object hiện tại để lưu trạng thái cũ.
- Tăng Object gốc lên 1 (chúng ta có thể tận dụng luôn hàm Tiền tố đã viết ở bài trước cho nhàn).
- Trả về bản sao (Object cũ). Vì trả về một Object cục bộ, nên kiểu trả về BẮT BUỘC KHÔNG ĐƯỢC CÓ DẤU
&(Tham chiếu), nếu không sẽ bị lỗi rác bộ nhớ (Dangling Reference).
3. Code Demo: Hoàn thiện bộ đôi Tiền/Hậu tố
#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;
}
// 1. TIỀN TỐ (++ps): Tăng xong trả về bản gốc
PhanSo& operator++() {
this->tuSo = this->tuSo + this->mauSo;
this->rutGon();
return *this;
}
// 2. HẬU TỐ (ps++): Có chữ 'int' bù nhìn. Trả về bản sao!
PhanSo operator++(int) {
// Bước 1: Tạo bản sao lưu giữ trạng thái CŨ
PhanSo banSao = *this;
// Bước 2: Tăng Object gốc lên 1
// (Mẹo Backend: Tái sử dụng luôn hàm Tiền tố ở trên bằng ++(*this))
++(*this);
// Bước 3: Trả về bản sao cũ
return banSao;
}
};
int main() {
cout << "--- HE THONG TOAN TU HAU TO ---\n";
PhanSo ps(1, 2); // 1/2
cout << "Ban dau: ps = " << ps << "\n";
// Test: Tăng Hậu tố và gán
PhanSo a = ps++;
cout << "\nThu nghiem: a = ps++\n";
cout << "-> Gia tri cua a (Gia tri CU): " << a << "\n"; // 1/2
cout << "-> Gia tri cua ps (Gia tri MOI): " << ps << "\n"; // 3/2
return 0;
}
Tạm kết & Bí mật tối ưu hiệu năng (Performance)
Anh em hãy nhìn thật kỹ vào hàm Hậu tố operator++(int) vừa viết. Để chạy được nó, hệ thống phải còng lưng tạo ra một cái Object banSao, cấp phát bộ nhớ cho nó, rồi trả về, sau đó lại hủy nó đi.
Trong khi đó, hàm Tiền tố ++ps không hề sinh ra bất kỳ Object phụ nào, nó đánh thẳng vào vùng nhớ gốc!
Bí mật của Senior:
Khi viết vòng lặp for với các Object (như Iterator trong std::vector hoặc std::map), hãy LUÔN LUÔN dùng Tiền tố (++it) thay vì Hậu tố (it++) nếu bạn không cần sử dụng giá trị cũ. Việc bỏ đi bước tạo "bản sao" vô ích sẽ giúp Backend của bạn tiết kiệm được kha khá CPU và RAM trên các hệ thống dữ liệu lớn đấy!Đến đây, Class Phân số của chúng ta đã cộng, trừ, tăng, giảm, in ấn mượt mà. Nhưng nó vẫn chưa biết phân biệt lớn bé. Làm sao để biết có bằng hay không? Hẹn gặp lại anh em ở Bài 34: Toán tử so sánh - Cán cân công lý của OOP! Nhớ Upvote để mình có động lực gõ code nhé!
All rights reserved