C++:重载运算符
基本概念
通常我们自定义的类类型,不具有内置类型的一些操作,比如 int
类型的算术运算,指针
类型的解引用、取地址操作,容器类型的下标操作等。因此,如果希望我们自定义的类类型具有一些运算符操作,就需要定义重载运算符函数,实现对应的功能。
重载运算符
是具有特殊名字的函数:它们的名字由关键字 operator
和其后要定义的运算符号
共同组成。
[返回类型] operator[运算符]([参数]) {}
和其他函数一样,重载的运算符也包含返回类型、参数列表以及函数体。
重载运算符的特点
- 重载运算符函数的参数数量与该运算符作用的运算对象数量一样多。一元运算符有一个参数,二元运算符有两个参数。
- 对于二元运算符来说,左侧运算对象传递给第一个参数,右侧运算对象传递给第二个参数。
- 除了重载调用运算符
operator()
的函数外,其他重载运算符不能含有默认实参。 - 如果一个运算符函数是成员函数,则它的第一个(左侧)运算对象绑定到隐式的
this
指针上。因此,成员运算符函数的(显式)参数数量比运算符的运算对象总数少一个。 - 对于一个运算符函数来说,它或是类的成员,或者至少有一个类类型的参数。也就是说内置类型不能有重载运算符函数。
- 不是所有的运算符都可以被重载,逻辑与、逻辑或、逗号运算符和取地址运算符不能被重载。
- 赋值(=)、下标([])、调用(())和成员访问箭头(->)运算符必须是成员函数。
改变对象状态
的运算符或者与给定类型密切相关的运算符,如递增、递减和解引用运算符,通常应该是成员函数
。- 具有
对称性
的运算符可能转换任意一端的运算对象,如算术、相等性、关系和位运算符等,通常应该是普通函数
。
输入和输出运算符
输出运算符 <<
的第一个形参是一个非 const
的 ostream
对象的引用。非 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 {}