第十四章:重载操作符与转换
重载操作符必须具有至少一个类类型或枚举类型的操作数,这条规则强制重载操作符不能重新定义用于内置类型对象的操作符含义。
重载操作符时使用默认实参是非法的。
大多数重载操作符可以定义为普通非成员函数或类的成员函数。作为类的成员重载函数,其形参看起来比操作数数目少1,因为它有一个隐含的this形参,限定为第一个操作数。作为普通函数时,函数的第一、第二个参数分别为操作符的左右操作数。
重载一元操作符如果作为类成员函数就没有形参,而作为非成员函数就有一个形参。
操作符定义为非成员函数时,通常将它们定义为操作类的友元。在这种情况下,操作符通常需要访问类的私有部分。
使用重载操作符有两种方式:
若+操作符是类A的成员函数:
1:item1+item2;
2:item1.operator+(iterm2);
若+操作符是普通函数:
1:item1+item2;
2:operator+(item1,item2);
赋值操作符、取地址操作符和逗号操作符在类类型操作数有默认含义。如果没有特定的重载版本,编译器就执行默认的操作。
当内置操作符与类型上的操作存在逻辑对应关系时,重载操作符最有用。使用重载操作符不是创造命名操作,可以令程序更自然。
将要用作关联容器键类型的类应当定义<操作符,关联容器默认使用键类型的<操作符。许多算法假定这些操作符存在,如find使用==操作符,sort算法使用<操作符。
赋值=,下标[],调用(),成员访问->等操作符,必须定义为类的成员函数,将它们定义为非成员函数将在编译时标记为错误。
重载<<和>>时,IO操作符必须为非成员函数。因为操作符函数的参数顺序,就是操作符操作数的顺序。如果将其作为类的成员函数,那么左操作数必须为类类型的对象,形如item<<cout;这与正常使用相反。如果想要支持正常用法,左操作数必须为ostream类型。这意味着如果该操作符是类的成员,则它必须是ostream类的成员。但是ostream是标准库的一部分,我们不能为标准库添加成员。另外,ostream和istream必须为引用类型,因为流对象是不可以复制和赋值的。且返回值也为流对象的引用。
如:istream & operator>>(istream& in,A&s);
ostream& operator<<(ostream& out,A&s);
除了处理可能发生的错误之外,输入操作符可能还需要设置输入形参条件状态以指出失败。如failbit。
加法操作符,为了与内置操作符保持一致要返回一个右值,而不是一个引用。算术操作符通常产生一个新值,是两个操作数计算的结果,它不同于任一操作数且在一个局部变量中计算,返回那个变量的引用是一个运行时错误。与加法操作符相比,复合赋值操作+=效率更高,因为它不必创建和撤销一个临时变量来保存计算结果。
赋值操作符必须是类的成员函数,以便编译器知道是否需要合成一个。为了与内置类型一致,赋值操作符需要返回*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);//后置。
可以为类类型的对象重载函数调用操作符。
一般为表示操作的类重载调用操作符。如
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的调用操作符。
标准库定义了一组算术、关系与逻辑函数对象类。除此之外还定义了函数适配器,使我们能够特化或者扩展标准库所定义的以及自定义的函数对象类。
每个函数对象类表示一个给定的操作符。即每个类都定义了应用命名操作的调用操作符。且每个函数对象类都是类模板,需要为该模板提供一个类型。
标准库提供了一组函数适配器,用于特化和扩展一元和二元函数对象。分为如下两类:
1:绑定器,有bind1st,bind2st,每个绑定器接受一个函数对象和一个值,bind1st将给定值绑定到二元函数对象的第一个实参,bind2st绑定到第二个实参。
2:求反器,它将谓词函数对象的真值求反。
关于函数对象此处不再介绍。具体细节参考《C++primer》P451。
接下来介绍一个很有意思的知识点。偶尔在论坛上见过但是当时很疑惑,因为《C++大学教程》上没讲,从来没见过这种形式,今天终于可以揭开庐山真面目了。
单参构造函数可以执行实参向类类型的隐式转换。除了这种转换之外,我们还可以定义从类类型到其他类型的转换。我们可以定义转换操作符,给定类类型的对象,这种操作符将产生其他类型的对象。
转换操作符是一种特殊的类成员函数,它定义将类类型的值转变为其他类型值得转换。转换操作符必须是类的成员函数,在保留字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));
另外一个错误:
class A
{
public:
A(B);
operator B()const
{
}
};
A a;
A f1(a);
原意是拷贝a,结果是将a隐式转换为B后在调用构造函数创建临时对象,然户在调用复制构造函数。
《C++primer》P461介绍了使用转换操作符,避免错误的一些方法、原则。由于刚刚接触转换操作符,高级用法暂时不会用到,在此作一记号,以后需要的时候可以在查阅。