重载运算与类型转换
- 当运算符作用于类类型的对象时,可以通过运算符重载重新定义该运算符的含义。
基本概念
-
重载运算符:
-
由operator加上运算符号组成,包括返回类型,参数类型和函数体;
-
重载运算符的参数数量与该运算符作用的运算对象数量一样多;
-
如果一个运算符函数是成员函数,则它的第一个运算对象绑定到隐式的this指针上,因此,成员运算符函数的参数数量比运算符的运算对象总数少一个;
-
可以重载的运算符
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0RA8eoK4-1653118951996)(img/C++类设计者的工具/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3hpYW93YW5iaWFvMTIz,size_16,color_FFFFFF,t_70-16531003320612.png)]
-
对于一个运算符函数来说,它或者是类的成员,或者至少含有一个类类型的参数,如果都是内置类型,则是错误的。
-
-
通常情况下,不应该重载逗号,取地址,逻辑与和逻辑或运算符;
-
重载运算符应使用与内置类型一致的含义;
-
选择作为成员或非成员:
- 赋值(
=
)、下标([]
)、调用(()
)和成员访问箭头(->
)必须是成员函数; - 复合赋值运算符(例如
+=
)一般来说应该是成员,但并非必须; - 改变对象状态的运算符或者与给定类型密切相关的运算符,如递增、递减和解引用运算符,通常应该是成员;
- 具有对称性的运算符可能转换任意一端的运算对象,例如算术、相等性、关系和位运算符等,因此它们通常应该是普通的非成员函数;
- 输入输出运算符必须是非成员函数,需要读写类的非公有数据成员时,IO运算符一般被声明为友元;
- 赋值(
-
当把运算符定义为成员函数时,它的左侧运算对象必须是运算符所属类的一个对象。
输入和输出运算符
-
类需要自定义适合其对象的新版本以支持IO操作。
-
重载输出运算符<<
- 通常情况下,输出运算符的第一形参是ostream的非常量引用,第二个形参一般为一个常量的引用,是我们想要打印的类类型;
- 为了保持一致,operatror<<一般返回它的ostream形参;
- 输出运算符应该主要负责打印对象的内容而非控制格式,输出运算符不应该打印换行符;
- 输入输出运算符必须是非成员函数;
ostream &operator<<(ostream &os, const Sales_data &item) { os << item.isbn() << " " << item.units_sold << " " << item.revenue << " " << item.avg_price(); return os; }
-
重载输入运算符>>
- 通常情况下,输入运算符的第一个形参为istream的非常量引用,第二个形参为要读入对象的非常量引用;
- 输入运算符必须处理输入可能失败的情况,而输出运算符则不需要;
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; }
算术和关系运算符
-
通常把算术运算符和关系运算符定义为非成员函数以允许对左侧或右侧的运算对象进行转换;
-
形参都是常量引用;
-
通常情况下,类定义了一个算术运算符,也会定义一个相应的复合运算符,然后使用复合运算符来实现对应的算术运算符;
Sales_data operator+(const Sales_data &lhs, const Sales_data &rhs) { Sales_data sum = lhs; sum += rhs; //如果类定义了相关的复合赋值运算符,使用复合赋值运算符来实现算术运算符 return sum; }
-
相等运算符operator==
-
比较对象的每一个数据成员,只有当所有对应的成员都相等时才认为两个对象相等;
bool operator==(const A &lhs, const A &rhs) { return lhs.unm == rhs.num; } bool operator!=(const A &lhs, const A &rhs) { return !(lhs==rhs); }
-
如果类定义了operator==,这个类也应该定义operator!=;
-
-
关系运算符operator</>
- 如果存在唯一一种逻辑可靠的<定义,则应该考虑为这个类定义<运算符。如果类还同时包含!=,则当且仅当<的定义和产生的结果一致时才定义<运算符。
赋值运算符
-
无论形参的类型是什么,赋值运算符都必须定义为成员函数,返回左侧运算符对象的引用;
class StrVec{ public: StrVec &operator=(std::initializer_list<std::string>); } StrVec &operator=(std::initializer_list<std::string> il){ //alloc_n_copy分配内存空间并从给定范围内拷贝元素 auto data = alloc_n_copy(il.begin(), il.end()); free();//销毁对象中的元素并释放内存空间 elements = data.first; //更新数据成员使其指向新空间 first_free = cap = data.second; return *this; }
-
复合赋值运算符通常情况下也定义为类成员:
//作为成员的二元运算符,左侧运算对象绑定到隐式的this指针 Sales_data& Sales_data::operator+=(const Sales_data &rhs) { units_sold += rhs.units_sold; revenue += rhs.revenue; return *this; }
下标运算符
-
表示容器的类通常可以通过元素在容器中的位置进行访问,一般会定义下标运算符operator[ ];
-
下标运算符必须定义为成员函数;
-
最好同时定义下标运算符的常量版本和非常量版本,当作用于一个常量对象时,下标运算符返回常量引用以确保我们不会为返回的对象赋值;
class StrVec{ public: std::string& operator[](std::size_t n) {return elements[n];} const std::string& operator[](std::size_t n) const {return elements[n];} private: std::string *elements; //指向数组首元素的指针 }
递增和递减运算符
-
递增和递减运算符的类应该同时定义前置和后置两个版本;
-
递增和递减运算符应定义为成员函数;
-
为了和内置版本保持一致,前置运算符应该返回递增或递减后对象的引用;
-
区分前置和后置运算符
- 普通的重载无法区分,前置和后置的区别,规定后置版本接受一个额外的(不被使用)int类型的形参,使用后置运算符时,编译器为这个形参提供一个值为0的实参。
class StrBlobPtr{ public: //递增和递减运算符 StrBlobPtr& operator++(); //前置++ StrBlobPtr operator++(int); //后置++ StrBlobPtr& operator--();//前置-- StrBlobPtr operator--(int);//后置-- } StrBlobPtr& StrBlobPtr::operator++() { check(curr, "increment past end of StrBlobPtr"); ++curr; return *this; } StrBlobPtr StrBlobPtr::operator++(int) { //此处无需检查有效性,调用前置运算符时才需要检查 StrBlob ret = *this; //记录当前的值 ++*this; //前置++需要检查递增的有效性 return ret; } StrBlobPtr& StrBlobPtr::operator--() { --curr; check(curr, "decrement past begin of StrBlobPtr"); return *this; } StrBlobPtr StrBlobPtr::operator--(int) { //此处无需检查有效性,调用前置运算符时才需要检查 StrBlob ret = *this; //记录当前的值 --*this;//前置--需要检查递增的有效性 return ret; }
成员访问运算符
-
箭头运算符必须是类的成员,解引用运算符通常也是类的成员,尽管并非必须如此;
-
成员访问运算符一般被定义为const;
-
对于形如point->mem的表达式来说,point必须指向类对象的指针或者是一个重载了operator->的类的对象。根据point类型的不同,point->mem 分别等价于:
(*point).mem;//point是一个内置的指针类型 point.operator()->mem;//point是类的一个对象
point->mem的执行过程如下:
-
如果point是指针,则我们应用内置的箭头运算符,表达式等价于(*point).mem 首先解引用该指针,然后从所得的对象中获取指定的成员。如果point所指的类型没有名为mem的成员,程序会发生错误;
-
如果point是定义了operator->的类的一个对象,则我们使用point.operator->()的结果来获取mem。其中,如果该结果是一个指针,则执行第一步;如果结果本身含有重载的operator->() ,则重复调用当前步骤。最终,当这一个过程结束时程序或者返回了所需的内容,或者返回一些表示程序错误的信息。
-
-
重载的箭头运算符必须返回类的指针或者定义了箭头运算符的某个类的对象。
函数调用运算符
-
如果类重载了函数调用运算符,则可以像使用函数一样使用该类的对象(这种对象称为函数对象):
-
struct absInt{ int operator()(int val) const{ return val<0 ? -val : val; } } int i = -42; absInt absObj; //含有函数调用运算符的对象 int ui = absObj(i); //将i传递给absObj.operator()
函数调用运算符必须是成员函数;
-
一个类可以定义多个不同版本的调用运算符,相互之间应该在参数数量或类型上有所区别;
-
lambda是函数对象
-
当我们编写了一个lambda之后,编译器将表达式翻译成一个未命名类的未命名对象,这个未命名的类,就重载了函数调用运算符;
for_each(...,...,class(a)); //其中,a为原lambda捕获列表中的参数(也是函数对象构造函数中的参数); //class中operator的参数列表就是lambda的参数列表,也即函数前两个迭代器中返回的参数 [](const string & a , const string & b){return a.size()<b.size();} //其行为类似于下面这个类的一个未命名对象: class ShortString{ public: bool operator()(const string & a , const string & b) {return a.size()<b.size();} } //该类可被如下调用: stable_sort(words.begin(),words.end(),ShortString());
-
默认情况下,由lambda产生的类当中的函数调用运算符是一个const成员。
-
-
标准库定义的函数对象
-
标准库定义了一组表示算术运算符、关系运算符、逻辑运算符的类,每个类分别定义了一个执行命名操作的调用运算符,定义在头文件functional中:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mzJxwAvb-1653118951997)(img/C++类设计者的工具/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3hpYW93YW5iaWFvMTIz,size_16,color_FFFFFF,t_70-16531165224373.png)]
-
表示运算符的函数对象类常用来替换算法中的默认运算符,例如排序算法默认使用
operator<
来将序列升序排列,如果要执行降序排列,可以传入一个greater
类型的对象:sort(svec.begin(), svec.end(), greater<string>());
-
标准库规定其函数对象对于指针同样适用:
vector<string *> nameTable; //指针的vector //错误:nameTable中的指针彼此之间没有关系,所以<产生未定义的行为 sort(nameTable.begin(), nameTable.end(), [](string *a, string *b) {return a<b;}); //正确:标准库规定指针的less是定义良好的 sort(nameTable.begin(), nameTable.end(), less<string *>());
-
-
可调用对象与function
-
c++中的可调用对象:函数,函数指针,lambda,bind创建的对象,重载了函数运算符的类;
-
可调用的对象有类型,例如,每个lambda有唯一的类类型,函数及函数指针的类型则由其返回类型和实参类型决定;
-
两个不同类型的可调用对象可能共享同一种调用形式,调用形式指明了调用返回类型以及传递给调用的实参类型,一种调用形式对应一个函数类型,例如:
int(int,int)
-
可以定义一个函数表用于存储指向这些可调用对象的“指针”,当程序需要执行某个特定的操作时,从表中查找该调用的函数:
//构建从运算符到函数指针的映射关系,其中函数接受两个int、返回一个int map<string,int(*)(int,int)>binops; binops.insert({"+",add});//{"+",add}是一个pair binops.insert({"%",mod});//错误:mod不是一个函数指针
-
标准库function类型
-
function是一个模板,在创建的时候,需要传递一个调用形式:
-
function<int(int,int) f = add;//add是一个函数指针
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VIRu9ucL-1653118951998)(img/C++类设计者的工具/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3hpYW93YW5iaWFvMTIz,size_16,color_FFFFFF,t_70-16531172018357.png)]
-
在这里我们声明了一个function类型,它可以表示接受两个int、返回一个int的可调用对象:
function<int(int,int)>f1 = add; function<int(int,int)>f1 = divide(); function<int(int,int)>f1 = [](int i,int j){return i*j}; //f1(4,2): 6 //f2(4,2): 2 //f3(4,2): 8
-
使用这个function可以重新定义map:
map<string,function<int(int,int)>>binops; map<string,function<int(int,int)>>binops={ {"+",add}, //函数指针 {"-",std::minus<int>()}, //标准库函数对象 {"/",divide()}, //用户定义的函数对象 {"*",[](int i,int j){return i*j}}, //未命名的lambda {"%",mod}, //命名的lambda } //调用操作 binops["+"](10,5); //调用add(10,5)
-
-
重载的函数与function:不能(直接)将重载函数的名字存入function类型的对象中,因为存在二义性。
-
重载、类型转换与运算符
-
可以定义对于类类型的类型转换,转换构造函数和类型转换运算符共同定义了类类型转换,也被称作用户定义的类型转换。
-
类型转换运算符
-
类型转换运算符是类的一种特殊成员函数,它负责将一个类类型的值转换成其他类型:
operator type()const;
- 其中type表示某种类型。类型转换运算符可以面向任意类型(除了void之外)进行定义,只要该类型能作为函数的返回类型。因此不允许转换成数组或者函数类型,但允许转换成指针(包括数组指针及函数指针)或者引用类型。
- 一个类型转换函数必须是类的成员函数;它不能声明返回类型,形参列表也必须为空。类型转换函数通常应该是const。
-
定义含有类型转换运算符的类
class SmallInt { public: SmallInt(int i = 0) :val(i) {} operator int()const { return val; } void print() { cout << val << endl; } private: size_t val; };
-
尽管类型转换函数不负责指定返回类型,但每个类型转换函数都返回一个对应类型的值:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KORcFtCv-1653118952001)(img/C++类设计者的工具/20200530140711637.png)]
-
显式的类型转换运算符:编译器通常不会将一个显示的类型转换运算符用于隐式类型转换,必须通过显式的强制类型转换才可以:
class SmallInt{ public: explicit operator int() const{ return val; } //... }; SmallInt si = 3; si + 3;//错误:此处需要隐式的类型转换,单类的运算符是显式的 static_cast<int>(si)+3;
- 如果表达式被用作条件,则编译器会将显式的类型转换自动应用于它;
- 向bool的类型转换通常用在条件部分,因此operator bool一般定义成explicit的。
-
-
避免有二义性的类型转换
-
如果类中包含一个或多个类型转换,则必须确保在类类型和目标类型之间只存在唯一一种转换方式;
-
二义性与转换目标为内置类型的多重类型转换:对某个给定的类来说,最好只定义最多一个与算术类型有关的转换规则;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XF4PwqF7-1653118952001)(img/C++类设计者的工具/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzExNjkwMA==,size_16,color_FFFFFF,t_70-165311840004311.png)]
-
除了显式地向bool类型的转换之外,我们应该尽量避免定义类型转换函数并尽可能地限制那些“显然正确”的非显式构造函数。
-
重载函数与用户定义的类型转换
struct C{ C(int); //... }; struct D{ D(int); }; void manip(const C&); void manip(const D&); manip(10);//二义性,含义是manip(C(10))还是manip(D(10)) //可以通过显示地构造正确的类型从而消除二义性: manip(C(10));
当我们调用重载的函数时,如果两个或多个类型转换都提供了同一种可行的匹配,则这些类型转换一样好,在这个过程中,我们不会考虑任何可能出现的标准类型转换的级别,只有当重载函数能通过同一个类型转换函数得到匹配时,我们才会考虑其中出现的标准类型转换.。
struct E{ E(double); //其他成员 }; void manip2(const C&); void manip2(const E&); manip2(10);//含义是manip2(C(10)),还是manip(E(double(10)))
-
-
函数匹配与重载运算符
class SmallInt{ friend SmallInt operator+(const SamllInt&,const SmallInt &); public: SmallInt(int = 0); operator int() const {return val;} private: std::size_t val; }; SmallInt s1,s2; SmallInt s3 = s1 + s2; //使用重载的operator+ int i = s3 + 0;//二义性错误
-
第二条加法语句具有二义性:因为我们可以把0转换成SmallInt,然后使用SmallInt的+;或者把s3转换成int,然后对两个int执行内置的加法运算。
-
如果我们对同一个类提供了转换目标是算术类型的类型转换,也提供了重载的运算符,则将会遇到重载运算符于内置运算符的二义性问题。
-