像重载函数一样,C++ 可以重载运算符,但它又比重载函数事多一些,值得单独写一篇博客来掰扯掰扯。

格式 链接到标题

(下文以复数类为例)

函数类型 类名::operator运算符符号(参数表) {
    逻辑代码
}

// 类内,返回指针
Complex& operator+(const Complex& right) {
    this->real += right.real;
    this->imag += right.imag;
    return (*this);
}

// 类外,返回对象,需要在类内声明为友元
Complex operator+(const Complex& left, const Complex& right) {
    Complex tmp;
    tmp.real = left.real + right.real;
    tmp.imag = left.imag + right.imag;
    return tmp;
}

规则 链接到标题

  1. 运算符的符号不可杜撰,只能选用 C++ 内置的那些;
  2. 运算符的参数数量是固定的,n元运算符在友元的定义法下有n个参数,在类内定义法下有n-1个参数;
  3. .(成员访问运算符)、.*(成员指针访问运算符)、?:(条件运算符)、::(域运算符)、sizeof(长度运算符)、typeid(类型识别运算符)、#(预处理符号)不能被重载;
  4. 重载后的效果应与该运算符本身的含义符合;
  5. =(赋值运算符)、[](下标运算符)、()(函数调用运算符)、->(类成员访问运算符)必须定义为类的成员函数;
  6. <<(流输入)、>>(流输出)必须定义到类外;
  7. 重载不能改变运算符的优先级和结合性;
  8. 重载的函数不能有默认参数;
  9. 重载的运算符必须和用户自定义类型的对象一起使用(即其参数至少有一个是类对象或类对象的引用);
  10. 用于类对象的运算符必须重载,但系统会分配一个默认的 =& 重载。

返回值的选择 链接到标题

返回值为对象 链接到标题

例如:

Complex operator+(const Complex& right) {
    Complex tmp;
    tmp.real = this->real + right.real;
    tmp.imag = this->imag + right.imag;
    return tmp;
}

此时编译器会用一个 临时无名对象 把函数返回的 tmp 对象接出来完成跨作用域赋值,但它是具有 const 修饰的,所以此时是不能将其作为左值或是再次传入重载后的函数,即c1 + (c2 + c3)(c1 = c2) = c3会报错或是无法达到预期目标。

返回值为引用 链接到标题

例如:

Complex& operator+(const Complex& right) {
    this->real += right.real;
    this->imag += right.imag;
    return (*this);
}

此时就不会有临时无名对象的事了,c1 + (c2 + c3)(c1 = c2) = c3 这样的问题也就不复存在了。

自增(自减)运算符的重载 链接到标题

(下文以自增运算符为例,自减运算符只需修改符号即可适用)

自增是存在前置和后置两种情况的,而这两者实现逻辑也是有所区别的,但运算符却长的一样,所以 C++ 约定:在自增运算符重载时,不加任何参数的是前置自增,加一个 int 类型形参的是后置自增。

定义 链接到标题

class Int {
public:
    Int& operator++() {
        ++ i;
        return (*this);
    }
    const Int operator++(int) {
        Int tmp = *this;
        ++ (*this);
        return tmp;
    }
    int n;
}

返回值的选择 链接到标题

这里返回值是选择是为了保持 C++ 底层的设计,我们可以从下面这些语句里总结这一设计:

#include <iostream>
using namespace std;
int main() {
    int a = 0;
    (a ++) ++; // error C2105: “++”需要左值
    ++ (a ++); // error C2105: “++”需要左值
    a ++ = 1;  // error C2106: “=”: 左操作数必须为左值
    (++ a) ++; // OK
    ++ (++ a); // OK
    ++ a = 1;  // OK
    return 0;
}

前置自增的特性 链接到标题

i 先自增后取出,且此时可以实现 (++ i) = 5 的操作,最终 i 为 5。

所以前置自增后返回的值是可以作为左值的,而满足这一条件的只有返回值是引用了。

后置自增的特性 链接到标题

i 先取出后自增,此时不可以实现 (i ++) = 5 的操作。

所以后置自增后返回的值是不可以作为左值的,也即返回了一个普通的对象。

但如果是返回普通的对象,运行 (i ++) ++ 时是可以通过编译的,但结果却是 1 (在初值是 0 的情况下),这是违背我们直觉的,原因也正是上文说的返回的临时无名对象自带 const 修饰,所以还不如直接定义为 const 对象,让编译器直接对该语句报错。

两者的其他区别 链接到标题

两者有一下区别:

  • 返回类型不同
  • 形参不同
  • 实现代码不同
  • 效率不同

前两者上文已有描述,第三个区别不需要说明。

而效率不同我们也能很直观的理解,因为在后置自增的重载函数中调用了前置自增,同时后置自增返回是需要用临时无名对象来跨域的,而前置自增返回的是引用类型,无需次步骤,所以后置自增显然是效率更低的。

这里调用前置自增的原因是提高代码的复用性,若前置自增需要多行代码来实现,调用一次显然比再写一次方便的多。

流插入运算符与流提取运算符的重载 链接到标题

流插入运算符 >> 与流提取运算符 << 是我们经常使用的两个运算符,所以在习惯的驱使下我们也想让自定义类的对象能用这两个运算符输出。

cin 是流输入类 istream 的对象,cout 是流输出类 ostream 的对象,而流运算符是一种二目运算符,其左操作数便是对应的流对象,也因如此,流运算符的重载不能是类的成员函数,只能被声明为友元。

定义 链接到标题

(下文以流提取运算符的重载为例)

ostream& operator<<(ostream& out, const complex& right) {
    out << right.real;
    if(right.imag < 0) {
        out << right.imag << 'i';
    } else if(right.imag > 0) {
        out << '+' << right.imag << 'i';
    } else {
        out << endl;
    }
    return out;
}

为了可以实现流提取运算符的链式调用,我们需要将返回值设置为引用类型。

类型转换函数 链接到标题

此类函数用于将类对象转换成其他类型的数据。

定义 链接到标题

operator double() {
    return real;
}

特性 链接到标题

  • 函数用关键词 operator 开头,不能在前面指定类型;
  • 函数没有参数;
  • 返回值类型在关键词后面;
  • 只能作为成员函数定义;
  • 类型转换函数与运算符重载函数类似,区别在于被重载的是类型名;
  • 类型转换函数与下文的转换构造函数都会在有需要的时候被系统自动调用,建立一个临时无名对象(变量)。

转换构造函数 链接到标题

此类函数就是之前在整理构造函数时说的构造函数所带的类型转换功能的提取,用于将其他类型的数据转换为类对象。

定义 链接到标题

complex(double r) {
    real = r;
    imag = 0;
}