什么是运算符重载?
- 运算符重载是通过创建运算符函数实现的,运算符函数定义了重载的运算符将要进行的操作。运算符函数的定义与其他函数的定义类似,惟一的区别是运算符函数的函数名是由关键字operator和其后要重载的运算符符号构成的。运算符函数定义的一般格式如下:
<返回类型说明符> operator <运算符符号>(<参数表>)
{
<函数体>
}
为什么要重载运算符?
- C++中预定义的运算符的操作对象只能是基本数据类型。但实际上,对于许多用户自定义类型(例如类),也需要类似的运算操作。这时就必须在C++中重新定义这些运算符,赋予已有运算符新的功能,使它能够用于特定类型执行特定的操作。运算符重载的实质是函数重载,它提供了C++的可扩展性,也是C++最吸引人的特性之一。
比如,我们定义两个string类对象a,b后我们之所以可以使用+运算,是因为string类重载了+运算符。
我们应该了解哪些运算符可以被重载,哪些运算符不能被重载
可以被重载的运算符 :
算术运算符:+ , - , * , / , % , ++ , –
位操作运算符:& , | , ~ , ^ , << , >>
逻辑运算符:! , && , ||
比较运算符:< , > , >= , <= , == , !=
赋值运算符:= , += , -= , *= , /= , %= , &= , |= , ^= , <<= , >>=
其他运算符:[] , () , -> , ,(逗号运算符) , new , delete , new[] , delete[] , ->*下列运算符不允许重载:
. , .* , :: , ?: ,siezof
如何重载运算符?
运算符函数重载一般有两种形式:重载为类的成员函数和重载为类的非成员函数。非成员函数通常是友元。
如果一个运算符函数是成员函数,则它的第一个(左侧)运算对象绑定到隐式的this指针上,因此,成员运算符函数的(显式)参数数量比运算符的运算对象总少一个(后置单目运算符除外,后面会介绍到)。
class A
{
private:
int a;
public:
A operator+(A&);
A()=default;
A(int x):a(x){}
};
A A::operator+(A& d)
{
return A(a+d.a);
}
int main()
{
A b,c,d;
d=b+c; //等价于d=b.operator+(c);
return 0;
}
当运算符函数是非成员函数时,函数的参数与该运算符作用的运算对象数量一样多。
class A
{
private:
int a;
public:
friend A operator+(A&,A&);
A()=default;
A(int x):a(x){}
};
A operator+(A& c,A& d)
{
return A(c.a+d.a);
}
int main()
{
A b,c,d;
d=b+c; //等价于d=operator+(b,c);
return 0;
}
两种重载形式的比较:
在多数情况下,将运算符重载为类的成员函数和类的友元函数都是可以的。但成员函数运算符与友元函数运算符也具有各自的一些特点:
- 赋值(=),下标([]),调用( ( ) ),和成员访问箭头(->)运算符必须是成员。
- 复合赋值运算符一般来说应该是成员,但并非必须,这一点与赋值运算符略有不同。
- 改变对象状态的运算符或者与给定类型密切相关的运算符,如递增,递减,和解引用运算符,通常应该是成员。
- 具有对称性的运算符可能转换任意一端的运算对象,例如算术,相等性,关系和位运算符等,因此它们通常应该是普通的非成员函数。
注意事项:
- 对于一个运算符函数来说,它或者是类的成员,或者至少含有一个类类型的参数
int operator+(int ,int); //error,不能为int重定义内置的运算符 - 重载运算符限制在C++语言中已有的运算符范围内的允许重载的运算符之中,不能创建新的运算符。
- 重载之后的运算符不能改变运算符的优先级和结合性,也不能改变运算符操作数的个数及语法结构。
- &&和||运算符的重载版本无法保留内置运算符的短路求值属性,两个运算对象总是会被求值。因此不建议重载它们。
- 通常情况下,不应该重载逗号,取地址,逻辑与和逻辑或运算符。
- 使用与内置类型一致的含义。重载运算符的返回类型通常情况下应该与其内置版本的返回类型兼容:逻辑运算符和关系运算符应该返回bool,算术运算符应该返回一个类类型的值,赋值运算符和复合赋值运算符应该返回左侧运算对象的一个引用。
几种特殊运算符的重载举例:
重载输出运算符<<
- 通常情况下,输出运算符的第一形参是一个非常量ostream对象的引用。之所以ostream是非常量是因为向流写入内容会改变其状态;而该形参是引用是因为我们无法直接复制一个ostream对象。
- 第二个形参一般来说是一个常量的引用,该常量是我们想要打印的类类型。第二个形参是引用的原因是我们希望避免复制实参;而之所以该形参可以是常量是因为(通常情况下)打印对象不会改变对象的内容。
- 为了与其他输出运算符保持一致,operator<<一般要返回它的ostream形参。
- 输出运算符尽量减少格式化的操作,可以使用户有权控制输出细节
- 输出运算符必须是非成员函数,原因很简单。如果输出运算符函数是类的成员函数的话,它们的左侧运算对象将是我们类的一个对象。
Sales_data data;
data << cout; //如果operator<< 是Sales_data的成员,与我们正常的用法不符
当然,IO运算符通常需要读写类的非公有数据成员,所以IO运算符一般被声明为友元函数。
重载<<举例
ostream &operator<<(ostream &os,const Sales_data &item)
{
os<<item.isbn()<<" "<<item.units_sold<<" "<<item.revenue<<" "<<item.avg_price();
return os;
}
重载输入运算符>>
- 该运算符通常会返回某个给定流的引用。
- 输入运算符的第一个形参是运算符将要读取的流的引用。
- 第二个形参是将要读入到的(非常量)对象的引用。第二个形参之所以必须是一个非常量是因为输入运算符本身的目的就是将数据读入到这个对象中。
- 输入运算符必须处理输入可能失败的情况,而输出运算符不需要。
重载>>举例
istream &operator>>(istream &is, Sales_data &item)
{
double price;
is>>item.bookNo>>item.units_sold>>price;
if(is) //检查输入是否成功
item.revenue = item.units_sold * price;
else
item=Sales_data(); //输入失败:对象被赋予默认的状态
return is;
}
重载递增和递减运算符
- c++语言并不要求递增和递减运算符必须是类的成员,但是因为它们改变的正好是所操作对象的状态,所以建议将其设定为成员函数。
- 定义递增和递减运算符的类应该同时定义前置版本和后置版本
- 为了区分运算符到底是前置还是后置运算符,后置版本接受一个额外的(不被使用)int类型的形参。使用后置运算符时,编译器为这个形参提供一个值为0的实参。
这个形参的唯一作用就是区分前置版本和后置版本的函数。 - 为了与内置版本保持一致,前置运算符应该返回递增或递减后对象的引用。后置运算符应该返回对象的原值,返回形式是一个值而非引用。
class StrBlobPtr
{
public:
StrBlobPtr& operator++(); //前置++运算符
StrBlobPtr& operator--(); //前置--
StrBlobPtr& operator++(int); //后置++运算符,因为我们不会用到int形参,所以无须为其命名
StrBlobPtr& operator--(int); //后置--运算符
};
int main()
{
StrBlobPtr p;
++p; //调用前置版本operator++
--p; //调用前置版本operator--
p++; //调用后置版本operator++
p--; //调用后置版本operator--
p.operator++(); //显式调用前置版本的operator++
p.operator++(0); //显式调用后置版本的operator++
}
函数调用运算符
如果类重载了函数调用运算符,则我们可以像使用函数一样使用该类的对象。
如果类定义了调用运算符,则该类的对象成为函数对象。
函数调用运算符必须是成员函数。一个类可以定义多个不同版本的调用运算符,相互之间应该在参数数量或类型上有所区别。
举个简单的例子,下面这个名为absInt的struct含有一个调用运算符,该运算符负责返回其参数的绝对值:
struct absInt{
int operator()(int val) const
{
return val<0?-val:val;
}
};
这个类只定义了一种操作:函数调用运算符,它负责接受一个int类型的实参,然后返回该实参的绝对值。
int i=-42;
absInt absObj; //含有函数调用运算符的对象
int ui=absObj(i); //将i传递给absObj.operator()
类型转换运算符
类型转换运算符是类的一种特殊成员函数,它负责将一个类类型的值转换成其他类型。
类型转换函数的一般形式如下:
operator type() const;
其中type表示某种类型。
- 一个类型转换函数必须是类的成员函数;它不能声明返回类型,形参列表也必须为空。类型转换函数通常应该是const。
- 因为类型转换运算符是隐式执行的,所以不能在类型转换运算符的定义中使用任何形参。尽管类型转换函数不负责指定返回类型,但实际上每个类型转换函数都会返回一个对应类型的值。
举个例子,我们定义一个比较简单的类,令其表示0-255之间的一个整数:
class SmallInt {
public:
SmallInt(int i=0):val(i)
{
if(i<0 || i> 255)
throw std::out_of_range("Bad SmallInt value");
}
operator int() const
{ return val;}
private:
std::size_t val;
};
int main()
{
SmallInt si;
si=4; //将4隐式地转换成SmallInt
si+3; //将si隐式的转换成int
}