目录
赋值运算符 (=, +=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>=)
运算符重载的概念
其实早在刚开始学习C++的时候我们就已经接触到运算符重载了,只是我们当时还没意识到。
std::cout << "Hello World" << std::endl;
对于这一句代码的解释如下:
cout其实是一个ostream类的对象,ostream类重载了左移运算符(operator<< ),所以cout每次调用 << 的时候就会向输出端(一般就是控制台)输出东西。也就是说 << 原本是一个左移运算符,但通过所谓的“重载操作”之后就可以执行输出操作了。
而至于运算符重载是什么呢?
运算符重载是一种C++编程特性,它允许程序员重新定义已有的运算符的行为,使得自定义的数据类型(类)可以像内置类型一样使用这些运算符。通过运算符重载,可以让自定义类对象之间进行各种运算和操作,使代码更加直观、灵活和易于理解。
在C++中,运算符重载通过重载函数(也称为运算符函数)来实现,这些函数的名称和运算符相同,但在函数名前加上"operator"关键字,后面紧跟着运算符的符号。运算符重载函数可以定义为类的成员函数或全局函数,取决于运算符重载的需求和设计。
重载的写法
接下来就让我们继续探讨C++运算符重载的写法。写法就是将函数设置为如下格式:
返回值 operator运算符( 参数… );
其中,一般对于单目运算符来说,参数就是操作数。而对于双目运算符来说,左参数就是左操作数,右参数就是右操作数。三目运算符不能重载。
下面是代码示例模板:
class MyClass {
public:
// 构造函数(如果需要)
MyClass(/* 参数列表 */)
{
// 在这里初始化对象的成员变量
}
// 一元运算符重载
// 例如:一元取反运算符(-),一元递增运算符(++)
// 返回类型 operator运算符() {
// 在这里定义运算符的操作
// }
// 二元运算符重载
// 例如:二元加法运算符(+),二元乘法运算符(*)
// 返回类型 operator运算符(const MyClass& other) {
// 在这里定义运算符的操作
// }
// 赋值运算符重载
// 返回类型 operator=(const MyClass& other) {
// 在这里定义运算符的操作
// }
// 其他常见运算符重载
// 例如:等于运算符(==),不等于运算符(!=),小于运算符(<),大于运算符(>)
// 返回类型 operator运算符(const MyClass& other) {
// 在这里定义运算符的操作
// }
// 友元函数(如果需要)
// 例如:输出运算符(<<),输入运算符(>>)
// friend 返回类型 operator运算符(std::ostream& os, const MyClass& obj) {
// 在这里定义运算符的操作
// return os;
// }
};
// 请注意,如果运算符是成员函数,它至少有一个操作数(隐含的 this 指针),
// 而如果运算符是全局函数(通过友元实现),它也许会有两个操作数。
// 如果运算符重载作为成员函数,使用对象的方式调用:
// MyClass obj1, obj2, result;
// result = obj1.operator+(obj2); // 调用二元加法运算符重载
// 如果运算符重载作为全局函数(通过友元实现),使用函数的方式调用:
// MyClass obj1, obj2, result;
// result = operator+(obj1, obj2); // 调用二元加法运算符重载
可重载和不可重载的运算符
- 一个疑惑点:同为成员访问运算符,为什么->可以发生重载,而.却不可以?
对于点运算符:(.)
左边操作数必须是一个类的对象或指向类对象的指针,而右边操作数是成员名。所以由于点运算符只用于直接访问成员,其行为是固定的,不允许进行重载。
对于箭头运算符:(->)
左边操作数必须是一个指向类对象的指针,而右边操作数是成员名。而由于箭头运算符是针对指针的操作,它的重载不会涉及对成员本身的直接访问,所以可以进行重载。
约定俗成 - 重载定义的位置
在C++中,一般来说,大多数运算符的重载既可以在类内声明,也可以在类外声明。然而,有一些约定俗成的实践和限制,决定了哪些运算符更适合在类内重载,哪些更适合在类外重载,以及哪些运算符在类内外重载无所谓。大致内容见下图:
具体细节如下:
类内重载
- 单目运算符:单目运算符只涉及一个操作数,比如自增(++)和自减(--)运算符。这类运算符通常可以在类内进行重载,因为它们的操作数是对象本身。
- 赋值运算符:赋值运算符通常在类内进行重载,用于对类的成员变量进行赋值操作。这样可以很方便地访问类的私有成员。
- 下标运算符:下标运算符用于对象进行类似于数组的访问,通常也在类内重载。
- 函数调用运算符:函数调用运算符(就是一对小括号)一般也在类内重载,用于使类的对象可以像函数一样被调用。
类外重载
- 双目运算符:双目运算符涉及两个操作数的运算,例如加法(+)、减法(-)和乘法(*)等。对于双目运算符,尤其是涉及非成员类型的运算符,更倾向于在类外重载。这样可以保持对称性,允许两个不同类型的对象之间进行运算。
- 流插入和流提取:流插入运算符(<<)和流提取运算符(>>)通常在类外部进行重载。这样使得能够以自定义的方式进行输入和输出操作,而不需要改变类的定义。
- 关系运算符:关系运算符(如<、>、<=、>=、==、!=)通常在类外部重载,特别是涉及两个不同类对象之间的比较。
类内类外无所谓
算术运算符:加法、减法、乘法、除法等算术运算符既可以在类内重载,也可以在类外重载,这取决于对类的设计和需求。如果运算符只涉及一个操作数(一元运算符),通常在类内重载。如果涉及两个操作数(二元运算符),则可以在类内或类外重载。
运算符重载的代码模板
下面是代码中形参的统一解释:
lhs: 左操作数,通常为运算符左侧的对象或值。
rhs: 右操作数,通常为运算符右侧的对象或值。
val: 通常代表一个对象或值。
shift: 用于位运算的位移量,通常为一个整数值。
ptr: 通常代表一个指针对象。
member: 成员访问运算符的右操作数,它是一个类的成员指针。index: 下标运算符的右操作数,它表示要访问的元素的索引。
还有一点,下面的代码大多数是将运算符重载函数写做普通的函数模板,并没有声明为类的成员函数。如果想要将对应的重载写成类的成员函数,那么只需要去掉函数中的第一个参数,一般来说第一个参数可以看作是被this指针代替了。(下面的标题是与上面的图片内容相对应的)
至于空间申请与释放运算符,由于太过复杂,所以就不写了。
双目算术运算符 (+, -, *, /, %)
双目运算符一般左操作数对应第一个参数,右操作数对应第二个参数。
// 加运算符重载
template <typename T>
T operator+(const T& lhs, const T& rhs) {
return lhs + rhs;
}
// 减运算符重载
template <typename T>
T operator-(const T& lhs, const T& rhs) {
return lhs - rhs;
}
// 乘运算符重载
template <typename T>
T operator*(const T& lhs, const T& rhs) {
return lhs * rhs;
}
// 除运算符重载
template <typename T>
T operator/(const T& lhs, const T& rhs) {
return lhs / rhs;
}
// 取模运算符重载
template <typename T>
T operator%(const T& lhs, const T& rhs) {
return lhs % rhs;
}
关系运算符 (==, !=, <, >, <=, >=)
// 等于运算符重载
template <typename T>
bool operator==(const T& lhs, const T& rhs) {
return lhs == rhs;
}
// 不等于运算符重载
template <typename T>
bool operator!=(const T& lhs, const T& rhs) {
return lhs != rhs;
}
// 小于运算符重载
template <typename T>
bool operator<(const T& lhs, const T& rhs) {
return lhs < rhs;
}
// 大于运算符重载
template <typename T>
bool operator>(const T& lhs, const T& rhs) {
return lhs > rhs;
}
// 小于等于运算符重载
template <typename T>
bool operator<=(const T& lhs, const T& rhs) {
return lhs <= rhs;
}
// 大于等于运算符重载
template <typename T>
bool operator>=(const T& lhs, const T& rhs) {
return lhs >= rhs;
}
逻辑运算符 (!, &&, ||)
// 非运算符重载
template <typename T>
bool operator!(const T& val) {
return !val;
}
// 与运算符重载
template <typename T>
bool operator&&(const T& lhs, const T& rhs) {
return lhs && rhs;
}
// 或运算符重载
template <typename T>
bool operator||(const T& lhs, const T& rhs) {
return lhs || rhs;
}
单目运算符(+,-,*,&)
// 正号运算符重载
template <typename T>
T operator+(const T& val) {
return +val;
}
// 负号运算符重载
template <typename T>
T operator-(const T& val) {
return -val;
}
// 指针运算符重载
template <typename T>
T& operator*(T* ptr) {
return *ptr;
}
template <typename T>
const T& operator*(const T* ptr) {
return *ptr;
}
// 取地址运算符重载
template <typename T>
const T* operator&(const T& val) {
return &val;
}
template <typename T>
T* operator&(T& val) {
return &val;
}
自增自减运算符(++,--)
// 自增运算符重载
// tips:后置++的占位参数必须为int!
template <typename T>
T& operator++(T& val) { //前置++
++val;
return val;
}
template <typename T>
T operator++(T& val, int) { //后置++
T old = val;
++val;
return old;
}
// 自减运算符重载
template <typename T>
T& operator--(T& val) { //前置--
--val;
return val;
}
template <typename T>
T operator--(T& val, int) { //后置--
T old = val;
--val;
return old;
}
位运算符 (&, |, ^, ~, <<, >>)
// 按位与重载
template <typename T>
T operator&(const T& lhs, const T& rhs) {
return lhs & rhs;
}
// 按位或重载
template <typename T>
T operator|(const T& lhs, const T& rhs) {
return lhs | rhs;
}
// 按位异或重载
template <typename T>
T operator^(const T& lhs, const T& rhs) {
return lhs ^ rhs;
}
// 按位取反重载
template <typename T>
T operator~(const T& val) {
return ~val;
}
// 左移运算符重载
template <typename T>
T operator<<(const T& val, int shift) {
return val << shift;
}
// 右移运算符重载
template <typename T>
T operator>>(const T& val, int shift) {
return val >> shift;
}
赋值运算符 (=, +=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>=)
// =运算符重载
template <typename T>
T& operator=(T& lhs, const T& rhs) {
lhs = rhs;
return lhs;
}
// +=运算符重载
template <typename T>
T& operator+=(T& lhs, const T& rhs) {
lhs += rhs;
return lhs;
}
// -=运算符重载
template <typename T>
T& operator-=(T& lhs, const T& rhs) {
lhs -= rhs;
return lhs;
}
// *=运算符重载
template <typename T>
T& operator*=(T& lhs, const T& rhs) {
lhs *= rhs;
return lhs;
}
// /=运算符重载
template <typename T>
T& operator/=(T& lhs, const T& rhs) {
lhs /= rhs;
return lhs;
}
// %=运算符重载
template <typename T>
T& operator%=(T& lhs, const T& rhs) {
lhs %= rhs;
return lhs;
}
// &=运算符重载
template <typename T>
T& operator&=(T& lhs, const T& rhs) {
lhs &= rhs;
return lhs;
}
// |=运算符重载
template <typename T>
T& operator|=(T& lhs, const T& rhs) {
lhs |= rhs;
return lhs;
}
// ^=运算符重载
template <typename T>
T& operator^=(T& lhs, const T& rhs) {
lhs ^= rhs;
return lhs;
}
// <<=运算符重载
template <typename T>
T& operator<<=(T& val, int shift) {
val <<= shift;
return val;
}
// >>=运算符重载
template <typename T>
T& operator>>=(T& val, int shift) {
val >>= shift;
return val;
}
其它运算符((),, ,[])
// 函数调用运算符 重载
// 这里只是展示一种模板泛化版本
// 具体的实现要根据实际需求进行定义。
template <typename ReturnType, typename... Args>
ReturnType operator()(Args&&... args) {
// 在这里实现函数调用的逻辑
// 返回合适的类型对象
}
// 逗号运算符重载
template <typename T>
T operator,(const T& lhs, const T& rhs) {
return (void(lhs), rhs); // 逗号运算符的返回值是右操作数的值
}
// 下标运算符重载
template <typename T, typename IndexType>
decltype(auto) operator[](T& val, const IndexType& index) {
return val[index];
}
template <typename T, typename IndexType>
decltype(auto) operator[](const T& val, const IndexType& index) {
return val[index];
}
注意事项
下面是使用C++运算符重载时的一些注意事项:
- 可以把运算符看作是一种特殊的函数,允许以一种类似于函数调用的方式来使用它们。这有利于我们对运算符重载的理解。
- C++中不允许用户定义新的运算符,只能对已有的运算符进行重载。
- 重载后的运算符的优先级、结合性也要保持不变,也不能改变其操作数及语法结构。
- 重载后的含义与原运算符的含义要保持一致。例如+重载之后也应为加的含义。
- 运算符重载函数不能有默认参数,否则就意味着改变了运算符操作数的个数。
- 运算符重载既可以在类内定义,也可以在类外全局下定义。
- 运算符重载函数至少要有一个自定义类型的参数,因为如果都是内置类型的话,运算符重载也就没有意义了。
- 当运算符重载函数在类内声明时,其形参看起来比操作数数目少1,这是因为函数的第一个参数为隐式的this指针。
- 建议不要重载operator&& 和 operatorll。原因是无法在这两种情况下实现内置操作符的完整语义。说得更具体一些,内置版本版本特殊之处在于:内置版本的 && 和 II 有很灵活的短路规则,而我们自定义的运算符重载无法实现 && 和 || 的短路规则,与长久以来的习惯冲突,所以最好不要重载 && 和 ||。
- 点运算符(.)、域运算符(::)、点星运算符 (.*)、条件运算符( ? : )以及sizeof运算符,这5个运算符不能发生重载。
- operator new的重载并不是重载的new运算符,重载的是申请空间的方法。operator new也同理。具体细节见:C++动态内存管理 - new和delete