C++ Primer : 第十四章 : 重载运算与类型转换之重载运算符

重载前须知

重载运算符是特殊的函数,它们的名字由operator和其后要重载的运算符号共同组成。 因为重载运算符时函数, 因此它包含返回值、参数列表和函数体。
对于重载运算符是成员函数时, 它的第一个运算对象被隐式的绑定到this指针上,因此,成员函数的重载运算符的显示参数数量比运算符的运算对象少一个。

对一个运算符函数来说, 要么它是一个类的成员函数, 或者它的参数至少包含一个类类型。


某些运算符不应该被重载

对于逻辑与&&、逻辑或 || 和逗号运算符来说,重载它们会无法保留下来它们的运算对象的求值顺序。 而且对于&& 和 || 来说,它们具有的短路求值属性也无法保留。

对于取地址运算符,它又特定的内置含义,它也不该被重载。


重载运算符应该和内置类型一样的含义
  • 如果类执行IO操作,则定义移位运算符使其与内置类的IO 一致。
  • 一般定义了相等性运算符==,那么也应该定义!= 运算符。
  • 一个类定义了一个比较运算符,那么它也应该定义其他比较运算符。
  • 重载运算符的返回类型应该和内置版本的返回类型一致。

选择座位成员还是非成员函数

  • 赋值(=)、 下标([])、调用(())和成员访问箭头运算符必须定义为成员函数
  • 复合赋值运算符一般定义为成员函数,但不是必须的
  • 改变对象状态的运算符或者与给定类型密切相关的运算符,如递增、递减和解引用运算符,一般定义为成员函数
  • 具有对称性的运算符可能转换任意一端的运算对象,例如算术、相等性、关系和位运算符等,通常应该为非成员函数。

输入和输出运算符

输入、输出运算符必须是非成员函数, 一般被定义为类的友元!

重载输出运算符

输出运算符的第一个形参是一个非常量的ostream对象的引用,第二个形参一般是一个常量的引用,因为输出运算符不会改变参数的值。
operator << 一般返回它的ostream形参。

Sales_data的输出运算符:

ostream& operator << (ostream& os, const Sales_data& item) {
os << item.isbn() << " " << item.units_sold << " " <<item.revenue << " " << item.avg_price();
return os; }

值得注意的是,为了和内置类型的输出运算符保持一致,我们重载的输出运算符应该尽量减少格式化操作, 尤其是换行符!


重载输入运算符

输入运算符的第一个参数应该是一个要读的流的引用,第二个参数形参应该是一个非常量的对象的引用,它返回输入流的引用。
输入运算符应该处理可能输入失败的情况!


Sales_data的输入操作:

istream& operator >> (istream& is, Sales_data& item) {

	double price;
is >> item.bookNo >> item.units_sold >> price;
if (is) // 检测输入流
item.revenue = item.units_sold * price;
else
item = Sales_data(); // 输入失败,对象被赋予默认状态 return is;
}

正如程序中看到的,重载的输入运算符应该要处理可能输入失败的情况,当读取失败时,输入运算符应该负责从错误中恢复。


算术和关系运算符

通常情况下,我们将算术和关系运算符定义为非成员函数,以允许向左侧或右侧的运算对象进行转换。

算术运算符

一般的,如果定义了算术运算符,则它一般也需要定义一个对应的复合赋值运算符。

Sales_data operator + (const Sales_data& lhs, const Sales_data& rhs) {

	Sales_data sum = lhs;
sum += rhs;
return sum;
}

同时定义了算术运算符和相应的复合赋值运算符,则一般用复合赋值运算符实现算术运算符。



相等运算符

判断两个类是否相等时,我们应该比较它的所有成员:
bool operator == (const Sales_data& lhs, const Sales_data& rhs) {

	return lhs.isbn() == rhs.isbn() &&
lhs.units_sold == rhs.units_sold &&
lhs.revenue == rhs.revenue;
} bool operator != (const Sales_data& lhs, const Sales_data& rhs) { return !(lhs == rhs);
}

