Loading

C++:重载运算符

基本概念

通常我们自定义的类类型,不具有内置类型的一些操作,比如 int 类型的算术运算,指针类型的解引用、取地址操作,容器类型的下标操作等。因此,如果希望我们自定义的类类型具有一些运算符操作,就需要定义重载运算符函数,实现对应的功能。

重载运算符是具有特殊名字的函数:它们的名字由关键字 operator 和其后要定义的运算符号共同组成。

[返回类型] operator[运算符]([参数]) {}

和其他函数一样,重载的运算符也包含返回类型、参数列表以及函数体。

重载运算符的特点

  1. 重载运算符函数的参数数量与该运算符作用的运算对象数量一样多。一元运算符有一个参数,二元运算符有两个参数。
  2. 对于二元运算符来说,左侧运算对象传递给第一个参数,右侧运算对象传递给第二个参数。
  3. 除了重载调用运算符 operator() 的函数外,其他重载运算符不能含有默认实参。
  4. 如果一个运算符函数是成员函数,则它的第一个(左侧)运算对象绑定到隐式的 this 指针上。因此,成员运算符函数的(显式)参数数量比运算符的运算对象总数少一个。
  5. 对于一个运算符函数来说,它或是类的成员,或者至少有一个类类型的参数。也就是说内置类型不能有重载运算符函数。
  6. 不是所有的运算符都可以被重载,逻辑与、逻辑或、逗号运算符和取地址运算符不能被重载。
  7. 赋值(=)、下标([])、调用(())和成员访问箭头(->)运算符必须是成员函数。
  8. 改变对象状态的运算符或者与给定类型密切相关的运算符,如递增、递减和解引用运算符,通常应该是成员函数
  9. 具有对称性的运算符可能转换任意一端的运算对象,如算术、相等性、关系和位运算符等,通常应该是普通函数

输入和输出运算符

输出运算符 << 的第一个形参是一个非 constostream 对象的引用。非 const 是因为需要修改 ostream 对象的状态,引用是因为无法对 ostream 进行拷贝。第二个形参是想打印的类类型的 const 引用。

std::ostream &operator<<(std::ostream &os, const Foo &foo) {
    os << foo.data1 << foo.data2;
    return os;
}

输入运算符 >> 的第一个形参是运算符要读取的流的引用,第二个形参是要读到的对象的引用,区别于输出运算符使用 const 引用的原因是要修改对象的内容。

std::istream &operator>>(std::istream &is, Foo &foo) {
    is >> foo.data1 >> foo.data2;
    if (is) {  // 输入运算符需要判断读取流是否成功。
        //
    } else {
        foo = Foo();
    }
    return is;
}

输入和输出运算符必须是非成员函数,输入运算符重载函数需要处理读错误的情况。

算数和关系运算符

通常情况下,需要把算术和关系运算符定义为非成员函数,是因为允许左侧和右侧运算对象转换。算数运算符通常会计算两个运算对象并得到一个新对象,并且新对象是函数内部栈区的,所以通常返回对象的副本,而非引用。算数和关系运算符也不需要更改对象状态,所以输入的两个运算对象通常是 const 的引用。

Foo operator+(const Foo &lhs, const Foo &rhs) {
    Foo tmp = lhs;
    tmp += rhs;
    return tmp;
}

一般情况下,如果重载了算数运算符,通常会一起重载复合运算符,并且算数运算符会使用复合运算符实现。

Foo operator+=(const Foo &lhs, const Foo &rhs) {
    Foo tmp = lhs;
    tmp.data1 += rhs.data1;
    tmp.data2 += rhs.data2;
    return tmp;
}

同样,关系运算符一般情况下也是把相关的运算符一并实现。

bool operator==(const Foo &lhs, const Foo &rhs) {
    return lhs.data1 == rhs.data1 && lhs.data2 == rhs.data2;
}

bool operator!=(const Foo &lhs, const Foo &rhs) {
    return !(lhs == rhs);
}

赋值运算符

除了在 C++:构造函数与拷贝控制 中介绍的两个赋值运算符之外,C++11 还支持使用花括号的方式对对象进行赋值操作。

Foo foo = {1, 2, 3};

这归功于重载了使用初始化列表参数的赋值运算符,不伦那种类型的赋值运算符都必须是成员函数。

Foo &operator(initializer_list<T> il) {
    // 
    return *this;
}

下标运算符

下标 [] 操作是容器类用来通过位置访问容器中的元素的成员函数,所以重载下标操作符函数必须是成员函数。通常情况下,需要重载常量版本(返回 const 引用)和非常量版本(返回引用)的下标运算符,常量版本返回的元素不可以被修改。

Foo &operator[](int n) {
    return this.data[n];
}

const Foo &operator[](int n) const {
    return this.data[n];
}

当使用常量对象运行下标操作时,调用的是常量版本的下标操作符函数。

Foo f1;
f1[1] = "aa";  // 正确,调用非常量版本下标操作符函数,返回值可以修改。
 
const Foo f2;
f2[1] = "bb";  // 错误,调用常量版本下标操作符函数,返回值不可以修改。

递增和递减运算符

递增、递减操作符分为前置递增递减和后置递增递减,为了区分前置和后置,后置操作符的形参是一个值为 0 的实参,通常不写。递增、递减运算符通常为成员函数,其中后置版本由于需要记录中间值,所以返回的是局部对象的副本而非引用。

Foo &operator++() {
    // 前置递增
    check(cur); // 需要检测有效性
    ++cur; 
    return *this;
}

Foo &operator--() {
    // 前置递减
    check(cur); // 需要检测有效性
    --cur;
    return *this;
}

Foo operator++(int) {
    // 后置递增
    Foo foo = *this;
    ++*this;   // 调用前置递增
    return foo;
}

Foo operator--(int) {
    // 后置递减
    Foo foo = *this;
    --*this;   // 调用前置递减
    return foo;
}

成员访问运算符

箭头(->)运算符必须是成员函数,解引用运算符(*)通常是成员函数。箭头、解引用通常是常函数,但是返回值不是常量的。

[返回类型] &operator*() const {}
[返回类型] *operator->() const {}

函数调用运算符

如果类重载了函数调用运算符,则可以像使用函数一样的使用类,该类也称为函数对象。函数调用运算符必须是成员函数,根据对象内容返回相应的类型,所以函数调用运算符函数,可以有多个重载。因为不能改变内部状态,所以是 const 的成员函数。

[返回类型] operator() const {}

类型转换运算符

类型转换运算符是类的特殊成员函数,负责将一个类类型的值转换为其他类型。类型转换运算符没有显式的返回值,没有形参,但是必须是成员函数,应该是 const 的。

operator [type]() const {}
posted @ 2022-12-24 21:21  hiyoung  阅读(48)  评论(0编辑  收藏  举报