返回项目列表

/note/ICPC笔记

ICPC笔记

当前项目读取 content/notes/ICPC笔记 下的 Markdown 文件。

ICPC笔记

运算符重载

运算符重载 下面按 “一类一类运算符” 给你讲。你可以把运算符重载理解成: 让自己写的类,也能像 int、double 一样使用 + / == () << 这些符号。 0. 运算符重载基本格式 成员函数写法 例如: 调用: 本质: 全局函数写法 例如: 调用: 本质: 1. 算术运算符重载 包括:...

运算符重载

下面按**“一类一类运算符”**给你讲。你可以把运算符重载理解成:

让自己写的类,也能像 intdouble 一样使用 + - * / == [] () << 这些符号。


0. 运算符重载基本格式

成员函数写法

返回类型 operator运算符(参数列表) {
    // ...
}

例如:

Vector operator+(const Vector& other) const;

调用:

a + b

本质:

a.operator+(b)

全局函数写法

返回类型 operator运算符(参数1, 参数2) {
    // ...
}

例如:

Vector operator+(const Vector& a, const Vector& b);

调用:

a + b

本质:

operator+(a, b)

1. 算术运算符重载

包括:

+  -  *  /  %

常用于向量、复数、矩阵、时间类。


1.1 重载 +

比如三维向量相加:

class Vector3 {
private:
    int x, y, z;

public:
    Vector3(int x = 0, int y = 0, int z = 0)
        : x(x), y(y), z(z) {}

    Vector3 operator+(const Vector3& other) const {
        return Vector3(x + other.x, y + other.y, z + other.z);
    }
};

使用:

Vector3 a(1, 2, 3);
Vector3 b(4, 5, 6);

Vector3 c = a + b;

注意:

Vector3 operator+(const Vector3& other) const

这里最后的 const 表示:

这个 + 不会修改当前对象。

也就是说:

c = a + b;

正常情况下,ab 都不应该变。


1.2 重载 -

Vector3 operator-(const Vector3& other) const {
    return Vector3(x - other.x, y - other.y, z - other.z);
}

使用:

Vector3 c = a - b;

1.3 重载 *

比如向量乘一个整数:

Vector3 operator*(int k) const {
    return Vector3(x * k, y * k, z * k);
}

使用:

Vector3 b = a * 3;

这个只能支持:

a * 3

不支持:

3 * a

因为 3 * a 的左边是 int,不会去调用 a.operator*(3)

如果你想支持 3 * a,一般写成全局函数:

friend Vector3 operator*(int k, const Vector3& v);

Vector3 operator*(int k, const Vector3& v) {
    return Vector3(v.x * k, v.y * k, v.z * k);
}

1.4 重载 /

比如向量除以整数:

Vector3 operator/(int k) const {
    return Vector3(x / k, y / k, z / k);
}

可以加判断:

Vector3 operator/(int k) const {
    if (k == 0) {
        throw runtime_error("division by zero");
    }
    return Vector3(x / k, y / k, z / k);
}

1.5 重载 %

这个比较少见,但也可以。

比如一个数类:

class Number {
private:
    int value;

public:
    Number(int value = 0) : value(value) {}

    Number operator%(const Number& other) const {
        return Number(value % other.value);
    }
};

2. 复合赋值运算符重载

包括:

+=  -=  *=  /=  %=

这类运算符一般会修改当前对象


2.1 重载 +=

Vector3& operator+=(const Vector3& other) {
    x += other.x;
    y += other.y;
    z += other.z;
    return *this;
}

使用:

a += b;

本质:

a.operator+=(b);

为什么返回 Vector3&

因为可以支持链式操作:

a += b += c;

也符合 C++ 内置类型的习惯。


2.2 重载 -=

Vector3& operator-=(const Vector3& other) {
    x -= other.x;
    y -= other.y;
    z -= other.z;
    return *this;
}

2.3 重载 *=

