重载操作符与转换

来源:互联网 发布:mac vim 命令大全 编辑:程序博客网 时间:2024/05/21 17:54

Ø  重载操作符的定义

重载操作符是具有特殊名称的函数:保留字operator后接需定义的操作符号。像任意其他函数一样,重载操作符具有返回类型和形参表,如下语句:

Sales_item  operator+(const Sales_item&, constSales_item&);

 

Ø  可以和不可以重载的操作符





 

Ø  重载操作符必须具有一个类类型操作数,这条规则强制重载操作符不能重新定义用于内置类型对象的操作符的含义。

Ø  操作符的优先级、结合性或操作数不能改变。

Ø  重载操作符并不保证操作数的求值顺序,尤其是,不会保证内置逻辑 AND、逻辑 OR和逗号操作符的操作数求值。在 && 和 ||的重载版本中,两个操作数都要进行求值(即不再短路求值),而且对操作数的求值顺序不做规定。

Ø  大多数重载操作符可以定义为普通非成员函数或类的成员函数。作为类成员的重载函数,其形参看起来比操作数数目少 1。作为成员函数的操作符有一个隐含的 this形参,限定为第一个操作数。

Ø  操作符定义为非成员函数时,通常必须将它们设置为所操作类的友元,才能访问类的私有部分。

Ø  使用重载操作符

v  cout<<item1+item2<<endl;            //隐式

v  cout<<operator+(item1, item2)<<endl;   //显式

 

v  item1 += item2;  ==  item1.operator+=(item2);

 

 

Ø  重载操作符的设计

²  不要重载具有内置含义的操作符

赋值操作符、取地址操作符和逗号操作符对类类型操作数有默认含义。如果没有特定重载版本,编译器就自己定义以下这些操作符。

•  合成赋值操作符进行逐个成员赋值:使用成员自己的赋值:使用成员自己的赋值操作依次对每个成员进行赋值,并返回对左操作数的引用(所有赋值操作符都是)

