首页 > 代码库 > 【C++ Primer】重载操作符与转换
【C++ Primer】重载操作符与转换
十四、重载操作符与转换
1. 重载操作符的定义
重载操作符必须具有至少一个类类型或枚举类型的操作数,这条规则强制重载操作符不能重新定义用于内置类型对象的操作符含义。
int operator +(int, int) // 错误,内置数据类型不能重载操作符
重载操作符,操作符的优先级、结合性或操作数数目不能改变。重载操作符并不保证操作数的求值顺序,不再具备短路求值特性,因此,重载&&、||、逗号操作符不是一种好的做法。除了函数调用操作符 operator()外,重载操作符时使用默认实参是非法的。
大多数重载操作符可以定义为普通非成员函数或类的成员函数。作为类的成员重载函数,其形参看起来比操作数数目少1,因为它有一个隐含的this形参,限定为第一个操作数。作为普通函数时,函数的第一、第二个参数分别为操作符的左右操作数。
重载一元操作符如果作为类成员函数就没有形参,而作为非成员函数就有一个形参。
操作符定义为非成员函数时,通常将它们定义为操作类的友元。在这种情况下,操作符通常需要访问类的私有部分。
使用重载操作符有两种方式:
若+操作符是类A的成员函数:
1:item1+item2;
2:item1.operator+(item2);
若+操作符是普通函数:
1:item1+item2;
2:operator+(item1,item2);
重载逗号、取地址、逻辑与、逻辑或等操作符通常不是好做法。这些操作符具有有用的内置含义,如果我们定义了自己的版本,就不能再使用这些内置含义。
将要用作关联容器键类型的类定义<操作符。关联容器默认使用键类型的<操作符。即使该类型将只存储在顺序容器中,类通常也应该定义相等(==)操作符和小于(<)操作符,理由是许多算法假定这些操作符存在。如果类定义了相等操作符,它也应该定义不等操作符!=。
下面是一些指导原则,有助于决定将操作符设置为类成员还是普通黑成员函数:
(1)赋值(=)、下标([ ])、调用( ( ))和成员访问箭头(->)等操作符必须定义为成员,将这些操作符定义为非成员函数将在编译时标记为错误。
(2)像赋值一样,复合赋值操作符通常应定义为类的成员。
(3)改变对象状态或与给定类型紧密联系的其他一些操作符,如自增、自减和解引用,通常应定义为类成员。
(4)对称的操作符,如算术操作符、相等操作符、关系操作符和位操作符,最好定义为普通非成员函数。
2. 输入和输出操作符
重载<<和>>时,IO操作符必须为非成员函数。因为操作符函数的参数顺序,就是操作符操作数的顺序。如果将其作为类的成员函数,那么左操作数必须为类类型的对象,形如item<<cout;这与正常使用相反。如果想要支持正常用法,左操作数必须为ostream类型。这意味着如果该操作符是类的成员,则它必须是ostream类的成员。但是ostream是标准库的一部分,我们不能为标准库添加成员。另外,ostream和istream必须为引用类型,因为流对象是不可以复制和赋值的。且返回值也为流对象的引用。
如:istream & operator>>(istream& in,A&s);
ostream& operator<<(ostream& out,A&s);
除了处理可能发生的错误之外,输入操作符可能还需要设置输入形参条件状态以指出失败。如failbit。
3. 各种操作符
加法操作符,为了与内置操作符保持一致要返回一个右值,而不是一个引用。算术操作符通常产生一个新值,是两个操作数计算的结果,它不同于任一操作数且在一个局部变量中计算,返回那个变量的引用是一个运行时错误。
与加法操作符相比,复合赋值操作+=效率更高,因为它不必创建和撤销一个临时变量来保存计算结果。
赋值操作符必须是类的成员函数,以便编译器知道是否需要合成一个。为了与内置类型一致,赋值操作符需要返回*this的引用。注意要防止自我赋值。
下标操作符同样必须为类的成员函数。它返回左值。可以定义const类型和非const类型的下标操作符函数。当被const对象调用时返回const引用。被非const调用时,返回非const引用。
重载箭头操作符时,如Screen* opeator->() (return ptr->sp;);
它可能表现的像二元操作符一样:接受一个对象和一个成员名。当通过箭头操作符进行调用时,如p->display(item);其实相当于(p.operator->())->display(item); p.operator->()的返回值调用display。这要特别注意哦。另外它必须返回执行类类型的指针或者返回定义了自己的箭头操作符的类类型的对象。
重载前置自增自减操作符的形式为:A&operator++();
后置为A opearator++(int); 因为没有使用int形参,所以没有为其命名,虽然在使用时可以为后置运算符提供额外的形参,但通常不这样做,这个形参不是后缀式正常工作所必需的,它唯一的作用是将前置与后置自增自减操作符区分开来。在一般使用中编译器根据运算符的位置,判断是使用前置还是后置运算符。但是当显式调用时就不能省略传递的int类型的形参了,如
item.operator++()//前置
item.operator++(0);//后置。
4. 调用操作符和函数对象
一般为表示操作的类重载调用操作符。如
class absint
{
public:
int operator()(int val)
{ return val<0?-val:val; }
};
absInt a; size_t b=a(-43);//相当于a.operator(-42);
尽管a是一个对象而非函数,我们仍然可以调用该对象。像这种定义了调用操作符的类,我们称之为函数对象。即它们是行为类似函数的对象。
函数对象通常用作标准库算法的实参。这是因为函数对象比函数更灵活,如count_if函数,它需要一对迭代器和一个函数。它要求的函数只能有一个形参且返回bool类型。该函数用于判断传入的字符串是否满足一定的要求,如长度是否大于10;理想情况下,我们能够将要判断的长度作为参数,传递给这个函数,但是由于它只有一个形参,用于传递字符串。因此我们会将10这个数字固化到程序中,灵活性大打折扣。
通过定义函数对象可以获得所需要的灵活性,因为字符串的长度可以保存在类的成员变量内。
如:
class GT_cls
{
public:
GT_cls(size_t val=0)
:bound(val){}
bool operator()(const string&s)
{return s.size()>=bound;}
private:
size_t bound;
};
count_if(words.begin(),words.end(),GT_cls(6));
这个count_if传递一个GT_cls类型的对象而不再是函数,GT_cls(6)将产生一个临时对象。这个count_if每次调用函数形参时,都会使用GT_cls的调用操作符。
标准库定义了一组算术、关系与逻辑函数对象类。除此之外还定义了函数适配器,使我们能够特化或者扩展标准库所定义的以及自定义的函数对象类。
每个函数对象类表示一个给定的操作符。即每个类都定义了应用命名操作的调用操作符。且每个函数对象类都是类模板,需要为该模板提供一个类型。这些类型是在functional头文件中定义的。
Arithmetic Function Objects Types 算术函数对象类型 | |
| applies applies applies applies applies applies |
Relational Function Objects Types 关系函数对象类型 | |
| applies applies applies applies applies applies |
Logical Function Object Types 逻辑函数对象类型 | |
| applies applies applies |
标准库提供了一组函数适配器,用于特化和扩展一元和二元函数对象。分为如下两类:
1:绑定器,有bind1st,bind2st,每个绑定器接受一个函数对象和一个值,它通过将一个操作数绑定到给定值而将二元函数对象转换为一元函数对象。bind1st将给定值绑定到二元函数对象的第一个实参,bind2st绑定到第二个实参。
count_if(vec.begin(), vec.end(),bind2nd(less_equal<int>(), 10));
传给 count_if
的第三个实参使用 bind2nd
函数适配器,该适配器返回一个函数对象,该对象用 10 作右操作数应用 <=
操作符。这个 count_if
调用计算输入范围中小于或等于 10 的元素的个数。
2:求反器,它将谓词函数对象的真值求反。not1将一元函数对象的真值求反,not2将二元函数对象的真值求反。
count_if(vec.begin(), vec.end(),not1(bind2nd(less_equal<int>(), 10)));
首先将 less_equal
对象的第二个操作数绑定到 10
,实际上是将该二元操作转换为一元操作。再用 not1
对操作的返回值求反,效果是测试每个元素是否 <=
。然后,对结果真值求反。这个 count_if
调用的效果是对不 <=
10 的那些元素进行计数
5. 转换与类类型
单参构造函数可以执行实参向类类型的隐式转换。除了这种转换之外,我们还可以定义从类类型到其他类型的转换。我们可以定义转换操作符,给定类类型的对象,这种操作符将产生其他类型的对象。
转换操作符是一种特殊的类成员函数,它定义将类类型的值转变为其他类型值的转换。转换操作符必须是类的成员函数,在保留字operator之后跟着转换的目标类型,这个目标类型就是函数的返回值,因此它不能指定返回类型,且形参表必须为空。因为本类就是它的参数。
如
class A
{
public:
A(){}
operator int() const
{
}
};
对任何可作为函数返回类型的类型都可以定义转换函数。一般而言,不允许转换为数组或函数类型,但可以转换为指针或引用类型。
转换函数一般不改变被转化的对象,因此通常被定义为const。
转化函数在以下条件下会被调用:
1:在表达式中,如A a; double d; a>=d;
2:在条件中,if(a)
3:将实参传给函数或从函数返回值。int cal(int); int i=cal(a);
4:作为重载函数的操作数。cout<<a<<endl;
5:在显式类型转换中。int i=static_cast<int>(a);
类类型在转换之后不能再跟另一个类类型转换。如
class B
{
public:
operator A()const
{
}
};
此类定义转换函数从B转换到A。
因此可以在需要A的地方使用B,在需要int的地方使用A,但是不能再需要int的地方使用B。因为从B到int需要两次转换,但是语言只允许一次类类型转换。所以这是错误的。
一般说来,给出一个类与两个内置类型之间的转换不是好的做法,可能导致二义性。
当两个类定义了相互转换时,很可能存在二义性。
如:
class A
{
public:
A(B b);
};
class B
{
public:
operator A()const
{
}
};
void func(A);
B b
func(b);
b可以用两种不同的方式转换为A的对象。编译器可以接受B类型对象的构造函数,也可以将调用B中的转换函数。这就有了二义性。为了消除这种情况,需要显式调用转换操作函数或构造函数。如
func(b.operator A());
func(A(b));
既为算术类型提供转换函数,又为同一类类型提供重载操作符,可能会导致重载操作符和内置操作符之间的二义性。