Vector3& operator*=(int k) {
    x *= k;
    y *= k;
    z *= k;
    return *this;
}

2.4 重载 /=

Vector3& operator/=(int k) {
    x /= k;
    y /= k;
    z /= k;
    return *this;
}

2.5 推荐写法:用 += 实现 +

很多时候可以这样写:

Vector3& operator+=(const Vector3& other) {
    x += other.x;
    y += other.y;
    z += other.z;
    return *this;
}

Vector3 operator+(const Vector3& other) const {
    Vector3 temp = *this;
    temp += other;
    return temp;
}

这样逻辑不重复。


3. 比较运算符重载

包括:

==  !=  <  >  <=  >=

返回值一般是 bool


3.1 重载 ==

bool operator==(const Vector3& other) const {
    return x == other.x && y == other.y && z == other.z;
}

使用:

if (a == b) {
    cout << "same";
}

3.2 重载 !=

可以直接利用 ==

bool operator!=(const Vector3& other) const {
    return !(*this == other);
}

3.3 重载 <

对于时间类很常见:

class ClockTime {
private:
    int hour;
    int minute;

public:
    ClockTime(int h = 0, int m = 0)
        : hour(h), minute(m) {}

    bool operator<(const ClockTime& other) const {
        return hour * 60 + minute < other.hour * 60 + other.minute;
    }
};

使用:

ClockTime a(10, 30);
ClockTime b(12, 0);

if (a < b) {
    cout << "a is earlier";
}

3.4 重载 >

bool operator>(const ClockTime& other) const {
    return other < *this;
}

意思是:

a > b

等价于:

b < a

3.5 重载 <=

bool operator<=(const ClockTime& other) const {
    return !(*this > other);
}

3.6 重载 >=

bool operator>=(const ClockTime& other) const {
    return !(*this < other);
}

3.7 比较运算符常见套路

一般只需要认真写:

operator<
operator==

其他的可以用它们推出来:

bool operator!=(const T& other) const {
    return !(*this == other);
}

bool operator>(const T& other) const {
    return other < *this;
}

bool operator<=(const T& other) const {
    return !(*this > other);
}

bool operator>=(const T& other) const {
    return !(*this < other);
}

4. 赋值运算符重载 =

这个非常重要,尤其是类里面有指针、动态数组的时候。


4.1 默认赋值的问题

比如:

class IntArray {
private:
    int* data;
    int size;

public:
    IntArray(int n) {
        size = n;
        data = new int[size];
    }

    ~IntArray() {
        delete[] data;
    }
};

如果你这样:

IntArray a(5);
IntArray b(10);

b = a;

默认赋值会把 a.data 的地址直接复制给 b.data

结果就是:

a.data 和 b.data 指向同一块内存

析构时会重复释放,容易炸。


4.2 正确重载 operator=

class IntArray {
private:
    int* data;
    int size;

public:
    IntArray(int n = 0) {
        size = n;
        data = new int[size];
    }

    ~IntArray() {
        delete[] data;
    }

    IntArray& operator=(const IntArray& other) {
        if (this == &other) {
            return *this;
        }

        delete[] data;

        size = other.size;
        data = new int[size];

        for (int i = 0; i < size; i++) {
            data[i] = other.data[i];
        }

        return *this;
    }
};

重点:

if (this == &other)

是防止自己给自己赋值:

a = a;

4.3 operator= 必须是成员函数

赋值运算符不能写成普通全局函数,必须写在类里面。

T& operator=(const T& other);

5. 自增自减运算符重载

包括:

++  --

分为前置和后置。


5.1 前置 ++

++a;

写法:

T& operator++() {
    // 修改自己
    return *this;
}

例子:

class Counter {
private:
    int x;

public:
    Counter(int x = 0) : x(x) {}

    Counter& operator++() {
        x++;
        return *this;
    }

    int value() const {
        return x;
    }
};

使用:

Counter a(5);
++a; // a 变成 6

