首页 > 代码库 > 【c++笔记十】运算符重载
【c++笔记十】运算符重载
2015年2月1日 阴雨 周日
今天难得休息,闲来无事就往岳麓山走了一遭,作为一个有情怀的程序猿就应该培养B格,不对,培养情操。拍了点雾凇,可以去我QQ空间看看。
今天笔记里要说的内容是:运算符重载。就像函数重载,运算符也可以重载。到底是什么样的重载呢?请听下回分解。哦不对,请看下面分解。
--------------------------------------分割线------------------------------------------
一.什么是运算符的重载?
我们先一起来看一个例子。我们定义了一个类叫做Integer,来模仿int数据类型完成两个数的四则运算(+-*/)。
我很高兴的定义了两个Integer类型的对象a和b,让他们相加的值赋值给对象c。可是编译器给我报错了!!!编译器你这是干嘛?!加法都不让我做了吗?!
看看编译器说了啥:它说,操作数是两个Integer类型的,没有适合他们的加法运算符!!!编译器觉得加法和Integer不适合,就不让他们在一起了(好狠心啊)
所以由此我们引入今天的知识点——运算符的重载。
和函数重载类似,运算符也能重载。重载后的运算符能根据相应的操作数的类型执行不一样的操作(函数重载也很相似,它能够根据不同的参数列表执行不一样的函数体)。
本质上,运算符重载是一种:函数的特殊变现形式。看吧,说白了运算符重载,就和函数重载是相同道理的。如果你还不懂运算符重载,欢迎去看一看我的【c++笔记】
那我们应该怎样去使用运算符的重载呢?
在说使用方法之前,我们先来回顾一下,有哪些运算符?(别说你到现在还不懂什么叫做运算符!!!)让你一个个说出来,你肯定记不全啦。那我们先来给运算符分个类:单目运算符、双目运算符、三目运算符和特殊运算符。
我们按照运算符的类型一一为大家讲解。
二.单目运算符的重载
什么叫单目?就是操作数只有一个。比如我们常见的:++,--,!,~等等就是单目运算符。单目运算符也有两种形式变现:
1.#R
运算符在操作数之前。这样的话,编译器会先去R这个对象对应的类型中找一个成员函数叫operator#()的。如果这个类中没有这个成员函数就去全局域中找一个全局函数叫operator#(R)
从以上这段话我们可以知道,运算符重载先是找成员函数,再是找全局函数。而且函数形式比较特殊,是operator关键加上要重载的运算符的符号#。
我们先来一起写一个成员函数的运算符重载。但是写之前你要好好考虑一下这个函数的返回值问题。我们以前++运算符为例,写一下运算符重载:
#include <iostream> using namespace std; class Integer{ int data; public: Integer(int data=http://www.mamicode.com/0):data(data){}>
我们发现a对象的data值的确发生了自增。说明我们重载的自增运算符很正确嘛!
但是,问题远远没有这么简单。我们稍作改动你再看看:
我们连续对对象a做前++的时候编译器报错了:没有相应的前++运算符?这是为什么?我们一起来分析:++++a等效于++(++a),我们看到函数:void operatot++(),表明我们对本类的对象a执行这个运算符重载的函数之后,返回是void类型。那么式子等价于++void。可想而知,++void是错误的,所以编译报错了。所以我们得考虑换一下返回值类型,用:Integer operator++(),试一试。
#include <iostream> using namespace std; class Integer{ int data; public: Integer(int data=http://www.mamicode.com/0):data(data){}>函数运行很成功,没有报错。可是结果就差强人意了。怎么还是11呢?我们明明做了两次前加加啊!!!结果应该是12才对。
我们一起回顾一下前几篇文章中提到的:拷贝构造函数(不懂的请看【c++笔记】)。如果我们的返回值类型是Integer类型的,返回出去的类型就会发生拷贝构造。++++a => ++(++a),实际上第二次++的是一个临时的Integer变量,并不是对象a本身。那么怎么解决这个问题呢?我们需要用到引用(不懂的请看【c++笔记】)! 这样我们就能确保函数返回的是对象本身了。
函数一切正常,如我们预期的做了两次前++。
如此这般,对于其他单目运算符的重载也类似。我们简单的对几个运算符进行重载。
#include <iostream> using namespace std; class Integer{ int data; public: Integer(int data=http://www.mamicode.com/0):data(data){}>
那运算符写成全局函数应该怎么写呢?也很简单,把运算符重载的函数实现体写在全局域中,参数列表加上Integer类型就行了。可是问题来了:
我们要对对象a的值data进行修改,可是data的值是private权限的,我们在类外更改不了啊!!!怎么办呢?这里需要引入友元函数的概念。
什么是友元函数?它是先在类中定义非成员函数并在函数体之前加上friend关键字,让这个非成员函数成为这个类的朋友,然后这个函数就可以随意访问该类的任何权限的成员了。我们对程序稍加修改,加上友元函数:
#include <iostream> using namespace std; class Integer{ int data; public: Integer(int data=http://www.mamicode.com/0):data(data){}>函数运行起来一切正常啦!
2.R#
运算符在操作数后面的。比较典型的是:后++和后--。先去R对象对应的类型中找一个成员函数叫operator#(int)。如果没有就去全局域中找一个全局函数叫operator#(R,int)。
我们可以发现,为了和#R相区别,重载运算符的函数参数列表多了一个哑元int,这int并不参与函数实现,只是为了告诉编译器,我这是R#!
我们举一个后++例子来说明,后--也是类似的。
#include <iostream> using namespace std; class Integer{ int data; public: Integer(int data=http://www.mamicode.com/0):data(data){}>关于全局域中的运算符重载这里就省略了。同1。
三.双目运算符
所谓双目,就是操作数是两个的,如:+,-,*,/都是双目运算符。
形如:L#R。
先去L对象对应的类型中找一个成员函数叫 operator#(R) ,如果找不到就去全局域中找一个全局函数叫operator#(L,R),最后综合选择最优的函数调用。
其实双目和单目的操作很类似。还是一起来看例子,我们就先来一起完成本文开篇时用的程序,完成两个Integer对象的相加操作。
#include <iostream> using namespace std; class Integer{ int data; public: Integer(int data=http://www.mamicode.com/0):data(data){}>很简单就实现了想加的功能啦,当然全局的运算符重载函数你肯定也会自己写啦。当然运用什么样的返回值,根据你的设计需要来设计啦。一般来说两数相加减乘除都是作为右值的,如果想做为左值返回就自己修改返回的类型吧。
四.特殊的运算符
你有没有发现,我并没有跟你讲三目运算符?是我老年痴呆了吗?还没到那个年龄啦。并不是所有的运算符都能重载的哦!!!
那有哪些运算符不能重载呢?有:
a) :: ,作用域运算符
b) . ,成员访问运算符
c) * ,成员指针解引用运算符(我们在成员指针中提到过)
d)sizeof ,求类型或者对象的大小
e)typeid ,获取类型信息
f) ?: ,三目运算符(唯一的三目运算符)
而且运算符重载遵循一定的原则:
1) 重载运算符的操作数中,至少要有一个类类型的操作数。
2) 不能自己发明运算符,只能用已有的运算符。
3) 不能改变运算符的运算特性。如:不能把二元运算符变为一元运算符。
那还有哪些特殊的运算符,我们需要来说明一下呢?请各位看官继续往下看:
1.输入、输出运算符(>> ,<<)
这种运算符并不是左移和右移运算符,而是我们经常使用的输
输出运算符。它们是特殊的二元运算符。
还记得我们说的二元运算符的形式:L#R。比如:cout<<a。
先去L(cout)对象对应的类型(ostream&)中找一个成员函数叫做 operator<<(R),如果找不到就去全局找一个全局函数叫 做operator<<(L,R)
类型ostream不是我们自己写的类,而是系统已经写好的类。所以我们肯定不可以把这种运算符的重载写作成员函数。只能写做全局函数了,但是记得作为类的友元函数,这样才可以访问内部私有成员哦。
关于这种流类型(输入输出流)的函数重载,还得注意两点:流类型不能加const修饰且流类型对象不能拷贝。 所以在定义返回值和参数列表的时候要注意了。
说了这么多,还是看例子吧:
#include <iostream> using namespace std; class Integer{ int data; public: Integer(int data=http://www.mamicode.com/0):data(data){}>
我们从键盘输入1234,然后输出了1234。这样,就轻松的完成了类的输入和输出了。想输出什么样的格式,你说了算!
2.new与delete的重载
new和delete也能重载?!当然了。除了我们说的那五种不能重载的运算符,其他的运算符只要你看到过的,都可以重载。
在重载之前,我们需要知道一点:new和delete的底层是通过malloc和free实现的。所以在重载之前你要知道我们得通过malloc和free来完成函数的实现。
void* operator new (size_t size);
void operator delete(void* ptr);
以上就是就是new和delete的函数原型。我们一起来写代码看看:
#include <iostream> #include <cstdlib> using namespace std; class Integer{ int data; public: Integer(int data=http://www.mamicode.com/0):data(data){}>3.赋值运算符“=”
其实重载“=”的实现很好写,但是我们还应该更加关注牵涉到的内存问题。我们回想一下为什么我们需要拷贝构造函数?就是为了保证内存的独立性。其实,赋值运算符同样需要考虑这个问题。
比如,你把一个含有分配了堆内存的对象赋值给另外一个对象时,他们的成员指针不能简单的指向相同的堆内存地址,需要自己额外申请一片堆内存。
我们还是写个代码:
#include <iostream> using namespace std; class Integer{ int* data; int size; public: Integer(int size=1):size(size){ data = http://www.mamicode.com/new int[size];>我们把a赋值给b,不仅需要让b重新new一块内存出来,还要把a中的数据赋值给b。
4.()圆括号运算符
回想我们在哪里用到过()与那算符?
一个是函数的参数列表用到了“()”,还有进行强制类型转换的时候也用到了“()”。所以“()”有两种重载功能。
1)像一个函数一样去使用一个对象
#include <iostream> using namespace std; class XiaoMi{ int price; public: XiaoMi(int price):price(price){} int operator()(int number){ return number*price; } }; int main() { XiaoMi note(2299); cout<<note(10)<<endl; return 0; }我新建了一个XiaoMi类,并创建了一个note对象(小米note2299,给它赋个初始价格)。在成员函数中,我们重载了“()”运算符,想给对象一个数量就能给我们返回一个总价出来。
“老板!来10台小米note!”,“好勒,客官一共22990元,欢迎下次光临!”(自己YY)
2)将当前对象转换成其他数据类型
这种方法的重载函数的写法有点特殊:operator 转换成的类型(){}
OK,我们看代码:
#include <iostream> using namespace std; class XiaoMi{ double price; int number; public: XiaoMi(double price=0.0,int number=0):price(price),number(number){} operator double(){ return price; } operator int(){ return number; } }; int main() { XiaoMi note(2299.5,10); cout<<(double)note<<endl; cout<<(int)note<<endl; return 0; }XiaoMi类中有double类型的价格price,还有int类型的数量number。通过重载“()”,我们如果把对象强制类型转换成double型的话,就返回price,转换成int的话就返回数量。
5.->和*,指针运算符
重载他们的目的:是把一个不是指针的类型,当做指针类型来使用。
在说这个重载之前,我还想给大家普及一个知识点——智能指针(auto_ptr)。智能指针是一种系统已经写好的类,我们一起看看c++帮助手册中对它的介绍:
其实看完这个,你就知道怎么去使用它了。但是我们为什么要使用智能指针呢?我来考你一个问题:如果你动态创建了一个类的对象*a,如果你把delete它,你说它会自动调用自己的析构函数吗?会不会我们看代码:
#include <iostream> using namespace std; class A{ public: A(){ cout<<"A()"<<endl; } ~A(){ cout<<"~A()"<<endl; } }; int main() { A *a = new A(); return 0; }细心的观察, 它并没有自动调用自己的析构函数!!!如果这个类中有自己申请的堆内存,可是你动态创建了这个对象又没有去调用它的析构函数,那么造成了内存溢出怎么办?!考虑到有犯这种粗心的人,所以就有了智能指针这个东西。智能指针会自动帮你析构函数~妈妈再也不用担心内存溢出了!
C++帮助文档那个例子就是个很好的例子,你可以自己动手敲一敲看看是不是真的那么好用。因为我们不是专门讲智能指针的,所以回到主题来:->和*的重载。
我们通过手动来实现智能指针的功能来为大家更好的讲解“->和*的重载”。
#include <iostream> using namespace std; class A{ public: A(){ cout<<"A()"<<endl; } void show(){ cout<<"A::show()"<<endl; } ~A(){ cout<<"~A()"<<endl; } }; class autoPtr{ A *data; public: autoPtr(A* data=http://www.mamicode.com/NULL):data(data){}>
我们在类autoPtr中重载了“->和*”,使得autoPtr的对象ap表现的像一个指针,达到了智能指针的作用。
6.[ ]下标运算符
这种运算符一般用在数组的取值上。看一个简单的程序你就懂怎么去使用了:
#include <iostream> using namespace std; class Array{ int* data; int size; public: Array(int size=0):size(size){ data = http://www.mamicode.com/new int[size];>
------------------------------------结束语--------------------------------------
今天我们主要讲解了:单目运算符、双目运算符和特殊运算符的重载(>>和<<,new和delete(),=,(),->和*,[ ])。还介绍了五种不能重载的运算符,告诉了大家运算符重载的三条原则。希望大家看完这篇文章之后,能学到很多知识!
最后,给大家布置一个作业:
设立一个数组类,能动态决定数组的大小(重键盘输入)。能对数组进行输入、输出,能完成数组类的两个对象之间的各种运算操作:
+ - * = ! ~ ++ --。能运用[ ]取对象中的值。当然你还可以扩展其他功能,灵活运用本文说提到的所有知识点。
写好之后我们可以相互交流一下,我的QQ137332024。
【c++笔记十】运算符重载