本文基于《C++ Primer(第5版)》中14章和《More Effective C++》条款7,整理而成。
其实写这篇博客之前,内心还是很忐忑的,因为,博主的水平很有限,视野比较窄,要是在理解书的过程中有了偏差,给读到这篇博客的人以错误的认识,那罪过就大了。再次声明本文仅是简介,若是有错误的地方欢迎留言指出。
个人认为运算符最重要的是:使用与内置类型一致的含义。
一、基本概念
- 当运算作用于类类型的运算对象时,可以通过运算符重载重新定义该运算符的含义。
重载的运算符是具有特别名字的函数,它们的名字由关键字 operator和其后要定义的运算符号共同组成。其包括:返回类型、参数列表以及函数体。
重载运算符函数的参数数量与该运算符作用的运算对象一样多,如:一元运算符有一个参数,二元运算符有两个。这里值得注意的是,当运算符函数时成员函数时,则它的第一个(左侧)运算对象绑定到隐式的this指针上,因此,成员运算符函数(显式)参数数量比运算符的运算对象总数少一个,但实际上总数不变。
1、某些运算符不应该被重载
不能被重载的运算符有 :
. | .* | :: | ?: |
new | delete | sizeof | typeid |
static_cast | dynamic_cast | const_cast | reinterpret_cast |
能被重载但最好不要重载的运算符有:
(1)逻辑与&&,逻辑或||
逻辑与运算符和逻辑或运算符都是先求左侧运算对象的值再求右侧运算对象的值,当且仅当左侧运算对象无法确定表达式的结果时,才会计算右侧运算对象的值,这种策略称为短路求值(short-circuit evaluation)。
《More Effective C++》给出的例子,若重载operator &&,下面的这个式子:
1 if(expression1 && expression2) ...
会被编译器视为以下两者之一:
1 //假设operator&& 是个成员函数 2 if(expression1.operator&&(expression2)) ... 3 4 //假设operator&& 是个全局函数 5 if(operator&&(expression1,expression2)) ...
即无法保留内置运算符的短路求值属性,两个运算对象总是被求值。
(2)逗号 ,
逗号操作符的求值顺序是从左往右依次求值,逗号运算符的真正的结果是右侧表达式的值。若是打算重载逗号运算符,就必须模仿这样的行为,但是,无法执行这些必要的模仿。求值顺序和返回结果同时满足才行。
2、选择成员函数或者非成员函数
当我们定义重载的运算符是,必须首先决定是将其声明为类的成员函数还是声明为一个普通的非成员函数。(有关成员函数和非成员函数,见成员函数与非成员函数的抉择)。下面有些准则有助于我们选择:
(1)赋值(=)、下标([ ])、调用( ( ))、和成员访问箭头(->)运算符必须是成员;
(2)复合赋值运算符一般来说应该是成员,但并非是必须的;
(3)改变对象状态的运算符或者与给定类型密切相关的运算符,如,递增、递减和解引用运算符,通常是成员;
(4)具有对称性的运算符可能转换任意一段的运算符对象,如,算术、相等性、关系和位运算等,因此它们通常应该是普通的非成员函数。
二、各种重载
1、重载输出运算符<<和输入运算符>>
(1)重载输出运算符<<
通常情况下,输出运算符的第一个形参是一个非常量ostream对象的引用,之所以ostream是非常量是因为向流写入内容会改变其状态,而该形参是引用时因为我们无法直接复制一个ostream对象;第二个形参一般是一个常量的引用,是引用的原因是希望避免复制实参,而之所以该形参可以是常量是因为(通常)打印对象不会改变对象的内容。另外,为了与其他输出运算符保持一致,返回ostream形参。
1 ostream &operator<<(ostream &os,const Sales_data &item) 2 { 3 os<<item.isbn()<<" "<<item.avg_price(); 4 return os; 5 }
(2)重载输入运算符>>
通常,输入运算符的第一个形参是运算符将要读取的流的引用,第二形参是将要读入的(非常量)对象的引用。该运算符通常会返回某个给定流的引用。第二个形参之所以必须是个非常量时因为输入运算符本身的目的就是将数据读入到这个对象中。
1 istream &operator>>(istream &is,Sales_data &item) 2 { 3 double price; 4 is>>item.bookNo>>item.units_sold>>price; 5 if(is) //必须处理可能失败的情况 6 { 7 item.revenue=item.units_sold*price; 8 } 9 else //若失败,则对象呗赋予默认状态 10 { 11 item=Sales_data(); 12 } 13 return is; 14 }
输入时的可能发生以下错误:
当流含有错误类型的数据时读取操作可能失败;当读取操作达到文件末尾或者遇到输入流的其他错误是也会失败。
输入运算符应该负责从错误中恢复。
(3)输入输出运算符必须是非成员函数
若是类的成员,它们的左侧运算对象将是我们类的一个对象:
1 Sales_data data; 2 data<<cout; //如果operator<<是Sales_data的成员
假如输入输出运算符是某个类的成员,则它们也必须是istream或ostream的成员,然而,这个类属于标准库,并且我们无法给标准库中的类添加任何成员。
2、算术和关系运算符
通常,把算术和关系运算符定义成非成员函数以允许对左侧或右侧的运算对象进行转换,因为这些运算符一般不需要改变运算对象的状态,所以形参都是常量的引用。
(1)算术运算符
算术运算符通常会计算它的两个运算对象并得到一个新值,这个值有别于任意一个运算对象,常常位于一个局部变量之内,操作完成之后返回该局部变量的副本作为其结果。
1 Sales_data operator+(const Sales_data &lhs,const Sales_data &rhs) 2 { 3 Sales_data sum=lhs; 4 sum+=rhs; 5 return sum; 6 }
(2)相等运算符
C++中的类通过定义相等运算符来检查两个对象是否相等,会对比对象的每一个数据成员,只有当所有的对应的成员都相等时,才认为两个对象相等。
1 bool operator==(const Sales_data &lhs,const Sales_data &rhs) 2 { 3 return lhs.isbn()==rhs.isbn()&& 4 lhs.units_sold==rhs.units_sold&& 5 lhs.revenue==rhs.revenue; 6 } 7 8 bool operator !=(const Sales_data &lhs,const Sales_data &rhs) 9 { 10 return !(lhs==rhs); 11 }
3、赋值运算符
赋值运算符必须定义为成员函数
(1)拷贝赋值
拷贝赋值运算符接受一个与其所在类相同类型的参数:
1 class Foo 2 { 3 public: 4 Foo &operator=(const Foo&); //赋值运算符 5 //... 6 }
(2)移动赋值运算符
移动赋值运算符不抛出任何异常,将它标记为noexecpt,类似拷贝赋值运算符,移动赋值运算符必须正确处理自赋值:
1 StrVec &StrVec::operator=(StrVec &&rhs) noexecpt 2 { 3 if(this !=&rhs) //直接检测自赋值 4 { 5 free(); //释放已有元素 6 elements=rhs.elements; //从rhs接管资源 7 first_free=rhs.first_free; 8 cap=rhs.cap; 9 rhs.elements=rhs.first_free=rhs.cap=nullptr; //将rhs置于可析构状态 10 } 11 return *this; 12 }
(3)标准库vector类定义的第三种赋值运算符,该运算符接受花括号内的元素列表作为参数,如:
1 vector<string> v; 2 v={"a","an"};
运算符添加到StrVec类中时:
1 StrVec &StrVec::operator=(initiallizer_list<string> il) 2 { 3 //alloc_n_copy分配内存空间并从给定范围内拷贝元素 4 auto data=alloc_n_copy(il.begin(),il.end()); 5 free(); //销毁对象中的元素并释放内存空间 6 elements=data.first; //更新数据成员使其指向新空间 7 first_free=cap=data.second; 8 return *this; 9 }
4、递增和递减运算符
(1)定义前置递增、递减运算符
它们首先调用check函数检验StrBolbPtr是否有效,若是,接着检查给定的索引值是否有效,若是check函数没有抛出异常,则运算符返回对象的引用。
1 class StrBlobPtr 2 { 3 public: 4 StrBlobPtr& operator++(); 5 StrBlobPtr& operator--(); 6 } 7 8 StrBlobPtr& StrBlobPtr::operator++() 9 { 10 //若curr已经指向容器的尾后位置,则无法递增它 11 check(curr,"increment past end of StrBlobPtr"); 12 ++curr; 13 return *this; 14 } 15 16 StrBlobPtr& StrBlobPtr::operator--() 17 { 18 --curr; 19 check(curr,"decrement past begin of StrBlobPtr"); 20 return *this; 21 }
(2)定义后置递增、递减运算符
和前置比较后置版本接受一个额外的(不使用)int 类型的形参。
1 class StrBlobPtr 2 { 3 public: 4 StrBlobPtr& operator++(int); 5 StrBlobPtr& operator--(int); 6 } 7 8 StrBlobPtr& StrBlobPtr::operator++(int) 9 { 10 StrBlobPtr ret=*this; //记下当前值 11 ++*this; //需要前置++检查是否有效 12 return *ret; //返回之前记录的状态 13 }
另外,其他运算符见书《C++ Primer(第5版)》。