前置 ++ 的特点:

先加,再返回加完后的自己。


5.2 后置 ++

a++;

写法:

T operator++(int) {
    T old = *this;
    // 修改自己
    return old;
}

注意这个 int 是假的,只是为了区分前置和后置。

完整例子:

class Counter {
private:
    int x;

public:
    Counter(int x = 0) : x(x) {}

    Counter& operator++() {
        x++;
        return *this;
    }

    Counter operator++(int) {
        Counter old = *this;
        x++;
        return old;
    }

    int value() const {
        return x;
    }
};

使用:

Counter a(5);

Counter b = ++a; // a = 6, b = 6
Counter c = a++; // c = 6, a = 7

5.3 前置 --

T& operator--() {
    // 修改自己
    return *this;
}

例如:

Counter& operator--() {
    x--;
    return *this;
}

5.4 后置 --

T operator--(int) {
    T old = *this;
    x--;
    return old;
}

6. 一元运算符重载

包括:

+  -  !  ~

注意这里的 +- 是一元运算符,不是加法减法。


6.1 一元负号 -

比如向量取反:

class Vector3 {
private:
    int x, y, z;

public:
    Vector3(int x = 0, int y = 0, int z = 0)
        : x(x), y(y), z(z) {}

    Vector3 operator-() const {
        return Vector3(-x, -y, -z);
    }
};

使用:

Vector3 a(1, 2, 3);
Vector3 b = -a; // (-1, -2, -3)

6.2 一元正号 +

这个比较少用。

Vector3 operator+() const {
    return *this;
}

使用:

Vector3 b = +a;

一般没什么实际意义。


6.3 逻辑非 !

比如判断一个对象是否为空:

class MyString {
private:
    string s;

public:
    MyString(string s = "") : s(s) {}

    bool operator!() const {
        return s.empty();
    }
};

使用:

MyString str("");

if (!str) {
    cout << "empty";
}

6.4 按位取反 ~

比如集合类里面表示补集,或者位集合:

class Bits {
private:
    int value;

public:
    Bits(int value = 0) : value(value) {}

    Bits operator~() const {
        return Bits(~value);
    }
};

7. 输入输出运算符重载

包括:

<<  >>

这两个非常常见。


7.1 输出运算符 <<

一般写成全局函数,而且通常配合 friend

#include <iostream>
using namespace std;

class Vector3 {
private:
    int x, y, z;

public:
    Vector3(int x = 0, int y = 0, int z = 0)
        : x(x), y(y), z(z) {}

    friend ostream& operator<<(ostream& out, const Vector3& v);
};

ostream& operator<<(ostream& out, const Vector3& v) {
    out << "(" << v.x << ", " << v.y << ", " << v.z << ")";
    return out;
}

使用:

Vector3 a(1, 2, 3);
cout << a << endl;

为什么返回 ostream&

为了支持连续输出:

cout << a << b << endl;

如果你返回 void,就不能连续输出。


7.2 输入运算符 >>

class Vector3 {
private:
    int x, y, z;

public:
    Vector3(int x = 0, int y = 0, int z = 0)
        : x(x), y(y), z(z) {}

    friend istream& operator>>(istream& in, Vector3& v);
    friend ostream& operator<<(ostream& out, const Vector3& v);
};

istream& operator>>(istream& in, Vector3& v) {
    in >> v.x >> v.y >> v.z;
    return in;
}

ostream& operator<<(ostream& out, const Vector3& v) {
    out << "(" << v.x << ", " << v.y << ", " << v.z << ")";
    return out;
}

使用:

Vector3 a;
cin >> a;
cout << a;

注意:

istream& operator>>(istream& in, Vector3& v)

这里的 v 不能加 const,因为输入要修改对象。


8. 下标运算符重载 []

[] 必须写成成员函数。


8.1 普通版本

class IntArray {
private:
    int data[100];

public:
    int& operator[](int index) {
        return data[index];
    }
};

