声明,所有的朋友,如果要转我的帖子,务必注明"作者:黑啤来源:CSDN博客"和 具体的网络地址http://blog.csdn.net/nx500/archive/2007/10/24/1842538.aspx,并且我的所有 博客内容,禁止任何形式的商业用途,请大家尊重我的劳动.谢谢!
目 录
十四.重载操作符与转换.
001 通过操作符重载,程序员能够针对类类型的操作数定义不同的操作符版本,允许程序使用表达式而不是命名函数,可以使编写和阅读程序容易得多.
002 重载操作符的定义.
不能重载的操作符.
:: .* . ?:
操作符重载必须至少有一个类类型或枚举类型的操作数,不能对内置数据类型重载操作符.
int operator+(int, int); // error, 内置类型.
操作符的优先级/结合性/操作数数目不能改变.除了函数调用operator()之外,重载操作符时使用默认实参是非法的.
x == y + z; 总是先计算 y + z,再将结果作为==的右操作数.
重载操作符并不保证操作数的求值顺序,尤其是,不会保证内置逻辑AND/OR/和逗号操作符的操作数求值顺序.
作为类成员的重载函数,操作符有一个隐含的this形参,限定为第一个操作数.重载一元操作符就没有显式形参.
一般而言,将算术和关系操作符定义为非成员函数,而将赋值操作符定义为成员.
操作符定义为非成员函数时,通常要将它们设置为所操作类的友元,因为操作符通常需要访问类的私有部分.
class Sales_item{
friend std::istream& operator>>(std::istream&, Sales_item&);
friend std::istream& operator<<(std::istream&, Sales_item&);
public:
Sales_items& operator+=(const Sales_item&);
};
Sales_item operator+(const Sales_item&, const Sales_item&);
重载操作符的使用.
cout << item1 + item2 << endl;
item1 += item2; // 隐式调用重载操作符.
item1.operator+=(item2); // 使用成员函数显式调用.
重载操作符的设计原则.
1.不要重载具有内置含义的操作符.赋值操作符/取地址操作符和逗号操作符对类类型操作数有默认含义,一般不要重载.编译器会自定义默认的重载操作符.
2.大多数操作符对类对象没有意义,根据类的公用接口,将功能相应的操作符映射到公用接口上,重载之.
3.如果一个类有算术运算或者是位操作符,就应该提供相应的复合赋值操作符.如重载了+,就应该重载+=.
4.对于要用作关联容器键类型的类,应重载<操作符和==操作符,如果类定义了==操作符,它也应该定义!=操作符,如果类定义了<操作符,则它可能应该定义四个关系操作符(> >= < <=).
5.成员实现还是非成员实现.
1)赋值=/下标[]/调用()/和成员箭头访问->操作符必须定义为成员,定义为非成员将出现编译错误.
2)和赋值一样,复合赋值操作符通常应定义为类的成员.
3)改变对象状态或与给定类型紧密联系的其他一些操作符,如自增/自减和解引用,通常定义为类成员.
4)对称的操作符,如算术操作符,相等操作符,关系操作符和位操作符,最好定义为普通非成员函数.
003 支持IO操作的类所提供的操作接口,一般应该与标准库iosteam为内置类型定义的接口相同,因此,许多类都需要提供重载IO操作.
为了与IO标准库一致,操作符应接受ostream&作为第一个形参,对类类型const对象的引用作为第二个形参,并返回const形参的引用.
第一个形参不能是const类型,因为IO操作会改变流的状态,第二个参数必须是引用类型以避免复制实参.
ostream& operator<<(ostream& os, const ClassType &obj){
os << //...;
return os;
}
// Sales_item输出操作符.
ostream& operator<<(ostream& out, const Sales_item& s){
out << s.isbn << "/t" << s.units_sold << "/t" << s.revenue << "/t" << s.avg_price();
return out;
}
输出操作符通常所做格式化应尽量少,一般的,输出操作符应输出对象的内容,进行最小限度的格式化,它们"不应该输出换行符".
不能将IO操作符定义为类的成员,否则,左操作数将只能是该类类型的对象.
// Sales_item的输入操作符.
istream& operator>>(istream& in, Sales_item& s){
double price;
in >> s.isbn >> s.units_sold >> price;
// 对于输入期间的错误,不需要逐一检验,只要在使用读入数据之前检查一次就可以了.
if (in)
s.revenue = s.units_sold * price;
// 如果输入操作符检测到输入失败了,则确保对象处于可用和一致的状态是必须的.
else
s = Sales_item();
return in;
}
// 除以上的错误处理外,其实还需要增加一些设置输入形参的条件状态,比如输入的isbn是否符合要求等.
004 算术操作符通常产生一个新值,该值是两个操作数的计算结果,它不同于任一操作数且在一个局部变量中计算,返回该值的引用会造成运行时的错误.
为了与内置操作符保持一致,加法返回一个右值,而不是一个引用.
即定义了算术操作符又定义了相关复合复制操作符的类,一般应使用复合赋值实现算术操作符.
Sales_item operator+(const Sales_item& lhs, const Sales_item& rhs){
Sales_item ret(lhs);
ret += rhs;
return ret;
}
005 相等操作符/不等操作符的设计原则.
如果类定义==操作符,该操作符的含义是两个对象包含同样的数据.
如果类需要一个操作,来确定该类型的两个对象是否相等,就应该将该函数定义为operator==.
如果类定义了operator==,它也应该定义operator!=.
相等和不等操作符一般应该相互联系起来定义,让一个操作符完成比较对象的实际工作,而另一个操作符只是调用前者.
// 必须预先声明操作符为Sales_item的友元函数.
inline bool operator==(const Sales_item &lhs, const Sales_item &rhs){
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);
}
006 定义了相等操作符的类一般也具有关系操作符.因为关联容器和某些算法使用小于操作符,所以定义operator<可能相当有用.
然而,如果<的逻辑定义与==的逻辑定义不一致,则根本不定义<会更好.
第十五节将会介绍想要将Sales_item对象存储到关联容器中时,怎样使用单独命名的函数来比较Sales_item对象.
007 赋值操作符.类赋值操作符必须是类的成员,以便编译器可以知道是否需要自动生成一个.
也可以为一个类定义许多附加的赋值操作符,这些赋值操作符会因右操作数类型的不同而不同.
例如标准库中string类的3个赋值操作符:分别接受const string&/const char*/char形参.
string car("Volks");
car = "studebaker";
string mode;
mode = 'T';
赋值操作符必须返回对*this的引用.
008 下标操作符.可以从容器中检索单个元素的容器类一般会定义下标操作符,即oprator[].
类定义下标操作符时,一般需要定义两个版本:一个为非const成员并返回引用,另一个为const成员并返回const引用.
// 假定Foo所保存的数据存储在一个vector<int>中.
class Foo{
public:
int& operator[](const size_t){
return data[index];
}
const int& operator[](const size_t) const{
return data[index];
}
private:
vector<int> data;
}
009 成员访问操作. 为了支持指针型类,例如迭代器,C++语言允许重载引用操作符(*)和箭头操作符(->).
解引用操作符和箭头操作符常用在实现智能指针的类中.
// 同上一节的HasPtr类一样,ScreenPtr类将对其指针进行使用计数.我们将定义一个伙伴类保存指针及其相关使用计数.
class ScrPtr{
friend class ScreenPtr;
Screen *sp;
size_t use;
ScrPtr(Screen *p):sp(p), use(1){}
~ScrPtr(){delete sp;}
};
// ScreenPtr类将管理使用计数.它没有默认构造函数,所以ScreenPtr类型的每个对象必须提供一个初始化参数.
class ScreenPtr{
public:
ScreenPtr(Screen *p):ptr(new ScrPtr(p)){}
ScreenPtr(const ScreenPtr &orig):ptr(orig.ptr){++ptr->use;}
ScreenPtr& operator=(const ScreenPtr&);
~ScreenPtr(){
if (0 == --ptr->use){
delete ptr;
}
}
// 定义解引用操作和箭头操作,注意返回左右值的情况,必须重载const和非const类两种.
Screen& operator*(){return *ptr->sp;}
const Screen& operator*() const{return *ptr->sp;}
// 箭头操作符表面上是个二元操作符,实际上不接受显式参数,因为->的右操作数不是表达式.
Screen* operator->(){return ptr->sp;}
const Screen* operator->() const{return ptr->sp;}
private:
ScrPtr *ptr;
};
ScreenPtr p(&myScreen);
p->display(cout);
注意重载箭头的使用.
point->action();
如果point是一个指针,指向具有名为action的成员的类对象,则编译器将代码编译为调用该对象的action成员.
否则,如果action是定义了operator->操作符的类的一个对象,则point->action与point.operator->()->action相同.
即,执行point的operator->(),然后使用该结果重复这三步.否则,代码出错.
重载箭头操作符必须返回指向类类型的指针,或者返回定义了自己的箭头操作符的类类型对象.
010 自增和自减操作符.这样的操作符经常由诸如迭代器这样的类实现,这样的类提供类似于指针的行为来访问序列中的元素.
C++语言不要求自增或自减操作符一定作为类的成员,但是,这些操作符改变操作对象的状态,所以更倾向于将它们作为成员.
class CheckedPtr{
public:
CheckedPtr(int *b, int *e):beg(b), end(e), curr(e){}
// 定义前自增/自减操作符
CheckedPtr& operator++(){
if (curr == end)
throw out_of_range("increment pass end of CheckedPtr");
++curr;
return *this;
}
CheckedPtr& operator--(){
if(curr == beg)
throw out_of_range("decrement pass the geginning of CheckedPtr");
--curr;
return *this;
}
// 后缀操作符函数接受一个额外的(无用的,所以不需要对其命名)int型形参.
CheckedPtr operator++(int){
CheckedPtr ret(*this);
++*this;
return ret;
}
checkedPtr operator--(int){
CheckedPtr ret(*this);
--*this;
return ret;
}
private:
int* beg;
int* end;
int* curr;
}
如果想显式的调用前缀操作符,必须给一个整形实参值.
CheckedPtr parr(ia, ia + size);
parr.operator++(0);
011 调用操作符和函数对象.调用操作符一般用于表示操作的类.
// 定义名为absInt结构,该结构封装int类型的值转换为绝对值的操作.
struct absInt{
int operator() (int val){
return val < 0 ? -val : val;
}
};
int i = -42;
absInt absObj;
unsigned int ui = absObj(i);
// unsigned int ui = absInt(i);
函数调用操作符必须声明为成员函数.一个类可以定义多个版本的函数调用操作符.
定义了调用操作符的类,其对象成为函数对象,即它们是行为类似函数的对象.
函数对象常用作通用算法的实参.
// 计算单词长度.
bool GT6(const string &s){
return s.size() >= 6;
}
vector<string>::size_type wc = count_if(words.begin(), words.end(), GT6);
// 上边函数的问题在于将6这个数字固化在GT6函数的定义中,理想情况下应传递string和我们想要的长度进行测试.
// 而count_if算法运行只用一个形参且返回bool的函数,如果将GT6定义为带函数调用成员的类,就可以传递长度参数了.
class GT_cls{
public:
GT_cls(size_val = 0):bound(val){}
bool operator()(const string &s){
return s.size() >= bound;
}
private:
std::string::size_type bound;
};
// 使用GT_cls类型的对象进行计算,用整形值6初始化这个对象,用调用操作符函数处理长度判断.
vector<string>::size_type wc = count_if(words.begin(), words.end(), GT_cls(6));
标准库定义的函数对象.
类型 函数对象 所应用操作符
算术函数对象类型 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> !
每个类表示一个操作符,例如muinus是乘法操作符的模板类型,模板中的调用操作符对一对Type类型的操作数执行*操作.
除了negate和logical_not是一元函数对象以外,其他都是二元函数对象.
使用标准库的函数对象.
// 一般用法.
plus<int> intAdd;
negate<int> intNegate;
int sum = intAdd(2, 30);
sum = intAdd(0, intNegage(10));
// 作为微词,在算法中使用使用.
sort(svec.begin(), svec.end(), greater<string>());
标准库还提供了一组函数对象的函数适配器,可以特化和扩展一元和二元函数对象.
绑定器,通过将一个操作数绑定到给定值而将二元函数对象转换为一元函数对象.bind1st将给定值绑定在二元函数的第一个对象.bind2nd类推.
求反器,将谓词函数对象的真值求反.not1将一元函数对象的真值求反,not2将二元函数对象的真值求反.
// 统计vector<int>容器中值大于8的元素个数.
count_if(ivec.begin(), ivec.end(), bind2nd(greater<int>, 8));
// 统计vector<int>容器中值不大于8的元素个数.
count_if(ivec.begin(), ivec.end(), not1(bind2nd(greater<int>, 8)));
012 转换与类类型.接受单个参数且未指定为explicit的构造函数定义了从其他类型到类类型的转换.重载操作符转换函数则定义了从类类型到其他类型的转换.
通过转换操作符可以有效的减少类重载算术操作符的数目.
转换操作符必须为所转换类的成员,没有形参并且不定义返回值,转换操作符返回操作符所具有类型的值.
operator type() const;
一般而言,不允许转换为数组或函数类型,转换为指针类型以及引用类型是可以的.转换一般不应该改变被转换的对象.
// 定义一个安全短整形,容纳0到255之间的数据,防止越界.
class SmallInt{
public:
SmallInt(int i = 0) : val(i) {
if(i < 0 || i > 255)
throw std::out_of_range("Bad SamllInt initializer");
}
// 转换操作符,将SmallInt类对象转换为int类型.
operator int() const {
return val;
}
private:
int val;
};
// 声明和使用对象
SmallInt si(9);
double deval(8.999);
si >= deval; // 编译器先将si转换为int类型对象,在转换为double,调用 >= 操作符比较两个对象.
if(si) // 编译器先将si转换为int类型对象,在转换为bool对象.
int val = static_cast<int>(si) + 3; // 显式类型转换.
使用转换函数时,被转换的类型不必与所需要的类型完全匹配.必要时可在类类型转换之后跟上标准转换以获得想要的类型.比如"si >= deval"中的方式.
但是类类型转换后不能在跟一个类类型转换.
class Integral{
public:
Integral(int i = 0) : val(i) {}
// 转换操作符,输出SmallInt类型对象
operator SmallInt() const { return val % 256; }
private:
std::size_t val;
};
int calc(int);
Integral intVal;
SmallInt si(intVal); // ok.
int i = calc(si); // ok.
int j = calc(intVal); // error, 只能转换一次到SmallInt类型,不能再继续使用SmallInt的int转换操作符了.
可以先标准转换,再接这类类型转换.
int calc(SmallInt);
short sobj;
calc(sobj); // sobj先转换为int,然后在转换为SmallInt对象.
类类型转换可能是编译时错误的一大来源,一般而言,不要为一个类做两个内置类型转换的成员函数.
两个构造函数也可能产生编译问题,导致二义性问题.要避免二义性,就要保证最多只有一种途径将一个类型转换为另一个类型,一种内置类型应该只有一个转换.
不要定义相互转换的类.如果定义了到算术类型的转换:则不要定义接受算术类型的操作符的重载版本,也不要定义转换到一个以上算术类型的转换.