Trong bài này chúng ta sẽ học nạp chồng toán tử trong C++ (operator overloading): cách cho class của mình dùng được các toán tử +, ==, <, <<, [] như kiểu có sẵn. Nhờ đó có thể cộng 2 phân số bằng dấu +, sắp xếp danh sách sách bằng std::sort, và in đối tượng bằng std::cout.
Bài 22 trong Serie Lập trình C++ từ A tới Z
Nạp chồng toán tử trong C++ là gì?
Với int, các bạn viết a + b, a == b, std::cout << a rất tự nhiên. Với class tự viết như PhanSo, mặc định không toán tử nào dùng được, phải gọi hàm kiểu a.cong(b), a.bang(b).
Nạp chồng toán tử là định nghĩa ý nghĩa của toán tử cho class của mình. Thực chất mỗi toán tử là một hàm có tên đặc biệt: a + b được trình biên dịch dịch thành lời gọi a.operator+(b) hoặc operator+(a, b). Đây là một dạng đa hình lúc biên dịch, giống nạp chồng hàm.
Dùng đúng thì code đọc như toán học. Dùng sai thì người đọc hiểu nhầm hoàn toàn, nên phần quy tắc ở cuối bài rất quan trọng.
Nạp chồng toán tử số học và so sánh
Ví dụ kinh điển là class phân số:
#include <iostream>
#include <numeric>
class PhanSo
{
public:
PhanSo(int tu = 0, int mau = 1) : tu_(tu), mau_(mau) { rutGon(); }
PhanSo operator+(const PhanSo& b) const // ham thanh vien
{
return PhanSo(tu_ * b.mau_ + b.tu_ * mau_, mau_ * b.mau_);
}
PhanSo& operator+=(const PhanSo& b)
{
*this = *this + b;
return *this;
}
bool operator==(const PhanSo& b) const { return tu_ == b.tu_ && mau_ == b.mau_; }
friend std::ostream& operator<<(std::ostream& os, const PhanSo& p);
private:
void rutGon()
{
int g = std::gcd(tu_, mau_);
tu_ /= g;
mau_ /= g;
if (mau_ < 0) { tu_ = -tu_; mau_ = -mau_; }
}
int tu_, mau_;
};
std::ostream& operator<<(std::ostream& os, const PhanSo& p) // ham tu do, ban cua PhanSo
{
return os << p.tu_ << '/' << p.mau_;
}
int main()
{
PhanSo a(1, 2), b(1, 3);
PhanSo c = a + b;
std::cout << a << " + " << b << " = " << c << '\n';
c += PhanSo(1, 6);
std::cout << "Cong them 1/6: " << c << '\n';
std::cout << "2/4 == 1/2 ? " << (PhanSo(2, 4) == PhanSo(1, 2)) << '\n';
return 0;
}
operator+nhận phân số thứ hai bằngconst&, trả về phân số mới, không sửa phân số hiện tại. Thêmconstcuối hàm vìa + bkhông được làm thay đổia.operator+=sửa chính đối tượng hiện tại rồi trả về tham chiếu tới nó (*this), để viết đượcc += xgiống kiểu có sẵn.operator==trả vềbool. Nhờ constructor luôn rút gọn (dùngstd::gcdcủa C++17), 2/4 và 1/2 được lưu giống nhau nên so sánh đúng.
Constructor PhanSo(int tu = 0, int mau = 1) mình cố ý không viết explicit như khuyên ở bài constructor, để số nguyên tự chuyển thành phân số: a + 1 hiểu là a + 1/1. Đây là trường hợp chuyển kiểu ngầm mang nghĩa đúng.
Toán tử xuất << để in bằng cout
std::cout << a gọi hàm operator<<(std::cout, a). Vế trái là std::ostream chứ không phải PhanSo, nên toán tử này phải là hàm tự do, không thể là hàm thành viên của PhanSo. Vì cần đọc tu_, mau_ là private, nó được khai báo là friend như bài tính đóng gói trong C++.
Điều quan trọng là toán tử phải trả về std::ostream&, chính là os nhận vào. Nhờ vậy std::cout << "a = " << a << '\n' mới nối tiếp được. Thử trả về void:
#include <iostream>
class PhanSo
{
public:
PhanSo(int tu, int mau) : tu_(tu), mau_(mau) {}
friend void operator<<(std::ostream& os, const PhanSo& p) // tra ve void
{
os << p.tu_ << '/' << p.mau_;
}
private:
int tu_, mau_;
};
int main()
{
PhanSo a(1, 2);
std::cout << "a = " << a << '\n';
return 0;
}
std::cout << "a = " << a chạy được, nhưng kết quả là void, nên << '\n' phía sau không có gì để nối vào và g++ báo invalid operands of types 'void' and 'char'.
Lưu ý: Toán tử << luôn có dạng std::ostream& operator<<(std::ostream& os, const T& x) và kết thúc bằng return os;. Toán tử gán kết hợp như += luôn trả về T& và kết thúc bằng return *this;. Sai kiểu trả về là không nối được các phép toán.
Toán tử < để sắp xếp và == để tìm kiếm
Thuật toán của thư viện chuẩn như std::sort và std::find dùng toán tử < và ==. Nạp chồng 2 toán tử này là class Sach dùng được ngay với chúng:
#include <algorithm>
#include <iostream>
#include <string>
#include <vector>
class Sach
{
public:
Sach(int ma, const std::string& ten) : ma_(ma), ten_(ten) {}
bool operator<(const Sach& b) const { return ten_ < b.ten_; } // sap theo ten
bool operator==(const Sach& b) const { return ma_ == b.ma_; } // cung ma la cung sach
friend std::ostream& operator<<(std::ostream& os, const Sach& s)
{
return os << "[" << s.ma_ << "] " << s.ten_;
}
private:
int ma_;
std::string ten_;
};
int main()
{
std::vector<Sach> kho = {{1003, "Lap trinh C++"}, {1001, "Clean Code"}, {1002, "Cau truc du lieu"}};
std::sort(kho.begin(), kho.end()); // dung operator<
for (const Sach& s : kho)
std::cout << s << '\n';
Sach timKiem(1001, "");
bool coTrongKho = std::find(kho.begin(), kho.end(), timKiem) != kho.end(); // dung operator==
std::cout << "Co sach ma 1001? " << coTrongKho << '\n';
return 0;
}
std::sort sắp xếp danh sách theo tên vì operator< so sánh tên. std::find tìm thấy cuốn mã 1001 vì operator== coi 2 cuốn cùng mã là một. Lần này operator<< được định nghĩa ngay trong class, viết friend ngay tại chỗ cho gọn.
Toán tử < chỉ nên dùng khi class có một thứ tự “tự nhiên” rõ ràng. Nếu cần sắp xếp theo nhiều tiêu chí khác nhau (theo tên, theo năm, theo lượt mượn), dùng hàm so sánh riêng truyền vào std::sort, chúng ta sẽ học ở bài STL.
Toán tử chỉ số []
Toán tử [] cho phép truy cập class giống mảng. Ví dụ class KeSach bọc một vector và kiểm tra chỉ số:
#include <iostream>
#include <stdexcept>
#include <string>
#include <vector>
class KeSach
{
public:
void them(const std::string& ten) { sach_.push_back(ten); }
const std::string& operator[](std::size_t viTri) const
{
if (viTri >= sach_.size())
throw std::out_of_range("ke sach khong co vi tri " + std::to_string(viTri));
return sach_[viTri];
}
std::size_t size() const { return sach_.size(); }
private:
std::vector<std::string> sach_;
};
int main()
{
KeSach ke;
ke.them("Lap trinh C++");
ke.them("Clean Code");
for (std::size_t i = 0; i < ke.size(); i++)
std::cout << "Ngan " << i << ": " << ke[i] << '\n';
try
{
std::cout << ke[5] << '\n';
}
catch (const std::out_of_range& e)
{
std::cout << "Loi: " << e.what() << '\n';
}
return 0;
}
ke[i] gọi ke.operator[](i). Bản này trả về tham chiếu hằng, nên chỉ đọc được. Muốn cho phép gán ke[0] = "..." thì viết thêm một bản không const trả về std::string&.
Hàm thành viên hay hàm tự do?
| Toán tử | Nên viết là | Lý do |
|---|---|---|
=, [], (), -> |
Hàm thành viên | Bắt buộc theo chuẩn C++ |
+=, -=, *= |
Hàm thành viên | Sửa chính đối tượng |
<<, >> với stream |
Hàm tự do (thường là friend) | Vế trái là std::ostream, std::istream |
+, -, ==, < |
Hàm thành viên hoặc hàm tự do | Hàm tự do cho phép chuyển kiểu cả 2 vế, ví dụ 1 + a |
Với ví dụ phân số, operator+ là hàm thành viên nên a + 1 chạy được nhưng 1 + a thì không, vì vế trái 1 là int không có hàm thành viên. Muốn cả 2 chiều chạy, viết operator+ thành hàm tự do.
Quy tắc khi nạp chồng toán tử
- Giữ đúng ý nghĩa quen thuộc:
+là cộng,==là bằng. Đừng dùng+để xóa sách khỏi kho chỉ vì viết cho ngắn. - Viết đủ cặp: có
==thì nên có!=, có+thì nên có+=. Người dùng class sẽ mặc định là có. - Không nạp chồng được:
::,.,.*,?:,sizeof. Cũng không tạo được toán tử mới, ví dụ**để lũy thừa. - Độ ưu tiên của toán tử không đổi:
a + b * cvẫn nhân trước dù là phân số.
Bài tập
- Thêm
operator-,operator*,operator/vàoperator!=cho classPhanSo. - Viết lại
operator+củaPhanSothành hàm tự do, rồi kiểm tra1 + PhanSo(1, 2)biên dịch được. - Viết class
ThoiGian(giờ, phút) cóoperator+cộng 2 khoảng thời gian (phút quá 60 thì tăng giờ),operator<vàoperator<<. Sắp xếp một vector cácThoiGianbằngstd::sort. - Thêm bản
operator[]khôngconstchoKeSachđể sửa được tên sách ở một ngăn.
Lời giải sẽ có trong bài tổng hợp bài tập OOP C++ nhé.
Kết
Nạp chồng toán tử trong C++ giúp class tự viết dùng được như kiểu có sẵn: cộng bằng +, so sánh bằng == và <, in bằng std::cout, truy cập bằng [], và nhờ đó làm việc được với std::sort, std::find của thư viện chuẩn. Ba điều cần nhớ: giữ đúng ý nghĩa của toán tử, << trả về std::ostream&, còn += trả về *this.
Danh sách đầy đủ và dạng chuẩn của từng toán tử có ở cppreference: operator overloading. Bài tiếp theo là tổng hợp bài tập OOP C++ có lời giải cho cả chương. Nếu thấy bài viết có ích, hãy chia sẻ cho bạn bè nhé.