使用:

IntArray arr;
arr[0] = 10;

为什么返回 int&

因为:

arr[0] = 10;

左边要能被修改。

如果返回 int,那只是返回副本,不能真正修改数组元素。


8.2 const 版本

const int& operator[](int index) const {
    return data[index];
}

完整写法:

class IntArray {
private:
    int data[100];

public:
    int& operator[](int index) {
        return data[index];
    }

    const int& operator[](int index) const {
        return data[index];
    }
};

为什么要两个版本?

普通对象:

IntArray a;
a[0] = 10;

调用:

int& operator[](int index)

const 对象:

const IntArray a;
cout << a[0];

调用:

const int& operator[](int index) const

9. 函数调用运算符重载 ()

这个重载后,对象就可以像函数一样调用。

这种对象叫:

函数对象 / 仿函数


9.1 基本例子

class GreaterThan {
private:
    int baseline;

public:
    GreaterThan(int x) : baseline(x) {}

    bool operator()(const int& value) const {
        return value > baseline;
    }
};

使用:

GreaterThan gt(10);

cout << gt(15); // true
cout << gt(5);  // false

本质:

gt.operator()(15);

9.2 在 find_if 中使用

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

class GreaterThan {
private:
    int baseline;

public:
    GreaterThan(int x) : baseline(x) {}

    bool operator()(const int& value) const {
        return value > baseline;
    }
};

int main() {
    vector<int> a{5, 10, 15, 20};

    auto it = find_if(a.begin(), a.end(), GreaterThan(10));

    if (it != a.end()) {
        cout << *it; // 15
    }
}

10. 成员访问运算符重载 ->

operator-> 比较特殊,常用于智能指针、迭代器。


10.1 基本例子

class Person {
public:
    string name;

    void sayHello() {
        cout << "Hello, I am " << name << endl;
    }
};

class PersonPtr {
private:
    Person* ptr;

public:
    PersonPtr(Person* p) : ptr(p) {}

    Person* operator->() {
        return ptr;
    }
};

使用:

Person p;
p.name = "Tom";

PersonPtr pp(&p);
pp->sayHello();

本质上:

pp.operator->()->sayHello();

10.2 operator-> 的返回值要求

operator-> 的返回值必须是:

  1. 裸指针;
  2. 或者是另一个也重载了 operator-> 的对象。

最常见是返回裸指针:

T* operator->();

11. 解引用运算符重载 *

这个也常用于智能指针、迭代器。


class PersonPtr {
private:
    Person* ptr;

public:
    PersonPtr(Person* p) : ptr(p) {}

    Person& operator*() {
        return *ptr;
    }

    Person* operator->() {
        return ptr;
    }
};

使用:

Person p;
PersonPtr pp(&p);

(*pp).name = "Tom";
pp->sayHello();

12. 类型转换运算符重载

这个比较容易被忽略。

它可以让对象自动转换成某种类型。


12.1 转换成 int

class Integer {
private:
    int value;

public:
    Integer(int value = 0) : value(value) {}

    operator int() const {
        return value;
    }
};

使用:

Integer a(10);

int x = a;       // 自动转成 int
cout << a + 5;   // a 转成 int 后参与计算

12.2 转换成 bool

class MyString {
private:
    string s;

public:
    MyString(string s = "") : s(s) {}

    operator bool() const {
        return !s.empty();
    }
};

使用:

MyString str("hello");

if (str) {
    cout << "not empty";
}

12.3 更推荐 explicit operator bool

为了避免乱转换,一般推荐:

explicit operator bool() const {
    return !s.empty();
}

这样允许:

if (str) {
    cout << "not empty";
}

但不太容易发生奇怪的隐式转换。


13. 逻辑运算符重载

包括:

&&  ||  !

其中 ! 前面讲过。

&&|| 虽然可以重载,但一般不推荐。


13.1 重载 &&

class Flag {
private:
    bool value;

public:
    Flag(bool value = false) : value(value) {}

