第14章 重载操作符与转换
14.1重载操作符的定义
重载操作符具有返回类型和形参表。
重载操作符的形参数目(包括成员函数的隐式 this 指针)与操作符的操作数数目相同。函数调用操作符可以接受任意数目的操作数。
1.重载的操作符名
+ | - | * | / | % |
^ | & | | | ~ | ! |
= | > | < | += | -= |
*= | /= | %= | ^= | &= |
|= | >> | << | >>= | <<= |
== | != | >= | <= | && |
|| | ++ | -- | ->* | , |
-> | [] | () | new | new[] |
delete | delete [] |
用于内置类型的操作符,其含义不能改变。例如,内置的整型加号操作符不能重定义:
// error: cannot redefine built-in operator for ints
int operator+(int, int);
也不能为内置数据类型重定义加号操作符。例如,不能定义接受两个数组类型操作数的 operator+。(也就是说重载操作符只能是自己定义的类)。
重载操作符必须具有至少一个类类型或枚举类型的操作数。
3.优先级和结合性是固定的
操作符的优先级、结合性或操作数目不能改变。不管操作数的类型和操作符的功能定义如何,表达式
x == y +z;
operator==.
总是将实参 y 和 z 绑定到 operator+,并且将结果用作 operator== 右操作数。
有四个符号(+, -, * 和 &)既可作一元操作符又可作二元操作符,这些操作符有的在其中一种情况下可以重载,有的两种都可以,定义的是哪个操作符由操作数数目控制。除了函数调用操作符 operator() 之外,重载操作符时使用默认实参是非法的。
4.不再具备短路求值特性
重载操作符并不保证操作数的求值顺序,因此,重载 &&、|| 或逗号操作符不是一种好的做法。
5.类成员与非成员
大多数重载操作符可以定义为普通非成员函数或类的成员函数。
作为类成员的重载函数,其形参看起来比操作数数目少 1。作为成员函数的操作符有一个隐含的 this 形参,限定为第一个操作数。
重载一元操作符如果作为成员函数就没有(显式)形参,如果作为非成员函数就有一个形参。类似地,重载二元操作符定义为成员时有一个形参,定义为非成员函数时有两个形参。
一般将算术和关系操作符定义非成员函数,而将赋值操作符定义为成员:
// member binary operator: left-hand operand bound to implicit this pointer
Sales_item& Sales_item::operator+=(const Sales_item&);
// nonmember binary operator: must declare a parameter for each operand
Sales_item operator+(const Sales_item&, const Sales_item&);
加返回一个右值,而复合赋值返回对左操作数的引用。
6.操作符重载和友元关系
操作符定义为非成员函数时,通常必须将它们设置为所操作类的友元。
7.使用重载操作符
使用重载操作符的方式,与内置类型操作数上使用操作符的方式一样。
cout << item1 + item2 << endl;
这个表达式隐式调用为 Sales_items 类而定义的 operator+。
也可以像调用普通函数一样调用重载操作符函数,指定函数并传递适当类型适当数目的形参:
// equivalent direct call to nonmember operator function
cout << operator+(item1, item2) << endl;
调用成员操作符函数与调用任意其他函数是一样的:指定运行函数的对象,然后使用点或箭头操作符获取希望调用的函数,同时传递所需数目和类型的实参。
重载操作符的设计
重载逗号、取地址、逻辑与、逻辑或等操作符不是好的做法。
如果类有算术操作符或位操作符,那么应该提供相应的复合赋值操作符。
如果类定义了了相等操作符,也应该定义不等操作符。类定义了<,可能应该定义四个关系操作符(>,>=,<,<=)。
选择成员或非成员实现:
(1)赋值(=)、小标([])、调用(())和成员访问箭头(->)等操作符必须定义为成员,将这些操作符定义为非成员函数将在编译时标记为错误。
(2)像赋值一样,复合赋值操作符通常应该定义为类的成员。与赋值不同的是,不一定非得这样做,如果定义为非成员复合赋值操作符,不会出现编译错误。
(3)改变对象状态或与给定类型紧密联系的其他一些操作符,如自增、自减和解引用,通常应该定义为类成员。
(4)对称的操作符,如算数操作符、相等操作符、关系操作符和位操作符,最好定义为普通非成员函数。
14.2 输入和输出操作符
14.2.1 输出操作符<<的重载
为了与 IO 标准库一致,操作符应接受 ostream& 作为第一个形参,对类类型 const 对象的引用作为第二个形参,并返回对 ostream 形参的引用。
重载输出操作符一般的简单定义如下:
// general skeleton of the overloaded output operator
ostream&
operator <<(ostream& os, const ClassType &object)
{
// any special logic to prepare object
// actual output of members
os << // ...
// return ostream object
return os;
}
第一个形参是对 ostream 对象的引用,在该对象上将产生输出。
第二个形参一般应是对要输出的类类型的引用。
返回类型是一个 ostream 引用,
IO 操作符必须为非成员函数
14.2.2 输入操作符>>的重载
输入操作符的第一个形参是一个引用,指向它要读的流,并且返回的也是对同一个流的引用。它的第二个形参是对要读入的对象的非 const 引用,该形参必须为非 const,因为输入操作符的目的是将数据读到这个对象中。
输入和输出操作符有如下区别:输入操作符必须处理错误和文件结束的可能性。
Sales_item 的输入操作符如下:
istream&
operator>>(istream& in, Sales_item& s)
{
double price;
in >> s.isbn >> s.units_sold >> price;
// check that the inputs succeeded
if (in)
s.revenue = s.units_sold * price;
else
s = Sales_item(); // input failed: reset object to default state
return in;
}
1.输入期间的错误
可能发生的错误包括如下种类:
任何读操作都可能因为提供的值不正确而失败。
任何读入都可能碰到输入流中的文件结束或其他一些错误。
2.错误处理
尽可能的确定错误恢复措施。有的输入不仅要检查是否读入,还要检查读入的格式是否正确。
14.3 算术操作符和关系操作符
一般而言,将算术和关系操作符定义为非成员函数。
根据复合赋值操作符(如=+)来实现算术操作符(如+),比其他方式更简单且更有效。
14.3.1. 相等操作符
Sales_item 的相等操作符应比较 isbn 以及销售数据:
inline bool
operator==(const Sales_item &lhs, const Sales_item &rhs)
{
// must be made a friend of Sales_item
return lhs.units_sold == rhs.units_sold &&
lhs.revenue == rhs.revenue &&
lhs.same_isbn(rhs);
}
inline bool
operator!=(const Sales_item &lhs, const Sales_item &rhs)
{
return !(lhs == rhs); // != defined in terms of operator==
}
这些函数的定义并不重要,重要的是这些函数所包含的设计原则:
如果类定义了 == 操作符,该操作符的含义是两个对象包含同样的数据。
如果类具有一个操作,能确定该类型的两个对象是否相等,通常将该函数定义为 operator== 而不是创造命名函数。
如果类定义了 operator==,它也应该定义 operator!=。
相等和不操作符一般应该相互联系起来定义,让一个操作符完成比较对象的实际工作,而另一个操作符只是调用前者。
14.3.2关系操作符
定义了相等操作符的类一般也具有关系操作符。尤其是,因为关联容器和某些算法使用小于操作符,所以定义 operator< 可能相当有用。
关联容器以及某些算法,使用默认 < 操作符。一般而言,关系操作符,诸如相等操作符,应定义为非成员函数。
14.4 赋值操作符
可以为一个类定义许多附加的赋值操作符
赋值操作符必须定义为成员函数
赋值必须返回对 *this 的引用
string 赋值操作符返回 string 引用,这与内置类型的赋值一致。而且,因为赋值返回一个引用,就不需要创建和撤销结果的临时副本。返回值通常是左操作数的引用,例如,这是 Sales_item 复合赋值操作符的定义:
// assumes that both objects refer to the same isbn
Sales_item& Sales_item::operator+=(const Sales_item& rhs)
{
units_sold += rhs.units_sold;
revenue += rhs.revenue;
return *this;
}
一般而言,赋值操作符与复合赋值操作符应返回操作符的引用。
14.5 下标操作符
可以从容器中检索单个元素的容器类一般会定义下标操作符,即operator[]。标准库的类string和vector都是例子。
下标操作符必须定义为类成员函数。
类定义下标操作符时,一般需要定义两个版本:一个为非 const 成员并返回引用,另一个为 const 成员并返回 const 引用。
下面的类定义了下标操作符。为简单起见,假定 Foo 所保存的数据存储在一个 vector<int>: 中:
class Foo {
public:
int &operator[] (const size_t);
const int &operator[] (const size_t) const;
// other interface members
private:
vector<int> data;
// other member data and private utility functions
};
下标操作符本身可能看起来像这样:
int& Foo::operator[] (const size_t index)
{
return data[index]; // no range checking on index
}
const int& Foo::operator[] (const size_t index) const
{
return data[index]; // no range checking on index
}
14.6 成员访问操作符
为了支持指针型类,例如迭代器,C++ 语言允许重载解引用操作符(*)和箭头操作符(->))。
箭头操作符必须定义为类成员函数。解引用操作不要求定义为成员,但将它作为成员一般也是正确的。
class ScreenPtr {
public:
// constructor and copy control members as before
Screen &operator*() { return *ptr->sp; }
Screen *operator->() { return ptr->sp; }
const Screen &operator*() const { return *ptr->sp; }
const Screen *operator->() const { return ptr->sp; }
private:
ScrPtr *ptr; // points to use-counted ScrPtr class
}
箭头操作符与众不同。它可能表现得像二元操作符一样:接受一个对象和一个成员名。对对象解引用以获取成员。不管外表如何,箭头操作符不接受显式形参。
重载箭头操作符必须返回指向类类型的指针,或者返回定义了自己的箭头操作符的类类型对象。
14.7 自增操作符好自减操作符
因为这些操作符改变操作对象的状态,所以更倾向于将它们作为成员。
class CheckedPtr {
public:
CheckedPtr& operator++(); // prefix operators
CheckedPtr& operator--();
// other members as before
};
为了与内置类型一致,前缀式操作符应返回被增量或减量对象的引用。
CheckedPtr& CheckedPtr::operator++()
{
if (curr == end)
throw out_of_range
("increment past the end of CheckedPtr");
++curr; // advance current state
return *this;
}
后缀式操作符函数接受一个额外的(即,无用的)int 型形参。class CheckedPtr {
public:
// increment and decrement
CheckedPtr operator++(int); // postfix operators
CheckedPtr operator--(int);
// other members as before
};
后缀式操作符应返回旧值(即,尚未自增或自减的值),并且,应作为值返回,而不是返回引用。
后缀式操作符可以这样实现:
// postfix: increment/decrement object but return unchanged value
CheckedPtr CheckedPtr::operator++(int)
{
// no check needed here, the call to prefix increment will do the check
CheckedPtr ret(*this); // save current value
++*this; // advance one element, checking the increment
return ret; // return saved state
}
14.8 调用操作符和函数对象
(1) 函数调用操作符必须声明为成员函数。
(2)一个类可以定义函数调用操作符的多个版本,由形参的数目或类别加以区别
(3)定义了调用操作符的类,其对象常称为函数对象,即它们是行为类似函数的对象
14.8.1将函数对象用于标准库算法
函数对象经常用作通用算法的实参。
class GT_cls(){
public:
GT_cls(size_t val=0):bound(val){}
bool operator()(const string &s){ return s.size()>bound;}
private:
std::string::size_type bound;
};
cout<<count_if(words.begin(),words.end(),GT_cls(6))<<"words 6 characters or longer"<<endl;
14.8.2 标准库定义的函数对象
标准库定义了一组算术、关系与逻辑函数对象类,表14-3列出了这些类。标准库还定义了一组函数适配器,使我们能够特化或者扩展标准库所定义的以及自定义的函数对象类。这些标准库函数对象类型是在functional头文件中定义的。
表14-3 标准库函数对象
算术函数对象类型
plus<Type> +
minus<Type> -
multiplies<Type> *
divides<Type> /
modulus<Type> %
negate<Type> -
关系函数对象类型
equal_to<Type> ==
not_equal_to<Type> !=
greater<Type> >
greater_equal<Type> >=
less<Type> <
less_equal<Type> <=
逻辑函数对象类型
logical_and<Type> &&
logical_or<Type> |
logical_not<Type> !
1.每个类表示一个给定操作符
每个标准库函数对象类表示一个操作符,即,每个类都定义了应用命名操作的调用操作符。
不同的函数对象定义了执行不同操作的调用操作符。
有两个一元函数对象(unary function-object)类:一元减(negate<Type>)和逻辑非(logical_not<Type>)。其余的标准库函数对象都是表示二元操作符的二元函数对象(binary function-object)类。为二元操作符定义的调用操作符需要两个给定类型的形参,而一元函数对象类型定义了接受一个实参的调用操作符。
2.表示操作数类型的模板类型
每个函数对象类都是一个类模板,我们需要为该模板提供一个类型。
例如,plus<string>将string加法操作符应用于string对象,对于plus<int>,操作数是int值,plus<Sales_item>将+应用于Sales_item对象,依次类推:
plus<int> intAdd; //function object that can add two int values
negate<int> intNegate; //function object that can negate an int value
//uses intAdd::operator(int,int)
int sum=intAdd(10,20);
sum=intAddd(10,intNegate(10));
3.在算法中使用标准库函数对象
函数对象常用于覆盖算法使用的默认操作符。如果svec是一个vector<string>对象,以下代码
sort(svec.begin(),svec.end(),greater<string>());
实参是greater<string>类型的临时对象,是一个将>操作符应用与两个string操作数的函数对象。
14.8.3函数对象的函数适配器
标准库提供了一组函数适配器,用于特化和扩展一元和二元函数对象。函数适配器分为两类:
(1)绑定器,是一种函数适配器,它通过将一个操作数绑定到给定值而将二元函数对象转换为一元函数对象。
(2)求反器,是一种函数适配器,它将谓词函数对象的真值求反。
标准库定义了两个绑定器适配器:bind1st 和 bind2nd,每个绑定期接收一个函数对象和一个值,
bind1st 将给定值绑定到二元函数对象的第一个实参;bind2nd 将给定值绑定到二元函数对象的第二个实参;
例如:计算一个容器中所有小于或等于 10 的元素个数,可以这样给 count_if 传递值:
count_if(vec.begin(), vec.end(), bind2nd(less_equal<int>(), 10));
第三个参数使用函数适配器,该适配器返回一个函数对象,该对象用10作右操作数应用 <= 操作符,这个调用计算输入范围中小于或等于10的元素的个数
标准库还定义了两个求反器:not1 和 not2,
not1 将一元函数对象的真值求反, not2 将二元函数对象的真值求反
例如:对于less_equal函数对象的绑定求反,可以编写这样的代码:
count_if(vec.begin(), vec,end(), not1(bind2nd(less_equal<int>(), 10)));