C++重载运算简介

本文基于《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版)》。

时间: 2024-12-26 10:05:31

C++重载运算简介的相关文章

位运算简介及实用技巧(一):基础篇[转]

位运算简介及实用技巧(一):基础篇 原贴链接:http://www.matrix67.com/blog/archives/264 去年年底写的关于位运算的日志是这个Blog里少数大受欢迎的文章之一,很多人都希望我能不断完善那篇文章.后来我看到了不少其它的资料,学习到了更多关于位运算的知识,有了重新整理位运算技巧的想法.从今天起我就开始写这一系列位运算讲解文章,与其说是原来那篇文章的follow-up,不如说是一个remake.当然首先我还是从最基础的东西说起. 什么是位运算?    程序中的所有

C++11(13):重载运算与类型转换

除了重载的函数调用运算符operator()之外,其他重载运算符不能含有默认实参. 当以个重载的运算符是成员函数时,this绑定到左侧运算对象.成员运算符函数的(显式)参数比运算对象的数量少一个. 当运算符作用于内置类型的运算对象时,我们无法改变运算的含义. ::              .*                     .          ?:   这四个运算符不能被重载. 我们只能重载已有的运算符,不能发明新的.优先级和结合律不变. data1 + data2; operat

位运算简介及实用技巧(二):进阶篇(1)[转]

位运算简介及实用技巧(二):进阶篇(1) 原贴链接:http://www.matrix67.com/blog/archives/264 =====   真正强的东西来了!   ===== 二进制中的1有奇数个还是偶数个    我们可以用下面的代码来计算一个32位整数的二进制中1的个数的奇偶性,当输入数据的二进制表示里有偶数个数字1时程序输出0,有奇数个则输出1.例如,1314520的二进制101000000111011011000中有9个1,则x=1314520时程序输出1.var   i,x,

c++ 重载运算与类型转换

1 基本概念 1.1 基本点 隐式的this指针 运算符函数至少有一个类类型参数 重载运算符的优先级应与内置运算符一致 ::, .*, ., ? : 不可重载 1.2 重载的运算符函数的调用方式 1) 非成员函数的调用   operator+为对象data1, data2的非成员函数 data1 + data2;        ==> operator+(data1 , data2); 2) 成员函数的调用    operator+为对象data1的成员函数 data1 + data2    

第十五章、重载运算与类型转换

一.基本概念 1.重载运算符是具有特殊名字的函数:由关键字operator和其他要定义的运算符号共同组成 2.重载运算符的参数数量与该运算符作用的运算对象一样多 当一个重载的运算符是成员函数时,this会绑定到左侧运算对象,成员运算符函数的显示参数数量比运算对象的数量少一个 3.对于一个运算符函数,它要么是类的成员,或者至少含有一个类类型的参数 当作用符作用于内置类型的运算对象时,无法改变该运算符的含义 例: //非成员运算符函数的等价调用 data1 + data2: //普通的表达式 ope

重载运算与类型转换——基本概念,输入和输出运算符,算术和关系运算符,赋值运算符,下标运算符,递增和递减运算符,成员访问运算符

一.基本概念 重载的运算符时具有特殊名字的函数:它们的名字由关键字operator和其后要定义的运算符号共同组成.和其他函数一样,重载的运算符也包含返回类型.参数列表以及函数体. 重载运算符函数的参数数量与该运算符作用的运算对象数量一样多.一元运算符有一个参数,二元运算符有两个.对于二元运算符来说,左侧运算对象传递给第一个参数,而右侧运算对象传递给第二个参数.除了重载的函数调用运算符operator()之外,其他重载运算符不能含有默认实参. 当一个重载的运算符时成员函数时,this绑定到左侧运算

C++之重载运算与类型转换

重载的运算符是具有特殊名字的函数:他们的名字是由关键字operator和其后定义的运算符号共同组成.重载运算符也包含返回类型.参数列表和函数体. 重载运算符的参数数量与该运算符作用的运算对象数量一样多.一元运算符有一个参数.二元运算符有两个参数.对于二元运算符来说,左侧对象传递给第一个参数,而右侧对象传递给第二个参数.除了operator(),其他重载运算符不能含有默认实参. 如果一个运算符函数是成员函数,则它的第一个(左侧)运算对象绑定到隐式的this指针上,因此成员运算符函数的参数数量比运算

c++ primer(第五版)学习笔记及习题答案代码版(第十四章)重载运算与类型转换

笔记较为零散,都是自己不熟悉的知识点. 习题答案至于一个.h 和.cc 中,需要演示某一题直接修改 #define NUM****, 如运行14.30题为#define NUM1430: Alice Emma has long flowing red hair. Her Daddy says when the wind blows through her hair, it looks almost alive, like a fiery bird in flight. A beautiful f

(转)位运算简介及使用技巧

转自http://www.matrix67.com/blog/archives/263 去年年底写的关于位运算的日志是这个Blog里少数大受欢迎的文章之一,很多人都希望我能不断完善那篇文章.后来我看到了不少其它的资料,学习到了更多关于位运算的知识,有了重新整理位运算技巧的想法.从今天起我就开始写这一系列位运算讲解文章,与其说是原来那篇文章的follow-up,不如说是一个remake.当然首先我还是从最基础的东西说起. 什么是位运算?    程序中的所有数在计算机内存中都是以二进制的形式储存的.