    bool operator&&(const Flag& other) const {
        return value && other.value;
    }
};

使用:

Flag a(true), b(false);

if (a && b) {
    cout << "true";
}

13.2 为什么不推荐重载 &&||

因为内置的 &&|| 有短路特性。

例如:

if (p != nullptr && p->value > 0)

如果 p == nullptr,后面的 p->value 不会执行。

但是你重载的 operator&& 不一定有这种短路效果,容易造成误解。

所以实际写类的时候,比较少重载 &&||


14. 位运算符重载

包括:

&  |  ^  ~  <<  >>

注意:

<<>> 既可以是位移,也可以是输入输出。


14.1 重载 &

class Bits {
private:
    int value;

public:
    Bits(int value = 0) : value(value) {}

    Bits operator&(const Bits& other) const {
        return Bits(value & other.value);
    }
};

14.2 重载 |

Bits operator|(const Bits& other) const {
    return Bits(value | other.value);
}

14.3 重载 ^

Bits operator^(const Bits& other) const {
    return Bits(value ^ other.value);
}

14.4 重载 ~

Bits operator~() const {
    return Bits(~value);
}

14.5 重载位移 <<>>

如果不是输出输入,而是位移:

Bits operator<<(int k) const {
    return Bits(value << k);
}

Bits operator>>(int k) const {
    return Bits(value >> k);
}

使用:

Bits a(3);
Bits b = a << 2;

15. 逗号运算符重载 ,

这个可以重载,但非常少用,也不推荐初学者使用。


class Number {
private:
    int value;

public:
    Number(int value = 0) : value(value) {}

    Number operator,(const Number& other) const {
        return other;
    }
};

使用:

Number a(1), b(2);
Number c = (a, b);

这会返回 b

但是逗号重载很容易让代码变难懂,所以考试一般不会重点考。


16. 内存分配运算符重载 newdelete

这个属于比较高级的重载。


16.1 重载 operator new

class MyClass {
public:
    void* operator new(size_t size) {
        cout << "custom new, size = " << size << endl;
        return ::operator new(size);
    }

    void operator delete(void* ptr) {
        cout << "custom delete" << endl;
        ::operator delete(ptr);
    }
};

使用:

MyClass* p = new MyClass;
delete p;

输出可能是:

custom new, size = ...
custom delete

16.2 重载数组版本

void* operator new[](size_t size) {
    return ::operator new[](size);
}

void operator delete[](void* ptr) {
    ::operator delete[](ptr);
}

这个一般了解即可。


17. 取地址运算符重载 &

这个也可以重载,但非常少用。


class MyClass {
public:
    MyClass* operator&() {
        cout << "custom address operator" << endl;
        return this;
    }
};

使用:

MyClass a;
MyClass* p = &a;

实际开发中不太建议随便重载 &,因为会破坏大家对取地址的直觉。


18. 不能重载的运算符

这些不能重载:

.
.*
::
?:
sizeof
typeid
alignof

常见记住这几个:

运算符含义能否重载
.成员访问不能
::作用域解析不能
?:三目运算符不能
sizeof求大小不能
typeid类型信息不能

19. 必须写成成员函数的运算符

有些运算符必须写在类里面,不能写成普通全局函数。

运算符说明
=赋值
[]下标
()函数调用
->成员访问
类型转换运算符例如 operator int()

例如:

T& operator=(const T& other);
R& operator[](int index);
bool operator()(int x) const;
T* operator->();
operator int() const;

20. 通常写成全局函数的运算符

最典型的是:

<<
>>

因为你希望写:

cout << obj;
cin >> obj;

左边是 coutcin,不是你的对象。

所以一般写成:

friend ostream& operator<<(ostream& out, const T& obj);
friend istream& operator>>(istream& in, T& obj);

21. 完整综合例子:三维向量类

这个例子把常见重载都放进去了。

#include <iostream>
using namespace std;

