C++primer第五版第14章笔记
重载运算与类型转换
当运算符作用于类类型时,可以重载相关运算符使程序更易理解、修改。
14.1基本概念
和普通函数一样,重载运算符也包含返回类型、参数列表、函数体,函数名为operator加上需要重载的符号,重载运算符的参数数量与需要运算的对象一样多,如果为成员函数,this指针则隐式的绑定到左侧运算对象,所以会少一个参数。
我们不能重定义内置类型的运算符,我们只能重载已有的运算符,我们将运算符作用于正确的运算类型隐式的使用,或者通过像普通函数一样显示调用:
data1+data2;//data是重载+的函数类型,隐式使用
operator+(data1,data2);//显示使用+的函数
有些运算符不应该被重载,因为求值顺序可能不会按照我们想要的顺序来使用,比如&&和||会直接使用两侧对象。我们重载的运算符应该与内置类型的运算符功能一样。
我们重载运算符时需要判断是否要定义成成员函数,如果定义成成员函数,左侧运算对象必须为类类型。
14.2输入和输出运算符
我们有时需要重载<<>>来支持类类型操作
14.2.1重载运算符<<
通常情况下第一个参数是非常量ostream的引用,第二个形参为一个常量的引用,并且一般要返回它的ostream形参。
ostream &operator<<(ostream &os,const strvec&);
输入输出必须为非成员函数,因为如果是成员函数,我们必须用左侧对象使用它,我们不能给类类型对象进行<<赋值。
14.2.2重载运算符>>
输入运算符第一个参数为读取流的引用,第二个参数为非常量的对象的引用,返回一个读取流的引用。
对于输入运算符我们一般需要判断输入是否正确。
istream &operator>>(istream &is,strvec &item){
is>>iteam.data;
if(is) //检查输入流是否正确
return is;
else
iteam.data=nullptr;//如果不正确,我们将对象的成员置为安全状态
return is;
}
14…3算数和关系运算符
通常情况下我们将算数运算符定义成非成员函数,以此支持左侧对象和右侧对象的转换,通常我们返回一个局部值,不改变对象状态,所以一般参数为const &类型。
14.3.1相等运算符
一般我们通过定义==运算符来定义类的两个对象是否相等,注意返回类型应该是bool类型。
14.3.2关系运算符
我们可以定义<运算符来按我们的方式比较两个对象的大小。
14.4赋值运算符
除了之前定义过的拷贝赋值和移动赋值运算符,还可以定义第三种赋值方法,如:
vector<string>s={"asd","asda"};
定义方式如下:
strvec &operator=(initializer_list<string>);
函数返回左侧对象的引用,并且和拷贝和移动赋值运算符一样,必须释放原有空间,再开辟新的空间。
复合赋值运算符不一定是类的成员,但是为了与内置类型保持一致,一般也设为成员函数。
14.5下标运算符
表示容器的类可以通过下标运算符访问元素,但必须定义成员函数,而且通常我们返回访问元素的引用,返回一个左值可以使下标出现在赋值符任意一端,通常我们定义两个const和非const两个版本,返回const的版本不能进行赋值。
14.6递增和递减运算
可以重载*和->运算符,箭头运算符必须定义为成员函数,解引用运算符一般也定义为成员函数,解引用运算符一般返回访问元素的引用,而箭头运算符不执行任何操作,而是调用解引用运算符并返回一个解引用结果元素的地址(指针)或者定义了->运算符的类对象。
(*point).mem;
point.operator->mem;
两种方式是等价的,箭头调用解引用访问mem
14.8函数调用运算符
若类重载了函数调用运算符,则可以像使用函数一样使用类的对象,同时类也能存储状态,比函数更加灵活。
struct abi{
int operator()(int val)const{
return val<0?-val:val;
}
}
abi test;//含有函数调用运算符的对象
int a=test(1);//将1传给abi.operator();
我们称这些具有调用运算符的类的对象为函数对象,因为像使用函数一样使用重载的调用符。
某些函数对象类具有初始状态,如:
class printstring{
public:
printstring(ostream &o=cout,chat c=' '):
os(o),sep(c){}
void operator()(const string &s){os<<s<<sep;};
private:
ostream &os;
char sep;
}
如代码所示,此类在构造函数中具有初始状态,我们的函数调用运算符使用这些数据成员。
函数对象也常常作为算法的实参,如:
for_each(vs.begin(),vs.end(),printstring(cerr,'\n'));//此函数将每一个元素都传递给函数调用运算符
14.8.1 lambda是函数对象
我们在上面使用printstring对象作为for_each的实参,与lambda表达式类似,编译器将其翻译成未命名类的未命名对象,转化如:
stable_sort(word.begin(),word.end(),[](const string &a,const string &b){
return a.size()<b.size();
});
class shorterstring{
bool operator()(const string &a,const string &b){
return a.size()<b.size();
}
};
stable_sort(word.begin(),word.end(),shorterstring());//lambda表达式相当于建立一个只有()函数调用运算符的类,再调用它
当lambda表达式通过引用捕获变量时,由程序确保变量存在。如果我们通过值捕获的方式,则创建的类行为,相当于建立相应的数据成员,再由构造函数进行赋值,最后在()运算符表达式中使用。
14.8.2标准库定义的函数对象
标准库定义了一组表示算术运算符、关系运算符和逻辑运算符的类,每个类分别定义了一个命名操作的调用运算符。我们可以为这些被定义成模板的类添加元素类型,如:
plus<int>intadd;
int sum=intadd(10,20);//sum=30
我们可以在算法中使用标准库函数对象。直接使用两个指针比较是产生未定义行为的,可以使用标准库函数对象使之合法。关联容器使用less<>排序,因此可以使用map或set来对指针进行排序,不用再定义less<>。
sort(nums.begin(),nums.end(),less<string*>());
14.8.3可调用对象于function
C++可调用对象包括:函数、函数指针、lambda表达式、bind创建的对象和重载了函数调用运算符的类。两个不同类型的可调用对象可能具有一种调用形式,调用形式指明了返回的类型和传入实参的类型,如:
int add(int i,int j){return i+j;}
auto mode=[](int i,int j){return i%j;}
struct{
int operator()(int d,int divisor){
return d/divisor;
}
}
这些调用对象实现的功能不同,但调用形式相同:int(int,int)
我们可以构建一个map用来存储字符和对象指针
map<string,int(*)(int,int)>hash;
hash.insert({"+",add});//add是一个函数指针
hash.insert({"-",mode})//mode不是一个函数指针
我们可以使用function模板来解决这个问题
function<int,(int,int)> f1=add;//前面function部分指明这个接收一个返回int,参数类型为int和int的可调用对象
map<string,function<int(int,int)>>hash;
hash.insert({"%",mode});
hash["-"](10,5);
14.9重载、类型转换与运算符
我们可以定义转换构造函数和类型转换运算符来定义类类型转换,这样的转换也被称为用户定义的类型转换。
14.9.1类型转换运算符
类型转换运算符是类的一种特殊成员函数类型转换运算符可以面向任何类型定义,但必须是函数返回类型,数组或者函数类型不行,但可以是指针。
class smallint{
public:
smallint(int i=0):val(i){};
operator int()const{return val}
private:
size_t val;
};
smallint si=4;//首先使用构造函数创建一个参数为4的临时对象,在调用合成的operator=,将对象赋值给si
si+3;//首相调用类型转换运算符将si转换成int,再与3相加
si=3.14;//最开始使用内置类型转换将3.14转化成int,其它与上面一样
si+3.14;//和上面一样,但转换成int后再转换成double
一般很少定义类型转换运算符,但大多数情况下定义向bool类型转换比较普遍,由于bool是算术类型,早期容易出错,如:
int i=42;
cin<<i;
由于istream未定义<<运算符,应该报错,但是定义了bool类型转换,会将cin先转换成算数类型,再将1或0左移42位
为了解决这个问题,我们可以增加explicit关键字使转换必须显示进行,若上述smallint加上explicit,则
si=4;//不会报错,因为构造函数不是显示的
si+3;//错误,我们想使用类型转换必须显示使用
static_cast<int>(si)+3;
当我们使用explicit时,若发生在条件语句中,则可以进行隐式的转换,如:
while(cin>>value)
首先会对value读入数据,然后返回cin,又因为还有条件求值,所以会将cin隐式转换成bool类型,
由于是explici的,所以我们不用条件语句时也不用担心误用。
14.9.2避免二义性的转换
有两种情况会产生二义性的转换
第一种:两个类提供相同的类型转换,A构造函数中接受了B的对象,B定义了转换成A类的类型转换符。
struct B;
struct A{
A(const B&);
};
struct B{
operator A()const{}
};
A f(const A&);//一个返回值位A,接收参数为A的引用的参数的函数
B b;
A a=f(b);//会出现二义性错误,使用A(const B&)或者operatorA()const
第二种:类定义了多个转换规则,这些转换涉及的类型可以同其他转换类型联系在一起,如:
struct A{
A(int=0);
A(double);
operator int()const;
operator double()const;
};
void f2(long double);
A a;
f2(a);//二义性错误,可以先转换成int再通过内置类型转换,也可以先转换成double再通过内置类型转换
14.9.3函数匹配与重载运算符
重载的运算符也是重载的函数,因此函数匹配规则同样适用于使用的是内置类型还是重载类型的运算符。当我们调用命名的函数时,相同名字的成员函数和非成员函数因为语法形式不同不会重载;当我们通过函数对象调用时使用的是重载类型。当我们在表达式中使用时,无法判断成员还是非成员函数。
class smallint{
friend smallint operator+(const smallint &,const smallint &);
public:
smallint(int=0);
operator int(){return val};
private:
size_t val;
};
smallint s1,s2;
smallint s3=s1+s2;//使用重载的+运算符
int s3=s1+0;//具有二义性,可以将s1转换成int,也可以将0转换成smallint再使用+