C++ 运算符重载全解析:+、=、>>、<<、++、==
什么是运算符重载?
运算符重载是 C++ 提供的一种语法扩展机制,使得自定义类也可以使用类似于内置类型的操作符,换句话说,你可以为你的类赋予自然的操作符语义,让代码更优雅、更贴近业务逻辑。
基本语法
重载函数本质上是一个特殊函数:
返回类型 operator符号(参数列表) 它可以是:
- 成员函数
- 非成员函数(全局函数,或友元)
一、重载 + 运算符
为什么要重载 + 运算符?
假设你有一个二维向量类:
class Vector2D { public: int x, y; }; 现在你希望支持类似以下操作:
Vector2D a(1, 2), b(3, 4); Vector2D c = a + b; // 理想行为 这是不可能的,除非你 告诉编译器“+”在你的类中应该怎么用 —— 也就是重载它。
如何重载 + 运算符?
成员函数版本
class Vector2D { public: int x, y; Vector2D(int x = 0, int y = 0) : x(x), y(y) {} Vector2D operator+(const Vector2D& other) const { return Vector2D(x + other.x, y + other.y); } }; 使用方式:
Vector2D a(1, 2), b(3, 4); Vector2D c = a + b; // 自动调用 operator+ 建议使用const引用作为参数,避免不必要的拷贝const成员函数表示不会修改当前对象
全局函数版本(推荐)
有些时候你希望支持这种写法:
Vector2D a(1, 2); Vector2D c = a + 5; // 或者 5 + a; 此时就不能用成员函数,因为左侧的 5 并不是类对象。
做法:写成友元 + 全局函数
class Vector2D { public: int x, y; Vector2D(int x = 0, int y = 0) : x(x), y(y) {} friend Vector2D operator+(const Vector2D& lhs, const Vector2D& rhs); }; Vector2D operator+(const Vector2D& lhs, const Vector2D& rhs) { return Vector2D(lhs.x + rhs.x, lhs.y + rhs.y); } 支持混合类型加法
你还可以重载 int + Vector2D 或 Vector2D + int:
Vector2D operator+(const Vector2D& v, int val) { return Vector2D(v.x + val, v.y + val); } Vector2D operator+(int val, const Vector2D& v) { return Vector2D(v.x + val, v.y + val); } 小练习:重载 + 支持字符串拼接类
class MyString { string data; public: MyString(string s) : data(s) {} MyString operator+(const MyString& other) const { return MyString(data + other.data); } friend ostream& operator<<(ostream& os, const MyString& s) { os << s.data; return os; } }; 使用:
MyString a("Hello, "), b("World!"); cout << a + b << endl; // 输出:Hello, World! 二、重载 += 运算符
operator+= 与 operator+ 的区别
| 运算符 | 是否修改自身 | 返回值类型 | 性能 |
|---|---|---|---|
+ | ❌ 不修改 | 新对象 | 中等 |
+= | ✅ 修改自身 | 返回引用(*this) | 更优 |
举例对比:
a = a + b; // 创建新对象并赋值 a += b; // 原地修改 a,更高效 基本写法(成员函数)
class Vector2D { public: int x, y; Vector2D(int x = 0, int y = 0) : x(x), y(y) {} Vector2D& operator+=(const Vector2D& other) { x += other.x; y += other.y; return *this; } }; 使用:
Vector2D a(1, 2), b(3, 4); a += b; // a = (4, 6) 返回 *this 的意义?
返回引用是为了支持链式调用:
a += b += c; 这样 b += c 会返回 b 的引用,接着 a += (b),逻辑就通了。
建议搭配 operator+ 一起写
通常这样配套使用:
Vector2D operator+(const Vector2D& lhs, const Vector2D& rhs) { Vector2D temp = lhs; temp += rhs; return temp; } 这样避免了重复逻辑:+ 内部直接调用 +=,更清晰。
示例:重载 += 实现字符串拼接类
class MyString { string data; public: MyString(string) : data(s) {} MyString& operator+=(const MyString& other) { data += other.data; return *this; } friend ostream& operator<<(ostream& os, const MyString& s) { os << s.data; return os; } }; 使用:
MyString a("Hello "), b("World"); a += b; cout << a; // 输出 Hello World 三、重载 >> 实现自定义输入
默认情况下,cin 只能读取内置类型(int、float、string 等)。对于自定义类,你必须自己定义输入逻辑。
通过重载 operator>>,你可以实现如下优雅写法:
cin >> person; // 自动读取 name、age、gender 等字段 基本语法(推荐写成全局友元函数)
class Person { public: string name; int age; // 声明为友元函数,允许访问私有成员 friend istream& operator>>(istream& in, Person& p); }; // 实现 istream& operator>>(istream& in, Person& p) { in >> p.name >> p.age; return in; } 使用示例:
Person p; cin >> p; 注意:引用传参 + 返回流对象引用 是标准做法,支持链式输入。
完整示例:输入二维坐标点
class Point { public: int x, y; friend istream& operator>>(istream& in, Point& p); friend ostream& operator<<(ostream& out, const Point& p); }; istream& operator>>(istream& in, Point& p) { cout << "请输入x和y:"; in >> p.x >> p.y; return in; } ostream& operator<<(ostream& out, const Point& p) { out << "(" << p.x << ", " << p.y << ")"; return out; } 使用方式:
Point pt; cin >> pt; cout << pt; // 输出 (x, y) 应用场景举例
- 输入学生信息
cin >> student; // 读取姓名、学号、成绩等 - 输入日期
cin >> date; // 读取年、月、日 - 批量数据输入
vector<Point> vec(3); for (auto& pt : vec) cin >> pt; 小技巧:配合 getline
如果类成员中有空格,如字符串:
class Book { public: string title; int pages; friend istream& operator>>(istream& in, Book& b) { getline(in >> ws, b.title); // 跳过前导空格 in >> b.pages; return in; } }; 四、重载 << 实现自定义输出
默认情况下,cout << 只能用于内置类型和部分 STL 类型(如 string、vector<int> 等)。
对于自定义类,如 Person、Date、Point,你需要自己定义如何将对象内容格式化输出到流中。
重载 << 后的代码将更直观、可维护性更高:
cout << person << endl; 基本语法:推荐使用全局友元函数
class Person { public: string name; int age; // 友元声明,允许访问私有成员 friend ostream& operator<<(ostream& out, const Person& p); }; // 函数定义 ostream& operator<<(ostream& out, const Person& p) { out << "姓名: " << p.name << ", 年龄: " << p.age; return out; } 注意:
- 参数用 常引用,避免拷贝
- 返回
ostream&,支持链式输出
完整示例:输出二维坐标点
class Point { private: int x, y; public: Point(int x = 0, int y = 0) : x(x), y(y) {} friend ostream& operator<<(ostream& out, const Point& p); }; ostream& operator<<(ostream& out, const Point& p) { out << "(" << p.x << ", " << p.y << ")"; return out; } 使用方式:
Point pt(3, 4); cout << pt << endl; // 输出 (3, 4) 支持链式调用
由于 cout 是一个 ostream 类型对象,重载函数返回 ostream& 可支持多次连续输出:
cout << p1 << ", " << p2 << ", " << p3 << endl; 如果你忘记 return out;,将会破坏链式调用行为。
常见错误
| 错误写法 | 正确写法 |
|---|---|
void operator<<(ostream&, T&) | ostream& operator<<(ostream&, const T&) |
| 写成类的成员函数(不推荐) | 写成友元的全局函数 |
忘记返回 out | 应该 return out; 支持链式调用 |
实际项目应用举例
- 打印调试对象
cout << user; // 打印用户信息 - 格式化输出日志信息
logFile << "[INFO] " << timestamp << ": " << event << endl; - 配合泛型容器使用
vector<Person> vec = {p1, p2}; for (auto& p : vec) cout << p << endl; 配合文件流也适用
重载的 << 不仅支持 cout,也支持其他 ostream 类型如 ofstream:
ofstream out("person.txt"); out << p1 << endl; 重载<<与>>的区别
| 运算符 | 目的 | 传参类型 | 返回类型 |
|---|---|---|---|
operator<< | 输出到流 | ostream&, const T& | ostream& |
operator>> | 从流中读取 | istream&, T& | istream& |
五、重载自增运算符 ++(前置与后置)
基础回顾:什么是 ++?
自增运算符 ++ 是一种一元运算符,可以作用于变量前或后:
int a = 5; ++a; // 前置 ++a:先加后用 a++; // 后置 a++:先用后加 同样地,我们可以将这种语义赋予自己的类,比如:
MyInteger a(10); ++a; // 让a的内部值+1 a++; // 后置自增 前置和后置 ++ 的底层区别
在 函数签名层面,C++ 是通过参数来区分前置和后置的:
| 类型 | 函数原型 | 说明 |
|---|---|---|
| 前置 ++a | MyClass& operator++() | 无参数,返回引用 |
| 后置 a++ | MyClass operator++(int) | 有一个 int 虚参,返回值拷贝 |
注:int 参数只是为了区分用的,不会传递值进去。
完整示例代码
让我们通过一个例子来看如何重载前置与后置 ++:
#include <iostream> using namespace std; class MyInteger { private: int val; public: MyInteger(int v = 0) : val(v) {} // 前置 ++a MyInteger& operator++() { ++val; // 先加 return *this; // 返回自身引用 } // 后置 a++ MyInteger operator++(int) { MyInteger temp = *this; // 保存旧值 val++; // 自增 return temp; // 返回旧值 } void show() const { cout << "val = " << val << endl; } }; 使用示例:
int main() { MyInteger a(10); ++a; // 前置:a变11 a.show(); a++; // 后置:a仍然变12,但返回旧值 a.show(); MyInteger b = ++a; // b = 13, a = 13 b.show(); MyInteger c = a++; // c = 13, a = 14 c.show(); } 返回值差异详解
| 类型 | 返回类型 | 原因 |
|---|---|---|
| 前置 ++a | T&(引用) | 可以链式调用,避免拷贝 |
| 后置 a++ | T(值拷贝) | 要返回旧值,所以需要保留临时对象 |
++(++a); // 合法:前置返回引用 (a++)++; // 不合法:后置返回临时变量,不能再自增 链式调用场景
我们可以支持链式写法:
MyInteger a(1); (++a)++; a.show(); // 合法:先++a,得到引用,再执行后置++ 这就要求:
- 前置返回引用
T& - 后置返回临时值
T
常见错误
| 错误写法 | 正确写法 |
|---|---|
没写后置函数参数 int | 必须写一个虚拟 int 参数区分 |
后置返回引用 T& | 应该返回 T 的拷贝 |
前置返回 void 或 T | 应返回引用 T& 以支持链式调用 |
面试经典问题
💬 “C++ 中如何实现 a++ 与 ++a 的区别?”
答:通过重载不同的函数原型实现:
T& operator++()实现前置T operator++(int)实现后置,其中int是虚参,仅用于区分
并且返回类型设计为:前置返回引用、后置返回副本。
六、重载 == 来比较对象是否相等
为什么我们要重载 ==
在 C++ 中,内置类型之间可以直接比较:
int a = 10, b = 20; cout << (a == b); // 输出 0(false) 但你自己写的类对象,默认不能比较“值相等”:
class Person { public: string name; int age; }; int main() { Person p1{"Tom", 18}; Person p2{"Tom", 18}; cout << (p1 == p2); // ❌ 报错!没有定义 == 运算符 } 原因:C++ 不知道怎么“比较两个对象”,默认只会比较地址或不支持。因此你需要自己告诉它什么是“两个对象相等”。
如何重载 == 运算符?
你可以在类中写一个特殊的函数,名字就叫 operator==,告诉 C++ 如何比较两个对象。
基本语法:
bool operator==(const ClassName& other) const; 完整案例讲解
我们来写一个完整类,表示“人”的信息:姓名和年龄。我们希望两个 Person 如果名字和年龄都一样,就认为他们是同一个人。
#include <iostream> using namespace std; class Person { public: string name; int age; // 构造函数 Person(string n, int a) : name(n), age(a) {} // 运算符重载:判断两个对象是否相等 bool operator==(const Person& other) const { return this->name == other.name && this->age == other.age; } }; 测试代码:
int main() { Person p1("Tom", 18); Person p2("Tom", 18); Person p3("Jerry", 20); if (p1 == p2) { cout << "p1 和 p2 相等!" << endl; } else { cout << "p1 和 p2 不相等!" << endl; } if (p1 == p3) { cout << "p1 和 p3 相等!" << endl; } else { cout << "p1 和 p3 不相等!" << endl; } } 输出结果:
p1 和 p2 相等! p1 和 p3 不相等! 重点解释每一行代码
bool operator==(const Person& other) const
| 关键词 | 解释 |
|---|---|
bool | 返回值是布尔类型,true 表示相等,false 表示不相等 |
operator== | 告诉 C++ 这是一个运算符函数,重载 == 运算符 |
const Person& other | 比较的另一个对象,引用可以避免拷贝,const防止被修改 |
const(末尾) | 表示这个函数不会修改当前对象,更安全,推荐写 |
== 重载必须写在类里面吗?
不一定。你也可以写在类外,但推荐写在类里,逻辑更清晰。
类外写法(不推荐给新手):
bool operator==(const Person& a, const Person& b) { return a.name == b.name && a.age == b.age; } 注意事项总结
| 常见问题 | 正确做法 |
|---|---|
忘记加 const 引用 | 比较函数参数要用 const ClassName&,避免拷贝提高性能 |
没有定义 == 就直接比较对象 | 会报错,需要重载 |
| 比较逻辑不清晰 | 根据你对象的数据来决定比较什么字段 |
配合重载 !=
通常 == 和 != 是成对出现的,只要写好 ==,你可以轻松写:
bool operator!=(const Person& other) const { return !(*this == other); // 复用前面的 == } == 运算符重载和 STL 一起用!
你定义好 == 后,你就可以使用:
std::find查找元素std::set,std::map比较键值- 判断对象是否存在于容器中
#include <vector> #include <algorithm> vector<Person> vec = {p1, p2}; if (find(vec.begin(), vec.end(), p1) != vec.end()) { cout << "找到了!" << endl; } 七、 重载 [](数组访问运算符)
为什么要重载 []?
我们知道,C++内置数组可以通过下标访问元素:
int arr[5] = {10, 20, 30, 40, 50}; cout << arr[2]; // 输出 30 但是,如果你写了自己的类,想用 obj[2] 这种写法访问类里的数据,C++默认是不支持的,因为编译器不知道你这个类应该怎么“用下标取数据”。
这时候,我们就需要重载 [] 运算符,告诉编译器“你该怎么用下标访问我的类”。
[] 运算符重载的基本语法
返回类型& operator[](参数); 返回类型&:一般返回的是引用,方便修改数据(可读可写)operator[]:告诉编译器这是重载的[]运算符参数:下标,一般用int表示访问哪个元素
举个最简单的例子
#include <iostream> using namespace std; class MyArray { private: int data[5]; public: MyArray() { for (int i = 0; i < 5; ++i) data[i] = i * 10; } int& operator[](int index) { return data[index]; // 返回对应位置元素的引用 } }; int main() { MyArray arr; cout << arr[2] << endl; // 输出 20 arr[2] = 99; // 修改数据 cout << arr[2] << endl; // 输出 99 return 0; } 重点讲解
- 为什么返回引用
int&?
因为返回引用,可以既读又写。比如上面例子,arr[2] = 99;修改了数组元素。如果返回的是值,就不能修改原数据。 - 参数
index是什么?
你用下标访问时,传入的就是这个数字,比如arr[2],index就是2。
增加“安全措施”——越界检查
原生数组不会帮你检查下标是否合法,访问越界会导致错误。我们可以加代码手动检查:
int& operator[](int index) { if (index < 0 || index >= 5) { cout << "⚠️ 越界访问,返回第一个元素!" << endl; return data[0]; } return data[index]; } 支持 const 对象访问
如果对象是 const,它调用的是 const 版本的 operator[],这时候我们要写一个只读版本:
int operator[](int index) const { if (index < 0 || index >= 5) { cout << "⚠️ 越界访问,返回0!" << endl; return 0; } return data[index]; } const 版本返回的是值,不能修改数据,只能读。
完整示例代码(含注释)
#include <iostream> using namespace std; class MyArray { private: int data[5]; public: MyArray() { for (int i = 0; i < 5; ++i) data[i] = i * 10; } // 非const版本,返回引用,可读写 int& operator[](int index) { if (index < 0 || index >= 5) { cout << "⚠️ 越界访问,返回第一个元素!" << endl; return data[0]; } return data[index]; } // const版本,只读访问 int operator[](int index) const { if (index < 0 || index >= 5) { cout << "⚠️ 越界访问,返回0!" << endl; return 0; } return data[index]; } }; int main() { MyArray arr; cout << arr[2] << endl; // 20 arr[2] = 99; cout << arr[2] << endl; // 99 const MyArray cArr; cout << cArr[3] << endl; // 30 // cArr[3] = 50; // ❌ 不能修改,编译报错 cout << arr[10] << endl; // 越界访问提示 } 八、重载赋值运算符=
赋值运算符=是我们日常最常用的操作之一,比如:
int a = 5; int b = 10; b = a; // 把a的值赋给b,b变成5 在C++里,赋值操作是用=来完成的。
为什么需要重载赋值运算符?
当你定义自己的类时,比如:
class Person { public: string name; int age; }; 如果写了:
Person p1, p2; p2 = p1; 编译器默认会给你生成一个赋值运算符,但它只做简单的“逐成员赋值”(浅拷贝)。
如果你的类里有指针或动态分配的资源(比如堆内存),简单的浅拷贝会导致两个对象指向同一块内存,出问题!
所以,我们要自己重载赋值运算符,保证赋值时数据被正确复制(深拷贝),避免程序崩溃和内存错误。
赋值运算符重载的格式
ClassName& operator=(const ClassName& rhs); ClassName&:返回自身对象的引用,支持链式赋值(a = b = c;)operator=:表示重载赋值运算符const ClassName& rhs:右侧赋值对象的常量引用,避免拷贝,提高效率
#include <iostream> #include <string> using namespace std; class Person { public: string name; int age; // 赋值运算符重载函数 Person& operator=(const Person& rhs) { // 1. 自我赋值检测,防止 p = p 时出错 if (this == &rhs) return *this; // 2. 逐成员赋值,将 rhs 的数据复制给当前对象 name = rhs.name; age = rhs.age; // 3. 返回自身引用,支持链式赋值 return *this; } }; int main() { Person p1; p1.name = "Alice"; p1.age = 30; Person p2; p2 = p1; // 调用赋值运算符重载 cout << "p2.name = " << p2.name << ", p2.age = " << p2.age << endl; } Person& operator=(const Person& rhs)
定义赋值运算符函数,rhs是右侧的赋值对象,使用const &避免复制且保证不被修改。if (this == &rhs)
判断自己是否给自己赋值。如果是,就直接返回,避免重复赋值带来的问题(比如内存释放两次)。name = rhs.name; age = rhs.age;
将rhs对象的成员变量复制给当前对象。这里是浅拷贝,但对于string类成员,它内部会自己管理深拷贝。return *this;
返回当前对象的引用,支持a = b = c;这种连续赋值写法。
什么时候必须重载赋值运算符?
当你的类含有指针成员或动态资源时,默认浅拷贝会导致:
- 多个对象指向同一块资源,修改一个会影响另一个
- 对象析构时重复释放同一块资源,程序崩溃
这时你必须写深拷贝版本的赋值运算符。
指针成员的赋值运算符(深拷贝)示例
#include <iostream> #include <cstring> using namespace std; class Person { private: char* name; int age; public: Person() : name(nullptr), age(0) {} Person(const char* n, int a) : age(a) { name = new char[strlen(n) + 1]; strcpy(name, n); } // 重载赋值运算符,深拷贝 Person& operator=(const Person& rhs) { if (this == &rhs) // 自我赋值检测 return *this; delete[] name; // 释放旧资源 name = new char[strlen(rhs.name) + 1]; // 分配新内存 strcpy(name, rhs.name); // 复制内容 age = rhs.age; return *this; } void print() { cout << "Name: " << (name ? name : "null") << ", Age: " << age << endl; } ~Person() { delete[] name; } }; int main() { Person p1("Tom", 20); Person p2; p2 = p1; // 调用深拷贝赋值 p2.print(); }