Skip to content

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() 时,编译器会优先调用这个移动赋值版本。