![Nạp chồng toán tử trong C++: operator+, ==, <, << và [] 1 Nạp chồng toán tử trong C++: operator+, ==, <, << và [] - Khuê Nguyễn Creator](https://khuenguyencreator.com/wp-content/uploads/2026/10/nap-chong-toan-tu-trong-c-plus.jpg)
![Nạp chồng toán tử trong C++: operator+, ==, <, << và [] 2 Nạp chồng toán tử trong C++ cho class PhanSo: cộng 1/2 với 1/3 ra 5/6, so sánh 2/4 bằng 1/2](https://khuenguyencreator.com/wp-content/uploads/2026/10/ket-qua-nap-chong-toan-tu-phan-so.png)
![Nạp chồng toán tử trong C++: operator+, ==, <, << và [] 3 g++ báo lỗi invalid operands of types void and char khi toán tử xuất trả về void](https://khuenguyencreator.com/wp-content/uploads/2026/10/loi-toan-tu-xuat-tra-ve-void.png)
![Nạp chồng toán tử trong C++: operator+, ==, <, << và [] 4 std::sort sắp xếp sách theo tên nhờ toán tử nhỏ hơn, std::find tìm sách nhờ toán tử so sánh bằng](https://khuenguyencreator.com/wp-content/uploads/2026/10/ket-qua-sap-xep-sach-toan-tu-nho-hon.png)
![Nạp chồng toán tử trong C++: operator+, ==, <, << và [] 5 Toán tử chỉ số [] của class KeSach có kiểm tra vị trí và ném ngoại lệ out_of_range](https://khuenguyencreator.com/wp-content/uploads/2026/10/ket-qua-toan-tu-chi-so.png)