•  默认情况下,取地址操作符(&)和逗号操作符(,在类类型对象上的执行, 与在内置类型对象上的执行一样。 取地址操作符返回对象的内存地址,逗号操作符从左至右计算每个表达式的值,并返回最右边操作数的值。

•  内置逻辑与(&&)和逻辑或(||操作符使用短路求值。如果重新定义该操作符,将失去操作符的短路求值特征。

 

²  当一个重载操作符的含义不明显时,给操作取一个名字更好。对于很少用的操作,使用命名函数通常也比用操作符更好。如果不是普通操作,没有必要为简洁而使用操作符。

 

²  如果类定义了相等操作符,它也应该定义不等操作符 !=。类用户会假设如果可以进行相等比较,则也可以进行不等比较。同样的规则也应用于其他关系操作符。如果类定义了 <,则它可能应该定义全部的四个关系操作符(>,>=,<,<=)。

 

²  选择成员或非成员实现

 赋值(=)、下标([])、调用(())和成员访问箭头(->)等操作符必须定义为成员,将这些操作符定义为非成员函数将在编译时标记为错误。

 像赋值一样,复合赋值操作符通常应定义为类的成员,与赋值不同的是,不一定非得这样做,如果定义非成员复合赋值操作符,不会出现编译错误。

 改变对象状态或与给定类型紧密联系的其他一些操作符,如自增、自减和解引用,通常就定义为类成员。单目操作符一般也为类成员函数。

 对称的操作符,如算术操作符、相等操作符、关系操作符和位操作符,最好定义为普通非成员函数。

 输入输出操作符必须定义为非成员函数,并声明为友元。

 

Ø  输入输出<<,>>的重载

ostream& operator <<(ostream& os, const ClassType &object)

{

os << // ...

return os;

}

    第一个形参是对 ostream 对象的引用, 在该对象上将产生输出。ostream为非 const,因为写入到流会改变流的状态。该形参是一个引用,因为不能复制ostream对象。

    第二个形参一般应是对要输出的类类型的引用。该形参是一个引用以避免复制实参。它可以是 const,因为(一般而言)输出一个对象不应该改变对象。使形参成为 const 引用, 就可以使用同一个定义来输出 const 和非 const 对象。

    返回类型是一个 ostream 引用,它的值通常是输出操作符所操作的ostream 对象。

ps:输出操作符不应该输出换行符,如果该操作符输出换行符,则用户就不能将说明文字与对象输出在同一行上。尽量减少操作符所做格式化,让用户自己控制输出细节。

pps:IO操作符必须为非成员函数,且一般设置为友元函数。

 

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(); // input failed: resetobject to default state

return in;

}

ps:它的第二个形参是对要读入的对象的非const 引用,该形参必须为非 const,因为输入操作符的目的是将数据读到这个对象中。

 

更重要但通常重视不够的是,输入和输出操作符有如下区别:输入操作符必须处理错误和文件结束的可能性。可能发生的错误包括如下种类:

1. 任何读操作都可能因为提供的值不正确而失败。例如,读入 isbn之后,输入操作符将期望下两项是数值型数据。如果输入非数值型数据,这次的读入以及流的后续使用都将失败。

2. 任何读入都可能碰到输入流中的文件结束或其他一些错误。

我们无需检查每次读入,只在使用读入数据之前检查一次即可,如上的if判断就是的。

 

Ø  算术关系操作符重载

加法:

ps:对称的,一般为非成员函数。

Sales_itemoperator+(const Sales_item& lhs, const Sales_item& rhs)

{

Sales_item ret(lhs);

ret += rhs;      //调用+=重载操作符!!

return ret;

}

与内置操作符保持一致,其返回一个右值,而不是一个引用。(复合)赋值操作符返回一个引用。

 

相等:

inlinebool operator==(const Sales_item &lhs, const Sales_item &rhs)

{

returnlhs.units_sold == rhs.units_sold &&lhs.revenue == rhs.revenue&&lhs.same_isbn(rhs);

}

inlinebool operator!=(const Sales_item &lhs, const Sales_item &rhs)

{

return!(lhs == rhs);         // != defined interms of operator==

}

psfind算法需要==,所以如果类定义了==,则这些算法可以无须任何处理而用于该类型。

 

 

小于:

关联容器和某些算法使用小于操作符,所以定义 operator<可能相当有用。

inlinebool operator<(const Sales_item &lhs, const Sales_item &rhs)

{

return**;

}

inlinebool operator>=(const Sales_item &lhs, const Sales_item &rhs)

{

return  !(lhs<rhs);

}

inlinebool operator>(const Sales_item &lhs, const Sales_item &rhs)

{

return  rhs<lhs;

}

inlinebool operator<=(const Sales_item &lhs, const Sales_item &rhs)

{

return!(rhs<lhs);

}

 

Ø  赋值操作符

classstring {

public:

string& operator=(const string&);   // s1 = s2;

string& operator=(const char *);    // s1 = "str";

string& operator=(char);           // s1 = 'c';

// ....

};

 

ü  赋值操作符可以重载。无论形参为何种类型,赋值操作符必须定义为成员函数(=[],(),->),这一点与复合赋值操作符有所不同(一般也定义为成员函数)。\

ü  赋值必须返回对  *this的引用,(不论是赋值操作符还是复合赋值操作符)

Sales_item& Sales_item::operator+=(constSales_item& rhs)

{

units_sold += rhs.units_sold;

revenue += rhs.revenue;

return *this;

}

 

 

Ø  下标操作符[]

下标操作符必须定义为类成员函数。

    定义下标操作符比较复杂的地方在于,它在用作赋值的左右操作符数时都应该能表现正常。下标操作符出现在左边,必须生成左值,可以指定引用作为返回类型而得到左值。只要下标操作符返回引用,就可用作赋值的任意一方。

可以对 const 和非 const 对象使用下标也是个好主意。应用于 const 对象时,返回值应为 const 引用,因此不能用作赋值的目标。类定义下标操作符时,一般需要定义两个版本:一个为非 const成员并返回引用,另一个为 const成员并返回 const引用。

 

例子:

classFoo {

public:

int &operator[](const size_t);

const int&operator[] (const size_t) const;

private:

vector<int> data;

};

下标操作符本身可能看起来像这样:

int&Foo::operator[] (const size_t index)

{

return data[index];

}

constint& Foo::operator[] (const size_t index)const

{

return data[index];

}

ps:基于const的重载,而不是基于不用返回值类型!!const对象只能调用const成员函数,非const对象既可以调用const成员函数,也可以调用非const成员函数,但是优先调用非const版本。

 

Ø  成员访问操作符->

C++ 语言允许重载解引用操作符(*)和箭头操作符(->)。箭头操作符必须定义为类成员函数(=,[],(),->)。解引用操作(单目)不要求定义为成员,但将它作为成员一般也是正确的。

 

Ø  自增自减操作符

classCheckedPtr {

public:

CheckedPtr(int *b, int *e): beg(b),end(e), curr(b) { }

private:

int* beg; // pointer to beginning of thearray

int* end; // one past the end of the array

int* curr; // current position within thearray

};

C++ 语言不要求自增操作符或自减操作符一定作为类的成员,但是,因为这些操作符改变操作对象的状态,所以更倾向于将它们作为成员。

 

²  定义前自增/前自减操作符

classCheckedPtr {

public:

CheckedPtr& operator++();

CheckedPtr& operator--();  //为了与内置类型一致,返回值是被操作对象的引用!

};

CheckedPtr&CheckedPtr::operator++()

{

if (curr == end)

throw out_of_range("increment pastthe end of CheckedPtr");

++curr;

return *this;

}

CheckedPtr&CheckedPtr::operator--()

{

if (curr == beg)

throw out_of_range("decrement pastthe beginning of CheckedPtr");

--curr;

return *this;

}

 

区别操作符的前缀和后缀形式:为了解决这一问题,后缀式操作符函数接受一个额外的(即,无用的) int型形参。使用后缀式操作符进,编译器提供 0 作为这个形参的实参。尽管我们的前缀式操作符函数可以使用这个额外的形参,但通常不应该这样做。那个形参不

是后缀式操作符的正常工作所需要的,它的唯一目的是使后缀函数与前缀函数区别开来。

 

²  定义后缀式操作符

classCheckedPtr {

public:

CheckedPtr operator++(int);

CheckedPtr operator--(int);//后缀应该返回旧值,并且作为值返回而不是引用!

};

CheckedPtr CheckedPtr::operator++(int)

{

CheckedPtr ret(*this); // save current value

++*this;

return ret; // return saved state

}

CheckedPtr CheckedPtr::operator--(int)

{

CheckedPtr ret(*this); // save current value

--*this;

return ret; // return saved state

}

ps:因为通过调用前缀式版本实现这些操作符,不需要检查 curr 是否在范围之内,那个检查以及必要的 throw,在相应的前缀式操作符中完成。因为不使用 int形参,所以没有对其命名。


 

²  显式调用

parr.operator++(0);   // call postfix operator++

parr.operator++();    // call prefix operator++

 

所传递的值通常被忽略,但该值是必要的,用于通知编译器需要的是后缀式版本。

 

Ø  调用操作符()和函数对象

函数调用操作符必须声明为成员函数。一个类可以定义函数调用操作符的多个版本,由形参的数目或类型加以区别。定义了调用操作符的类,其对象常称为函数对象, 即它们是行为类似函数的对象。这种对象通常用于定义与标准算法结合使用的谓词函数。

boolGT6(const string &s)

{

returns.size() >= 6;

}

vector<string>::size_typewc = count_if(words.begin(), words.end(), GT6);

ps:这里的6被写死了,不好!

classGT_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;

};

