7 运算符重载
运算符重载的方式
类内定义 (成员函数方式)
将运算符重载函数定义在类的内部。
- 对于二元运算符(如 +, -, *, /),左操作数是通过 this 指针隐式传递的,所以函数只需要显式接收一个参数(右操作数)。
- 单目运算符(如负号 -、自增 ++)通常都定义为成员函数。
类外定义 (全局/友元函数方式)
- 对于二元运算符,函数必须接收两个参数(左操作数和右操作数)。
- 当你需要改变操作数的顺序(例如实现 int + Object 而不是 Object + int)时,必须使用类外定义。
可以重载的运算符
| 运算符 | 操作符 | |
|---|---|---|
| 二元 | 算术运算符 | + (加), - (减), * (乘), / (除), % (取模) |
| 二元 | 关系运算符 | == (等于), != (不等于), < (小于), > (大于), <= (小于等于), >= (大于等于) |
| 二元 | 逻辑运算符 | \|\| (逻辑或), && (逻辑与) |
| 二元 | 位运算符 | \| (按位或), & (按位与), ^ (按位异或), << (左移), >> (右移) |
| 单目运算符 | + (正), - (负), * (指针), & (取地址), ! (逻辑非), ~ (按位取反) |
|
| 自增自减运算符 | ++ (自增), -- (自减) |
|
| 赋值运算符 | =, +=, -=, *=, /=, %=, &=, \|=, ^=, <<=, >>= |
|
| 其他运算符 | () (函数调用), -> (成员访问), , (逗号), [] (下标) |
|
| (超纲) | 空间申请与释放 | new, delete, new[], []delete |
不可重载的运算符
::(作用域解析).(成员访问).*(通过成员指针的成员访问)?:(三元条件)sizeof(包括除了new,delete之外的关键字运算符,比如alignof,typeid等)#(预处理符号)
注意:
- 不能创建新运算符,例如 **(C++没有这个幂运算符)、<> 或 &|。
- 运算符 && 与 || 的重载失去短路求值。
- 重载的运算符 -> 必须要么返回裸指针,要么(按引用或值)返回同样重载了运算符 -> 的对象。
- 不可能更改运算符的优先级、结合方向或操作数的数量(常考)。
普通运算符重载
| 运算符名 | 语法 | 类内定义 (成员函数) | 类外定义 (友元函数) |
|---|---|---|---|
| 一元加 | +a |
T T::operator+() const; |
T operator+(const T &a); |
| 一元减 | -a |
T T::operator-() const; |
T operator-(const T &a); |
| 加法 | a + b |
T T::operator+(const T2 &b) const; |
T operator+(const T &a, const T2 &b); |
| 减法 | a - b |
T T::operator-(const T2 &b) const; |
T operator-(const T &a, const T2 &b); |
| 乘法 | a * b |
T T::operator*(const T2 &b) const; |
T operator*(const T &a, const T2 &b); |
| 除法 | a / b |
T T::operator/(const T2 &b) const; |
T operator/(const T &a, const T2 &b); |
| 取模 | a % b |
T T::operator%(const T2 &b) const; |
T operator%(const T &a, const T2 &b); |
- 部分情况下,需要返回左值对象,可使用 T& 作为返回值。例如 operator<< 和 operator>> 作为流插入和流提取的重载所返回的是 T&
- T2 可以是包括 T 在内的任何类型
Integer operator + (const Integer& Int) {
return Integer(x+Int.x);
}
d = a + 4; // 不报错,因为4可以被转换成const Integer&
e = 4 + a; // 报错,4 + 不会调用重载的运算符
//若重载函数的参数改成Integer& Int,则第一个会报错,因为4是右值(见上一课的笔记)
//若改成Integer,则第一个不报错,因为按值传递,可以转换
自增运算符
- 若为前缀自增运算符,直接重载。
- 若为后缀自增运算符,该函数有一个 int 类型的虚拟形参,这个形参在函数的主体中是不会被使用的,这只是一个约定,它告诉编译器递增运算符正在后缀模式下被重载。
| 运算符 | 形式 | 类内定义原型 (Class T) | 类外定义原型 |
|---|---|---|---|
| 自增 | 前缀 (++a) |
T& T::operator++(); |
T& operator++(T& a); |
| 自减 | 前缀 (--a) |
T& T::operator--(); |
T& operator--(T& a); |
| 自增 | 后缀 (a++) |
T T::operator++(int); |
T operator++(T& a, int); |
| 自减 | 后缀 (a--) |
T T::operator--(int); |
T operator--(T& a, int); |
- 内建运算符的前置版本返回引用,后置版本返回值,用户重载要和原生 int 的行为保持一致。
-
后缀的int参数在调用时,编译器会自动传递 0 给它。虽然用户也可以手动调用(例如
a.operator++(2)),但这通常没有实际意义,只是为了语法上的可行性。class Integer { int x; public: Integer(int x=0):x(x) { } Integer& operator ++ () { cerr<<"prefix is invoked"<<endl; ++x; return *this; } Integer operator ++ (int) { cerr<<"suffix is invoked"<<endl; return Integer(x++); // 另一种方式: // Integer temp = *this; x++; // return temp; } }; -
前缀运算符,先增后用,一般返回增后的对象的引用(左值)
- 后缀运算符,先用后增,先建立原始对象的副本再自增,一般 返回副本对象(右值)
赋值运算符
| 运算符名 | 日常语法 | 类内定义(成员函数)原型 | 类外定义(友元函数)原型 |
|---|---|---|---|
| 简单赋值 | a = b |
T& T::operator=(const T2 &b); |
N/A(不允许) |
| 加法赋值 | a += b |
T& T::operator+=(const T2 &b); |
T& operator+=(T& a, const T2 &b); |
| 减法赋值 | a -= b |
T& T::operator-=(const T2 &b); |
T& operator-=(T& a, const T2 &b); |
| 乘法赋值 | a *= b |
T& T::operator*=(const T2 &b); |
T& operator*=(T& a, const T2 &b); |
| 除法赋值 | a /= b |
T& T::operator/=(const T2 &b); |
T& operator/=(T& a, const T2 &b); |
| 取模赋值 | a %= b |
T& T::operator%=(const T2 &b); |
T& operator%=(T& a, const T2 &b); |
| 逐位与赋值 | a &= b |
T& T::operator&=(const T2 &b); |
T& operator&=(T& a, const T2 &b); |
| 逐位或赋值 | a \|= b |
T& T::operator\|=(const T2 &b); |
T& operator\|=(T& a, const T2 &b); |
| 逐位异或赋值 | a ^= b |
T& T::operator^=(const T2 &b); |
T& operator^=(T& a, const T2 &b); |
| 左移赋值 | a <<= b |
T& T::operator<<=(const T2 &b); |
T& operator<<=(T& a, const T2 &b); |
| 右移赋值 | a >>= b |
T& T::operator>>=(const T2 &b); |
T& operator>>=(T& a, const T2 &b); |
常见语义
- 浅拷贝赋值
- 深拷贝赋值
- 移动赋值:这是C++11引入的的新特性,通过自定义 operator=(T&&) 来重载,专门处理右值。直接转移资源所有权到新对象,源对象指针置空。
// 深拷贝赋值运算符 完整实现
T& operator=(const T& other)
{
// 1. 自赋值检查
if (this != &other)
{
// 地址不相等才执行赋值
// 2. 内存大小不匹配时,先销毁旧内存、重新开辟新空间
if (other.size != size)
{
delete[] mArray; // 释放当前对象旧的堆数组内存
size = 0; // 长度置0
mArray = nullptr; // 指针置空,防止野指针(异常安全保证)
mArray = new int[other.size]; // 按照源对象大小,开辟全新堆内存
size = other.size; // 更新数组长度
}
// 3. 数据完整深拷贝:把源对象数组的所有元素,全部复制到新内存
std::copy(other.mArray, other.mArray + other.size, mArray);
// 含义:从源数组起始地址 ~ 末尾地址,全部拷贝到当前对象的新数组空间
}
// 4. 最后返回当前对象自身引用,支持连续赋值
return *this;
}
// 移动赋值运算符
T& operator=(T&& other) noexcept {
// 1. 防止自赋值
if (this != &other) {
// 2. 释放自己原有的资源
delete[] mArray;
// 3. 直接窃取资源,而不是 new + copy
mArray = other.mArray; // 指针直接指过去
size = other.size;
// 4. 将源对象置空,防止其析构时释放资源
other.mArray = nullptr;
other.size = 0;
}
return *this;
}
std::move本身并不执行任何移动操作,它只是将一个对象无条件地转换为“右值引用”。如果你有一个左值(比如 MyClass a;),默认情况下它只能调用拷贝构造函数。如果你想让它调用移动构造函数(以节省性能),你就必须把它强制转换成右值引用。
C++ 依靠参数类型来区分重载函数。
- 如果你写
operator=(const T& other),这是拷贝赋值。它适用于所有对象,但它会进行深拷贝。 - 如果你写
operator=(T&& other),这是移动赋值。它只匹配右值(临时对象)。 - 如果不加
&&(只用&):编译器会认为这是一个普通变量的引用。你不能把一个临时对象(右值)绑定到一个非常量左值引用上(这是 C++ 的安全机制,防止你意外修改一个临时变量然后发现结果丢失了)。 - 加上
&&:编译器就知道“用户专门写了一个函数来处理临时对象”,于是当代码中出现obj = getObject()时,编译器会优先调用这个移动赋值版本。