如果定义了==, 则运算符应该判断给定的两个对象是否含有重复数据。
==应该具有传递性,如果 a == b, b == c, 则 a == c。
如果定义了==,则我们也应该定义 !=
相等运算符和不等运算符中的一个应该把工作委托给另一个。


关系运算符

一般定义了相等运算符的类,也应该定义关系运算符,特别的是,关联容器和一些算法需要用到小于运算符,因此定义operator < 比较有用。
关系运算符应该:1. 定义顺序关系,令其与关联容器对关键字的要求一样,严格弱序。 2. 如果类同时有 == 运算符,则定义一种关系令其与==保持一致,如果两个对象是!= 的,那么一个对象应该 < 另一个。

存在唯一的逻辑可靠的 < 定义,才考虑为一个类定义 < 运算符。如果类同时定义了==, 当且仅当<的定义与==产生的结果一致时才定义<运算符。


赋值运算符

赋值运算符必须定义为成员函数。
重载赋值运算符应该与内置类型的赋值运算符保持一致,应该返回左则运算对象的引用:

StrVec& StrVec::operator = (initializer_list<string> il) {

	auto data = alloc_n_copy(il.begin(), il.end());
free();
first = data.first;
last_end = cap = data.last_end;
return *this;
}

赋值运算符应该先释放左则对象的内存空间。


复合赋值运算符

复合赋值运算符不一定是类的成员函数,不过我们应该把包括复合赋值在内的所有赋值运算都定义在类的内部。复合赋值运算符也要返回左侧运算对象的引用:
Sales_data& Sales_data::operator += (const Sales_data& rhs) {

	units_sold += rhs.units_sold;
revenue += rhs.revenue;
return *this;
}


下标运算符

下表运算符必须是成员函数。

下标运算符应该以所访问的元素的引用作为返回值,而且我们最好定义常量版本和非常量版本。、

class StrVec {
public:
std::string& operator [] (size_t n) { return first[n]; }
const std::string& operator [] (size_t n) const { return first[n]; }
private:
std::string* first; // 指向数组的首元素
};

递增和递减运算符

递增和递减运算符通常应该定义为类的成员函数。

前置版本:

class StrBlobPtr {
public:
StrBlobPtr& operator++();
StrBlobPtr& operator--();
//
};

前置版本的递增/递减运算符应该返回递增或递减后的对象的引用。



后置版本:
class StrBlobPtr {
public:
StrBlobPtr operator++(int);
StrBlobPtr operator--(int);
//
};

为了前置版本和后置版本,将后置版本中的参数列表中添加一个int型参数,但是这个int型只是用来区分前置和后置版本的运算符,并不使用它。

后置版本的递增/递减运算符应该返回递增/递减前的对象的值。


成员访问运算符

箭头运算符必须是类的成员, 解引用也应该是类的成员,尽管并非如此。


class StrBlobPtr {
public:
std::string& operator*() const {
auto p = check(curr, "dreference past end");
return (*p)[curr];
} std::string* operator->()const {
return & this->operator*();
}
//
};

对箭头运算符返回值的限定

箭头运算符永远不能丢掉获取成员这一事实。

对于形如point->mem的表达式,point必须是指向类对象的指针或者是一个重载了operator->的类的对象,根据point类型不同,可分为两种情况:
(*point).mem; // (1)
point.operator()->mem; // (2)

如果point是指针,则应该使用内置的箭头运算符,表达式等价于上面的第一条。

如果point是定义了operator->的类的一个对象,则使用operator->的结果来获取mem。如果该结果是一个指针,则执行第一步,如果该结果本身含有重载的operator->(), 则重复调用当前步骤。


重载的箭头运算符必须返回类的指针或自定义了箭头运算符的某个类的对象。



上一篇:C++Primer 第十四章


下一篇:ACL配置