for(size_t i = 0; i != 11; ++i)

cout<< count_if(words.begin(), words.end(),GT_cls(i))<< " words " << i<< "characters or longer" << endl;

ps:GT_cls(i)(const string &s)这样调用!

 

e.g. #include<iostream>

using namespace std;

class n{

public:

       voidoperator()(){

              cout<<"helloworld!"<<endl;

       }

};

int main(){

       n i;

       i();

       return 0;

}

 

Ø  类型转换

 

²  可用一个实参调用的 explicit 构造函数定义一个隐式转换。当提供了实参类型的对象而需要一个类类型的对象时,编译器将使用该转换。这种构造函数定义了到类类型的转换。除了定义到类类型的转换之外,我们还可以定义从该类类型到其他类型的转换。

 

²  转换为什么有用?

假定想要定义一个名为 SmallInt 的类,该类实现安全小整数,我们希望可以在混合模式表达式中使用这些操作符。例如,应该可以将两个 SmallInt 对象相加,也可以将任意算术类型加到 SmallInt。通过为每个操作符定义三个实例来达到目标:

intoperator+(int, const SmallInt&);

intoperator+(const SmallInt&, int);

SmallIntoperator+(const SmallInt&, const SmallInt&);

好处:转换减少所需操作符的数目,即使忽略浮点或大整型操作数的问题,如果要实现这个设计, 也必须定义 48个操作符!幸好,C++ 提供了一种机制,利用这种机制,一个类可以定义自己的转换,应用于其类类型对象。

 

