文章目录
- 输出运算符<<
- 输入运算符>>
- 相等/不等运算符
- 复合赋值运算符
- 下标运算符
- 自增/自减运算符
- 成员访问运算符
输出运算符<<
通常情况下,输出运算符的第一个形参是一个 非常量ostream对象的引用
。之所以 ostream
是非常量是因为向流写入内容会改变其状态;而该形参是引用是因为我们无法直接复制一个 ostream
对象。
第二个形参一般来说是一个 常量的引用,该常量是我们想要打印的类类型。第二个形参是引用的原因是我们希望避免复制实参;而之所以该形参可以是常量是因为(通常情况下)打印对象不会改变对象的内容。
为了与其他输出运算符保持一致,operator<<
一般要返回它的 ostream
形参。
通常我们需要在类中重载 <<
以避免查看成员时输出操作过于繁琐:
class A {friend ostream& operator<<(ostream& os, const A& a);int i = 1;double d = 3.14;
};
ostream& operator<<(ostream& os, const A& a) {os << a.i << " " << a.d;return os;
}
值得注意的几点:
- 减少格式化操作(如:换行符): 目的是给用户更大的自由去决定输出的格式,如果我们自带换行,那么用户就无法在同一行内解接着打印一些描述性文本了。
- 输入输出运算符必须是非成员函数: 如果是某个类的成员函数,则输入输出运算符也必须是
istream
或ostream
成员(详见下文),但是这两个类(istream
、ostream
)属于标准库,而我们无法给标准库中的类添加任何成员。 - 可以将IO运算符声明为友元: 既然我们的
IO操作
又想访问类的私有成员,又不能是类的成员函数,那么声明成友元是最佳选择。
用例子来解释一下第二点:
我们都知道重载运算符的返回类型一定要与它的实际操作相匹配,因此,重载 ==
返回值为 bool
;重载 +
返回值为 类的引用
……
输入输出运算符是 IO类
的成员函数,因此其返回类型是 IO类本身
,那么如果某个类将 重载的输入输出运算符 作为 成员函数 的话,返回类型 就会变成 这个类本身,重载的输入输出运算符的左侧运算对象则是这个类的一个对象:
class B {int i = 1;double d = 3.14;
public:ostream& operator<<(ostream& os) {os << i << " " << d;return os;}
};
B b;
b << cout; // 这样调用不符合我们的输出习惯
如此一来改变了 <<
的调用方式,也就不算构成重载了,如果既要 cout << b;
,还要 <<
是 B
的成员。那么就要在 ostream类
中添加 ostream& operator<<(B&);
,可正如前文所说,ostream
属于标准库,我们无法给标准库中的类添加任何成员。ostream
中 <<
的各类重载如下:
输入运算符>>
- 通常情况下,输入运算符的第一个形参是运算符将要读取的流的引用
- 第二个形参是将要读入到的对象的引用(对象不能是常量,因为将数据读入到这个对象中实际是修改了这个对象)
- 通常会返回某个给定流的引用
与输出运算符不同的是,输入运算符必须处理输入失败的情况:
class A {friend istream& operator>>(istream& is, A& a);friend ostream& operator<<(ostream& os, const A& a);int i = 1;double d = 3.14;
};istream& operator>>(istream& is, A& a) {int i1;double d1;is >> i1 >> d1;if (is) {a.i = i1;a.d = d1;}else a = A();return is;
}ostream& operator<<(ostream& os, const A& a) {os << a.i << " " << a.d;return os;
}
重写输入运算符后,可以对输入的数据进行对应处理:
当没有输入/输入错误时,用构造函数创建一个临时量,然后调用赋值运算符为 a
赋值:
当有多个输入时,对输入进行处理:
相等/不等运算符
对于类而言,判断相等需要比较每一项数据成员,因此有必要对相等运算符进行重载。
如果定义了 operator==
,则这个类也应该定义 operator!=
。对于用户来说,当他们能使用 ==
时肯定也希望能使用 !=
,反之亦然。
相等运算符和不相等运算符中的一个应该把工作委托给另外一个,这意味着其中一个运算符应该负责实际比较对象的工作,而另一个运算符则只是调用那个真正工作的运算符。
class A {friend istream& operator>>(istream& is, A& a);friend ostream& operator<<(ostream& os, const A& a);int i = 1;double d = 3.14;
public:bool operator==(const A& a) {return this->d == a.d && this->i == a.i;}bool operator!=(const A& a) {return !(*this == a);}
};
复合赋值运算符
复合赋值运算符不一定非得是类的成员,不过我们还是倾向于把包括复合赋值在内的所有赋值运算都定义在类的内部。为了与内置类型的复合赋值保持一致,类中的复合赋值运算符也要返回其左侧运算对象的引用。
PS:赋值运算符必须是类的成员
A& operator+=(const A& a) {i += a.i;d += a.d;return *this;
}
下标运算符
- 下标运算符必须是成员函数。
- 为了与下标的原始定义兼容,下标运算符通常以所访问元素的引用作为返回值,这样做的好处是下标可以出现在赋值运算符的任意一端。
- 如果一个类包含下标运算符,则它通常会定义两个版本:一个返回普通引用,另一个是类的常量成员并且返回常量引用。
class IntVec // IntVec是对标准库vector类的模仿,仅存储int元素
{int* begin; // 指向已分配的内存中的首元素int* end; // 指向最后一个实际元素之后的位置int* cap; // 指向分配的内存末尾之后的位置
public:int& operator[](int n) { return begin[n]; }const int& operator[](int n) const { return begin[n]; } // 第二个const修饰*this
};
下标运算符返回的是元素的引用,当 IntVec
是非常量时,我们可以给元素赋值;而我们对常量对象取下标时,不能对其赋值。
自增/自减运算符
与内置类型一样,重载的自增自减同时要有前置版本和后置版本。
要想同时定义前置和后置运算符,必须首先解决一个问题,即普通的重载形式无法区分这两种情况。
为了解决这个问题,后置版本接受一个额外的(不被使用)int
类型的形参。当我们使用后置运算符时,编译器为这个形参提供一个值为 0
的实参。尽管从语法上来说后置函数可以使用这个额外的形参,但是在实际过程中通常不会这么做。这个形参的唯一作用就是区分前置版本和后置版本的函数,而不是真的要在实现后置版本时参与运算。
前置版本:
// 仅作伪代码实现
类名& operator++();
类名& operator--();
后置版本:
为了与内置版本保持一致,后置运算符应该返回对象的原值(递增或递减之前的值),返回的形式是一个值而非引用。
类名 operator++(int); // 我们不会用到int形参,因此无需为它命名。
类名 operator--(int);
// 举个例子,但不详细实现Ptr类了,可以将它理解为 IntVec(或真正的顺序容器) 的指针类
Ptr operator++(int){Ptr ret = *this; // 记录当前值++*this; // 调用前置++运算符,前置++需要检查自增的有效性return res; // 返回之前记录的状态
}
/* 显式地调用后置运算符 */
Ptr p(v); // p指向v中的vector
p.operator++(0); // 调用后置版本,尽管0会被忽略,却必不可少,因为编译器只有通过它才知道应该使用后置版本。
p.operator++(); // 调用前置版本
成员访问运算符
箭头运算符(->
)必须是类的成员。 解引用运算符(*
)则无硬性要求。
// 伪代码
class Ptr{
public:int& operator*() const {// 检查解引用对象是否在规定范围内return *p[下标]; // *p可以是形如vector的对象}int* operator->() const {return & this->operator*(); //将工作委托给解引用运算符}
};
较之解引用运算符,重载箭头运算符有些限制,重载的箭头运算符必须返回类的指针或者自定义了箭头运算符的某个类的对象。
例如,对于形如 p->mem
的表达式来说,根据 p
类型的不同,表达式分别等价于:
(*p).mem; // p是一个内置的指针类型
p.operator()->mem; // p是类的对象
除此之外,代码都将发生错误。p->mem
的执行过程如下所示:
1.如果 p
是指针,则我们应用内置的箭头运算符,首先解引用该指针,然后从所得的对象中获取指定的成员。如果 p
所指的类型没有名为 mem
的成员,程序会发生错误。
2.如果 p
是定义了 operator->
的类的一个对象,如果 p.operator()->
的结果是一个指针,则执行第1步;如果该结果本身含有重载的 operator->()
,则重复调用当前步骤。最终,当这一过程结束时程序或者返回了所需的内容,或者返回一些表示程序错误的信息。