class Vector3 {
private:
    int x, y, z;

public:
    Vector3(int x = 0, int y = 0, int z = 0)
        : x(x), y(y), z(z) {}

    Vector3 operator+(const Vector3& other) const {
        return Vector3(x + other.x, y + other.y, z + other.z);
    }

    Vector3 operator-(const Vector3& other) const {
        return Vector3(x - other.x, y - other.y, z - other.z);
    }

    Vector3 operator-() const {
        return Vector3(-x, -y, -z);
    }

    Vector3 operator*(int k) const {
        return Vector3(x * k, y * k, z * k);
    }

    Vector3& operator+=(const Vector3& other) {
        x += other.x;
        y += other.y;
        z += other.z;
        return *this;
    }

    Vector3& operator-=(const Vector3& other) {
        x -= other.x;
        y -= other.y;
        z -= other.z;
        return *this;
    }

    bool operator==(const Vector3& other) const {
        return x == other.x && y == other.y && z == other.z;
    }

    bool operator!=(const Vector3& other) const {
        return !(*this == other);
    }

    int& operator[](int index) {
        if (index == 0) return x;
        if (index == 1) return y;
        return z;
    }

    const int& operator[](int index) const {
        if (index == 0) return x;
        if (index == 1) return y;
        return z;
    }

    friend ostream& operator<<(ostream& out, const Vector3& v);
    friend istream& operator>>(istream& in, Vector3& v);
};

ostream& operator<<(ostream& out, const Vector3& v) {
    out << "(" << v.x << ", " << v.y << ", " << v.z << ")";
    return out;
}

istream& operator>>(istream& in, Vector3& v) {
    in >> v.x >> v.y >> v.z;
    return in;
}

int main() {
    Vector3 a(1, 2, 3);
    Vector3 b(4, 5, 6);

    cout << a + b << endl;
    cout << a - b << endl;
    cout << -a << endl;

    a += b;
    cout << a << endl;

    a[0] = 100;
    cout << a << endl;

    if (a != b) {
        cout << "not equal" << endl;
    }

    return 0;
}

22. 最重要的考试模板总结

+

T operator+(const T& other) const {
    return T(...);
}

+=

T& operator+=(const T& other) {
    // 修改当前对象
    return *this;
}

==

bool operator==(const T& other) const {
    return ...;
}

<

bool operator<(const T& other) const {
    return ...;
}

<<

friend ostream& operator<<(ostream& out, const T& obj);

ostream& operator<<(ostream& out, const T& obj) {
    out << ...;
    return out;
}

>>

friend istream& operator>>(istream& in, T& obj);

istream& operator>>(istream& in, T& obj) {
    in >> ...;
    return in;
}

[]

R& operator[](int index) {
    return ...;
}

const R& operator[](int index) const {
    return ...;
}

()

bool operator()(const int& x) const {
    return ...;
}

前置 ++

T& operator++() {
    // 修改
    return *this;
}

后置 ++

T operator++(int) {
    T old = *this;
    // 修改
    return old;
}

=

T& operator=(const T& other) {
    if (this == &other) {
        return *this;
    }

    // 释放旧资源
    // 申请新资源
    // 拷贝内容

    return *this;
}

23. 最好记的一句话

运算符重载按用途分:

+ - * /       // 返回新对象,一般不修改自己
+= -= *= /=   // 修改自己,返回 *this
== < >        // 返回 bool
<< >>         // 输入输出,通常写 friend 全局函数
[]            // 像数组一样访问,返回引用
()            // 像函数一样调用,仿函数
++ --         // 分前置和后置
=             // 深拷贝时非常重要
-> *          // 智能指针、迭代器常用

你现在最该优先掌握的是:

operator+
operator+=
operator==
operator<
operator<<
operator>>
operator[]
operator()
operator++
operator=

这几个会了,学校 C++ 类与对象、继承、实验题里的运算符重载基本就够用了。