C++ | 运算符重载

运算符重载是C++的一个重要特性,允许自定义数据类型使用内置运算符。重载可以通过成员函数或全局函数实现,如加法、减法、赋值等运算符。文章介绍了重载的概念、写法、规则和常见运算符的模板,同时强调了注意事项,如保持运算符原有语义、避免重载某些特定运算符等。
摘要由CSDN通过智能技术生成

目录

运算符重载的概念

重载的写法

可重载和不可重载的运算符

约定俗成 - 重载定义的位置

运算符重载的代码模板

双目算术运算符 (+, -, *, /, %)

关系运算符 (==, !=, <, >, <=, >=)

逻辑运算符 (!, &&, ||)

单目运算符(+,-,*,&)

自增自减运算符(++,--)

位运算符 (&, |, ^, ~, <<, >>)

赋值运算符 (=, +=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>=)

其它运算符((),, ,[])

注意事项


运算符重载的概念

其实早在刚开始学习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++中,一般来说,大多数运算符的重载既可以在类内声明,也可以在类外声明。然而,有一些约定俗成的实践和限制,决定了哪些运算符更适合在类内重载,哪些更适合在类外重载,以及哪些运算符在类内外重载无所谓。大致内容见下图:

 具体细节如下:

类内重载

  1. 单目运算符:单目运算符只涉及一个操作数,比如自增(++)和自减(--)运算符。这类运算符通常可以在类内进行重载,因为它们的操作数是对象本身。
  2. 赋值运算符:赋值运算符通常在类内进行重载,用于对类的成员变量进行赋值操作。这样可以很方便地访问类的私有成员。
  3. 下标运算符:下标运算符用于对象进行类似于数组的访问,通常也在类内重载。
  4.  函数调用运算符:函数调用运算符(就是一对小括号)一般也在类内重载,用于使类的对象可以像函数一样被调用。

类外重载

  1. 双目运算符:双目运算符涉及两个操作数的运算,例如加法(+)、减法(-)和乘法(*)等。对于双目运算符,尤其是涉及非成员类型的运算符,更倾向于在类外重载。这样可以保持对称性,允许两个不同类型的对象之间进行运算。
  2. 流插入和流提取:流插入运算符(<<)和流提取运算符(>>)通常在类外部进行重载。这样使得能够以自定义的方式进行输入和输出操作,而不需要改变类的定义。
  3. 关系运算符:关系运算符(如<、>、<=、>=、==、!=)通常在类外部重载,特别是涉及两个不同类对象之间的比较。

类内类外无所谓

算术运算符:加法、减法、乘法、除法等算术运算符既可以在类内重载,也可以在类外重载,这取决于对类的设计和需求。如果运算符只涉及一个操作数(一元运算符),通常在类内重载。如果涉及两个操作数(二元运算符),则可以在类内或类外重载。

运算符重载的代码模板

 下面是代码中形参的统一解释:

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++运算符重载时的一些注意事项:

  1. 可以把运算符看作是一种特殊的函数,允许以一种类似于函数调用的方式来使用它们。这有利于我们对运算符重载的理解。
  2. C++中不允许用户定义新的运算符,只能对已有的运算符进行重载。
  3. 重载后的运算符的优先级、结合性也要保持不变,也不能改变其操作数及语法结构。
  4. 重载后的含义与原运算符的含义要保持一致。例如+重载之后也应为加的含义。
  5. 运算符重载函数不能有默认参数,否则就意味着改变了运算符操作数的个数。
  6. 运算符重载既可以在类内定义,也可以在类外全局下定义。 
  7. 运算符重载函数至少要有一个自定义类型的参数,因为如果都是内置类型的话,运算符重载也就没有意义了。
  8. 当运算符重载函数在类内声明时,其形参看起来比操作数数目少1,这是因为函数的第一个参数为隐式的this指针。
  9. 建议不要重载operator&& 和 operatorll。原因是无法在这两种情况下实现内置操作符的完整语义。说得更具体一些,内置版本版本特殊之处在于:内置版本的 && 和 II 有很灵活的短路规则,而我们自定义的运算符重载无法实现 && 和 || 的短路规则,与长久以来的习惯冲突,所以最好不要重载 && 和 ||。
  10. 点运算符(.)、域运算符(::)、点星运算符 (.*)、条件运算符( ? : )以及sizeof运算符,这5个运算符不能发生重载。
  11. operator new的重载并不是重载的new运算符,重载的是申请空间的方法。operator new也同理。具体细节见:C++动态内存管理 - new和delete

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值