如果存在一个到 int 的转换,则以下代码:

SmallIntsi(3);

si +3.14159; // convert si to int, then convert to double would be resolved by

可这样确定:

1. 将 si 转换为 int 值。

2. 将所得 int 结果转换为 double 值并与双精度字面值常量 3.14159 相加,得到 double 值。

 

²  转换操作符是一种特殊的类成员函数。它定义将类类型值转变为其他类型值的转换。转换操作符在类定义体内声明,在保留字 operator 之后跟着转换的目标类型(与操作符不同的是,必须有空格!)。

 

classSmallInt {

public:

SmallInt(int i = 0): val(i)

{ if (i < 0 || i > 255)

throw std::out_of_range("Bad SmallIntinitializer");

}

operator int() const {return val; }

private:

std::size_t val;

};

转换函数采用如下通用形式:

operator type();    //无返回值类型,或者type就是,且无参数。

注:type 表示内置类型名、类类型名或由类型别名定义的名字。对任何可作为函数返回类型的类型(除了 void 之外) 都可以定义转换函数。 一般而言,不允许转换为数组或函数类型,转换为指针类型(数据和函数指针)以及引用类型是可以的。

转换函数必须是成员函数,不能指定返回类型,并且形参表必须为空。另转换函数一般不应该改变被转换的对象。因此,转换操作符通常应定义为 const成员。如上!!

 

²  使用类类型转换

只要存在转换,编译器将在可以使用内置转换的地方自动调用它:

•  在表达式中:

SmallIntsi;

doubledval;

si >=dval

•  在条件中:

if (si) // si converted toint and then convert to bool

•  将实参传给函数或从函数返回值:

intcalc(int);

SmallIntsi;

int i =calc(si); // convert si to int and call calc

 作为重载操作符的操作数:

//convert si to int then call opeator<< on the int value

cout<< si << endl;

•  在显式类型转换中:

intival;

SmallIntsi = 3.541; //int往类类型转,隐式转换

ival =static_cast<int>(si) + 3; //instruct compiler to cast si to int

 

²  只能应用一个类类型转换,类类型转换之后不能再跟另一个类类型转换。如果需要多个类类型转换,则代码将出错。

 

²  类型转换的二义性

classSmallInt {

public:

SmallInt(int = 0);

SmallInt(double);

operator int() const { return val; }

operator double() const { return val; }

private:

std::size_t val;

};

ps:一般而言,给出一个类与两个内置类型之间的转换是不好的做法,在这里这样做是为了举例说明所包含的缺陷。

voidcompute(int);

voidfp_compute(double);

voidextended_compute(long double);

SmallIntsi;

compute(si);          //SmallInt::operator int() const

fp_compute(si);        //SmallInt::operator double() const

extended_compute(si);    // error: ambiguous

 

voidmanip(const SmallInt &);

doubled; int i; long l;

manip(d);          // ok: useSmallInt(double) to convert the argument

manip(i);           // ok: use SmallInt(int) to convert theargument

manip(l);            // error: ambiguous

 

当两个类定义了相互转换时,很可能存在二义性:

class Integral;

classSmallInt {

public:

SmallInt(Integral); // convert from Integralto SmallInt

};

classIntegral {

public:

operator SmallInt() const; // convert fromIntegral to SmallInt

};

voidcompute(SmallInt);

Integralint_val;

compute(int_val);         // error: ambiguous

实参 int_val 可以用两种不同方式转换为 SmallInt 对象,编译器可以使用接受 Integral 对象的构造函数,也可以使用将 Integral 对象转换为SmallInt 对象的类型转换操作。因为这两个函数没有高下之分,所以这个调用会出错。

compute(int_val.operator SmallInt()); // ok: use conversionoperator

compute(SmallInt(int_val)); // ok: use SmallInt constructor

 

²  重载操作符、转换

ClassXsc;

int iobj= sc + 3;

有四种可能性:

•  有一个重载的加操作符与  ClassX 和  int 相匹配。

•  存在转换,将  sc 向int 值转换。如果是这样,该表达式将先使用转换,接着应用适当的加操作符。

•  因为既定义了转换操作符又定义了  + 的重载版本,该表达式具有二义性。

•  因为既没有转换又没有重载的  + 可以使用,该表达式非法。

 

*****正确设计类的重载操作符、转换构造函数和类型转换函数需要多加小心。*****

 


0 0
原创粉丝点击