操作重载与类型转换
14.操作重载与类型转换
运算符在某种程度上也是一种函数,它是一种名为跟在关键字operator后的符号的函数,它也有返回值、参数列表、函数体,运算符的重载必须满足一下两个条件中的一个:1、是某个类的成员函数2、它的参数中有某种类类型。因此是不允许对内置类型进行运算符重载的,另外大部分的运算符都是可以重载的,但是也有四到五个不能被重载。注意:作为成员函数的运算符,this指针绑定到运算符的左侧对象,其(显式)参数数量比运算对象少一个。
注:通常情况下不推荐重载“逗号、取地址、逻辑与、逻辑或”运算符。
输入与输出运算符
重载输出运算符
一般情况下,一个输出运算符的第一个参数是一个非常量的ostream引用,第二个参数是一个常量的类类型引用,返回值是一个ostream的形参,一个实际的例子如下:
ostream operator<<(ostream& os,const Sales_data & is){
os<<is.isbn<<" "<<is.name<<" "<<is.price;
return os;
}
输出运算符应该和内置类型一样不打印换行符,具体的格式控制应该由用户自己控制.输入输出运算符必须是非成员函数。由于IO运算符一般情况下需要读写类的非公有数据成员,因此一般将它们声明为友元。
输入运算符的第一个形参是一个非常量输入流的引用,第二个形参是一个非常量的类类型的引用,返回值应该是某个给定流的引用。一个简单的例子:
std::istream& operator>>(std::istream& is,Sales_data& item){
is>>item.number>>item.price;
//必须要有的流错误预防措施,输入流有可能出现错误
if(is)
is.revenue=is.number*is.price;
else
item=Sales_data();
return is;
}
算术和关系运算符
赋值运算符
赋值运算符应返回其左侧对象的引用,赋值运算符必须定义为成员函数。复合赋值运算符也尽量这样做,一个例子:
//this指针隐式绑定到运算符的左侧对象
Sales_data& operator+=(const Sales_data& rhs){
units_sold += rhs.units_sold;
revenue += rhs.revenue;
return *this;
下标运算符
注意有两类。一类返回的是非常量引用可以放在赋值符号的两侧,一种是常量引用不能再被赋值。
递增递减运算符
一般如果在一个类中定义了前置的递增/递减运算符,也要定义相应的后置版本,对于前置版本,需要返回结果的一个引用,一个例子如下:
StrBlob& operator++(){
//需要检查curr是否已经到达容器的尾部
check(curr,"increment past end of StrBlob");
++curr;
return *this;
}
StrBlob& operator--(){
--curr;
//如果curr已经是0,再递减会出现异常,下面check会报错
check(curr,"decrement past begin of StrBlob");
return *this;
}
后置版本的则需要返回一个数值,而不是一个引用,例子如下:
//为了区分前置和后置,在声明后置时有一个不使用的int型的形参,仅做区分使用
StrBlob operator++(int){
StrBlob ret=*this;//记录当前值。
++curr;//有效性检查在此
return ret;
}
函数调用运算符
如果一个类或者结构体重载了函数调用运算符,那么我们可以像调用函数一样调用对象,一个具体例子如下:
struct abcInt{
int operator()(int val){
return val<0? -val:val;
}
//具体使用
int i=-1;
abcInt abc;
//输出i的绝对值
cout<<abc(i)<<endl;
虽然abc仅仅是一个对象但在使用时却像调用了一个函数一样,函数调用运算符必须是类的成员函数。另外,一个类可以定义多个函数调用符但是在返回值和参数数量上需要有所区分。
- lambda是函数对象
- 标准库定义的函数对象:标准库定义了一组表示算术运算符、关系运算符、逻辑运算符的类,每个类分别定义了一个执行命名操作的调用运算符,这些类都被定义成了模板的形式以供使用,比如plus表示对int对象执行加法操作,或者plus等。一个使用的小例子:
plus<int> intAdd;
int sum=intAdd(10,30);//等价于sum=30;
其余类似的一些操作如下表:
在标准库算法中使用标准库函数对象:比如对于sort算法,在默认情况下对于存储了string的vector进行排序时,使用的是默认的<按升序排列,但是如果我们想要按照降序排列时就可以传入一个greater进行修改默认的比较符,sort(svec.begin(),svec.end(),greater<string>())
.
另外需要特别注意的一点是,不允许对指针进行比较,会产生未定义的行为,但是标准库中允许比较指针的地址,进行sort:
vector<string *> nameTable;
sort(nameTable.begin(),nameTable.end(),less<string*>());
- 可调用对象与function:
可用的可调用对象包括函数、函数指针、lambda表达式、bind创建的对象以及重载了调用运算符的类,几者可能有相同的调用形式,比如如下例子:
int add(int i,int j){return i+j;};
//lambda表达式
auto mod=[](int i,int j){return i%j;};
//结构体,函数对象类
struct divide{
int operator()(int denominator,int divisor){
return denominator/divisor;
}
//三者的调用形式都是int(int,int)
可以考虑在一个map中存储这些可调用对象,进行计算,形如map<string,int(*)(int,int)> bindops,bindops.insert({"+",add});但是不能将mod存入,因为mod不是函数指针。为了能有一种统一的格式,可以使用标准库类型function,function存储在functional头文件中。可以声明为如下形式:
function<int(int,int)> f1=add;
重新声明map为map<string,function<int(int,int)>> binops,我们可以将所有的调用对象加入到该map中,只要他们的调用类型统一是int(int,int)形式的即可。
std::map<std::string, std::function<int(int, int)>> binops =
{
{"+", add}, // function pointer
{"-", std::minus<int>()}, // library functor
{"/", wy_div()}, // user-defined functor
{"*", [] (int i, int j) { return i*j; }}, // unnamed lambda
{"%", mod} // named lambda object
};
我们不能将重载的函数名放入function中,因为无法进行区分,最好是使用函数指针进行指定,将相应的函数指针放入function中。
重载、类型转换与运算符
类型转换运算符
为了防止隐式的类型转换被混乱使用,有时可以使用关键字explicit来进行标注,这样编译器就不会自动调用该显式的类型转换运算符进行转换,除非显式的使用static_cast进行强制的转换。如下:
class smallInt{
public:
//其他不变
explicit operator int() const{return val;}
};
smallInt si=3;//正确,smallInt的构造函数不是显式的
si+3; //错误:此处需要隐式的类型转换,但是类的运算符是显式的
static_cast<int>(si)+3;//正确:显式的请求类型转换。