重载运算和类型转换
- 重载的运算符使具有特殊名字的函数,重载运算福德参数数量应该和该运算符作用的运算对象数量保持一致,一元运算符有一个,二元运算符有两个;
- 当重载的运算符是成员函数时,
this
绑定到左侧运算对象,成员运算符函数的参数数量比运算对象的数量少一个; - 对于一个运算符函数来说,它或者是类的成员,或者至少含有一个类类型的参数;
- 运算符重载仅仅限于重载已经存在的运算符,无权发明新的运算符;
- 运算符的优先级和结合律应该和内置的运算符保持一致;
- 通常情况下,逗号,取地址,逻辑与和逻辑或运算符是不应该被重载的;
- 如果某些操作在逻辑上与运算符有关,就应该定义成重载的运算符;
- 如果类执行
IO
操作,则定义移位运算符使其与内置类型的IO
保持一致. - 如果是检查相等性,使用
operator==
; - 如果是包含一个内在的单序列的比较实用
operator<
; - 重载运算符的返回类型通常情况下应该与其内置版本的返回值类型兼容;
- 选择是否作为成员或者是非成员函数:
- 1.赋值,下标,调用,和箭头访问运算符
(->)
必须是成员函数; - 2.复合赋值运算符一般来说应该是成员,但必须;
- 3.改变运算对象状态的运算符或者与给定类型密切相关的运算符,比如递增,递减运算符和解引用运算符都应该是成员函数;
- 4.具有对称性的运算符可能转换任意一端的运算对象,比如算术,相等性关系和位运算符,适合作为非成员函数;
- 如果想要提供含有类对象的混合类型表达式,那么运算符必须定义成非成员函数;
- 当我们把运算符定义成成员函数时,它的左侧对象必须是所属于类的一个对象;
- 1.赋值,下标,调用,和箭头访问运算符
输入输出运算符重载:
输出运算符重载:
ostream &operator <<(ostream &os,const Sales_data &item){ os << item.isbn() << " " << item.units_sold << " " << item.revenue << " " << item.arg_price(); return os; }
- 首先第一个性参数一个非常量
const
对象的引用,ostream
是非常量因为向流里面写入内容会改变其状态,使用引用是因为我们无法直接复制一个ostream
对象; - 输出运算符尽量较少格式化操作,比如输出运算符不应该打印换行操作,应该交给用户进行控制;
- 输入输出运算符必须是非成员函数,通常应该被声明为友元函数,原因是输入输出运算符假设为某个类的成员,大师同时也必须是
istream
和ostream
的成员,
标准库里面的类,我们使无法添加任何成员的; 输入运算符重载:
第一个形参表示的是运算符将要读取的流的引用,第二个形参表示的是将要读取的非常量对象的引用,通常需要返回某个给定的流的引用.第二个形参必须
是非常量,因为输入运算符本身的目的就是将数据写入这个流里面;istream &operator>>(istream &is,Sales_data &item){ double price; is >> item.price; if(is) item.revenue = item.units_sold * price; else item = Sales_data(); return is; }
再进行输入控制时,运算符必须处理可能失败的情况;
- 当流含有错误类型的数据时,会导致读取操作可能失败;
- 当读取操作到达文件结尾或者遇到输入流的其他错误时也会失败;
- 当流读取数据时发生错误,输入运算符应该负责从错误里面恢复;
算数运算符和关系运算符;
- 通常情况下,算术和关系运算符定义成非成员函数以允许对左侧或者右侧到的运算对象进行转换,形参一般是常量的引用,因为运算对象本身的值是不允许
进行改变的; - 如果类同时定义了算术运算符和相关的复合运算符,那么在通常情况下,应该使用复合赋值来实现算赋值;
- 相等运算符:如果某个类在逻辑上面有相等性的含义,则该类应该定义
operator==
,这样做可以使得用户更容易使用标准库算法来处理这个类; - 关系运算符通常定义
operator<
算法,这个更加常用; - 如果存在唯一一种逻辑可靠的
<
定义,则应该考虑为这个类定义<
运算符.如果类同时还包含==
,则当且仅当<
的定义和==
产生的结果一致时,需要
定义<
运算符; - 拷贝赋值和移动赋值完成的是将类的一个对象赋值给类的另一个对象;
赋值运算符完成的是可以使用其他类型作为右侧运算对象;
class StrVec{ public: StrVec &operator=(initilizer_list<string> i1){ auto data = alloc_n_copy(i1.begin(),i1.end()); free(); elements =data.first; first_free = cap = data.second; return *this; } };
无论赋值运算符的形参类型是什么,赋值运算符都必须定义为成员函数;
复合赋值运算符
复合运算符不一定是类的成员,但是一般建议将包括复合运算符在内的所有辅助运算符定义在类的内部,复合运算符也需要返回左侧对象的引用;
Sales_data& Sales_data::operator+=(const Sales_data &rhs){ units_sold += rhs.units_sold; revenue += rhs.revenue; return *this; }
- 赋值运算符必须定义成了的成员,复合赋值运算符通常情况下也应该这样,然后需要返回左侧运算对象的引用;
- 下标运算符:可以通过位置访问元素的一种方法,通常需要定义下表运算符
operator[]
,下标运算符必须是成员函数. - 下标运算符应该包含两个版本,一个返回普通引用,另一个是类的常量成员,并且返回常量引用,使用引用作为返回值,下标可以出现在赋值运算符的任何一段;
class StrVec{ public: string& operator[](size_t n){ return elements[n]; } const string& operator[](size_t n)const{ return elements[n]; } private: string *elements; };
- 通常情况下,算术和关系运算符定义成非成员函数以允许对左侧或者右侧到的运算对象进行转换,形参一般是常量的引用,因为运算对象本身的值是不允许
递增递减运算符:递增,递减运算符,
C++
并不要求必须是类的成员,但是因为会改变操作对象的状态,所以将其设定未成员函数;递增递减运算符应该同时定义前置和后置版本,通常设置为类的成员;
前置递增递减:class StrBlobPtr{ public: StrBlobPtr& operator++(){ ++curr; return *this; } StrBlobPtr& operator--(){ --curr; return *this; } };
前置运算符应该返回递增或者递减后对象的引用;
后置运算符
class StrBlobPtr{ public: StrBlobPtr operator++(int){ StrBoloPtr ret = *this; ++*this; return ret; } //后置运算符; StrBlobPtr operator--(int){ StrBlobPtr ret = *this; --*this; return ret; } //后置运算符; };
为了保持与内置版本一致,后置运算符应该返回对象的原值(也就是递增或则递减之前的值),返回的是值,而不是引用.
- 因为我们不会用到
int
的形参,所以不需要命名; 成员访问运算符,通常在迭代器或者智能指针中常常使用解引用运算符
(*)
运算符和箭头运算符(->)
.class StrBlobPtr{ public: string& operator*()const{ auto p=check(curr,"dereference past end"); return (*p)[curr]; } string* operator->()const{ return & this->operator*(); } };
箭头运算符必须是类的成员,解引用运算符通常也是类的成员,但是并非必须如此;
- 箭头运算符返回值的限定:
- 对于形如
point->mem
的表达式来说,point
必须是指向类对象的指针或者是一个重载了operator->
的类的对象.如果是前者(*point).mem
;如果是
后者等价于point.operator()->mem
; - 分析前者:如果
point
是指针,应用的是内置的箭头运算符,表达式等价于(*point).mem
,首先解引用指针,然后获取.mem()
的成员; - 分析后者:首先执行的是
operator->()
的类的一个对象,则使用operator->()
的结果来获取对象mem
,如果该结果是一个指针,则执行第一步,如果结果
包含重载,重复执行当前步骤,直到返回需要的信息; - 重载的箭头运算符必须返回类的指针或者自定义了箭头运算符的某个对象.
- 对于形如
函数调用运算符
如果类重载了函数调用运算符,我们可以像使用函数一样使用该类的对象;
struct absInt{ int operator()(int val)const{ return val < 0 ? -val : val; } }; 对于上述的调用的过程: int i = -42; abdInt absObj; int ui = abdObj(i);//这个的调用过程类似与函数调用;
- 函数调用运算符必须是成员函数,一个类可以定义多个不同版本的调用运算符,相互之间应该在参数数量或者类型上面有所区别;
- 如果类定义了调用运算符,则该类的对象称作函数对象;
- 函数对象除了
operator()
之外的也可以包含其他成员;
class PrinString{ public: PrintString(ostream &o = cout,char c=' '): os(o),sep(c) { } void operator()(const string &s)const {os << s << sep} private: ostream &os; char sep; };
lambda
:编译器将一个lambda
翻译成一个未命名类的未命名对象.在lambda
表达式禅城的类中含有一个重载的函数调用运算符;stable_sort(words.begin(),words.end(),[] (const string &a,const string &b){return a.size() < b.size();}); 上面函数的行为类似于: class ShorterString { public: bool operator()(const string &s1,const string &s2)const{ return s1.size() < s2.size(); } };
在默认情况下
.lambda
不能够改变它所捕获的变量.所以上面的类应该是const
类型的;- 当
lambda
表达式引用变量时,由程序负责保证lambda
所引用的对象时存在的,所以编译器可以直接使用,而不用进行存储; 如果是通过值捕获的变量,在对应的类里面就需要建立相应的数据成员,同时创建构造函数,使用捕获的变量来初始化数据成员;
auto wc = find_if(words.begin(),words.end(),[sz](const string &a){return a.size() > sz}); 转换为类的形式: class SizeComp{ SizeComp(size_t n):sz(n) { } bool operator()(const string &s)const{ return s.size(0 >= sz;) } private: size_t sz; //需要使用这个变量来存储捕获的变量; };
lambda
表达式产生的类不喊又默认构造函数,赋值运算符以及默认的析构函数,是否需要含有默认的拷贝/移动构造函数需要根据数据成员的类型来确定;- 标准库定义的函数对象,标准库定义的大多数函数对象唯一头文件
functional
里面; - 可调用对象和
function
:
- 可调用对象包括:函数,函数指针,
lambda
表达式,bind
创建的对象以及重载了函数调用运算符的类; - 每个可调用对象也是有类型的,
lambda
有自己唯一的未命名的类类型,函数以及函数指针的类型则由其返回值类型和实参类型决定. - 对于不同类型的可调用那个对象却能够共享一种调用形式,调用形式指明了调用返回的类型以及传递给调用的实参类型;
- 一种调用形式对应一个函数类型
int(int,int)
; - 对于不同的类型可能具有相同的调用形式;
- 可调用对象包括:函数,函数指针,
- 标准库
function
类型
function<T> f;
表示的含义是f
是一个用来存储可调用对象的空function
,这些可调用对象的调用形式应该于函数类型T
相同;function<int(int,int)>
:表示的含义是接受两个int
型的参数,并且返回值是int
类型;- 使用这种方式可以将所有的可调用对象,包含函数指针,
lambda
或者函数在内,都添加到map
里面;
重载.类型转换与运算符
- 构造函数将实参类型转换成类类型,同样的也可以使用类类型转换成基本类型;转换构造函数和类型转换运算符共同定义了类类型转换,这种转换有时被称为用户定义的类型转换;
- 类型转换运算符是类的一种特殊的成员函数,负责将一个类类型的值,转换成其他类型,一般形式
operator type()const
; - 类型转换运算符可以面向任意类型进行转换,但是
void
类型除外,前提是这个类型可以作为函数的返回值,还有不能够转换为数组或者函数类型,但是可以转换成为指针(数组指针以及函数指针)或者是引用类型; - 类型转换运算符既没有显示的返回类型,也没有形参,而且必须定义成为类的成员函数,类型转换运算符通常不应该改变带转换对象的内容,所以一般是
const
成员; - 编译器一次只能够执行一个用户定义的类型转换,但是隐式的用户定义的类型转换可以至于一个标准(内置)类型转换之前或者之后;
- 显示的类型转换运算符:类类型转换可能会出现错误情况,不能够过度使用.
C++11
标准引入了显示的类型转换运算符explict
可以用于禁止那些隐式的类型转换,用于防止出现错误; - 上处约定的例外情况,当表达式出现在下列情况,显示的转换类型仍然会被隐式的执行;
- 1.
if
,while
以及do
语句的条件部分; - 2.
for
语句头的条件表达式; - 3.逻辑非运算符
(!)
,逻辑(||)
运算符,逻辑与(&&)
运算符; - 4.条件运算符
(?:)
的条件表达式;
- 1.
- 向
bool
的类型转换通常用在条件部分,因此operator bool
一般定义为explict
的;
- 避免有二义性的类型转换:在通常情况下,不要在类里面定义相同的类型转换,也不要在类中定义两个以及两个以上转换源或者转换目标是算术类型的转换;
- 再进行转换的过程中还可能出现二义性与转换目标为内置类型的多重转换;
- 不应该创建两个转换对象都是算术类型的类型转换;
- 当使用两个用户定义的类型转换时,如果转换函数之前或者之后存在标准类型转换,则标准类型转换将决定最佳匹配到底是哪个;
- 设计类的几条原则:
- 1.不要令两个类执行相同的类型转换,也就是说不能够出现类似环型转换;
- 2.转换目标是内置算术类型的类型转换,特别是定义了一个转换成算数类型的类型转换;
- 1.不要在定义接受算术类型的重载运算符;
- 2.不要定义转换到多种算术类型的类型转换;
- 总结就是说:除了显示地向
bool
类型的转换之外,应该尽量避免定义类型转换函数并尽可能的限制那些显然正确的非显示构造函数.
- 重载函数与转换构造函数
- 在调用重载函数时,如果需要额外的标准类型转换,则该转换的级别只有当所有可行函数都请求同一个用户定义的类型转换时才有用,如果所需的用户定义
的类型转换不止有一个,那么该调用据有二义性; - 重载的运算符也就是重载的函数:
- 表达式中运算符的候选函数集既应该包括成员函数,也应该包括非成员函数.
- 如果对同一个类既提供了转换目标是算术类型的类型转换,也提供了重载的运算符,就会遇到重载运算符与内置运算符的二义性问题;
- 在调用重载函数时,如果需要额外的标准类型转换,则该转换的级别只有当所有可行函数都请求同一个用户定义的类型转换时才有用,如果所需的用户定义