【c++笔记十】运算符重载

来源:互联网 发布:尼尔森数据 编辑:程序博客网 时间:2024/06/04 20:05

201521日 阴雨 周日

        今天难得休息,闲来无事就往岳麓山走了一遭,作为一个有情怀的程序猿就应该培养B格,不对,培养情操。拍了点雾凇,可以去我QQ空间看看。 

        今天笔记里要说的内容是:运算符重载。就像函数重载,运算符也可以重载。到底是什么样的重载呢?请听下回分解。哦不对,请看下面分解。

--------------------------------------分割线------------------------------------------

一.什么是运算符的重载?

         我们先一起来看一个例子。我们定义了一个类叫做Integer,来模仿int数据类型完成两个数的四则运算(+-*/)。


         我很高兴的定义了两个Integer类型的对象ab,让他们相加的值赋值给对象c。可是编译器给我报错了!!!编译器你这是干嘛?!加法都不让我做了吗?!

         看看编译器说了啥:它说,操作数是两个Integer类型的,没有适合他们的加法运算符!!!编译器觉得加法和Integer不适合,就不让他们在一起了(好狠心啊)

 

         所以由此我们引入今天的知识点——运算符的重载。

         和函数重载类似,运算符也能重载。重载后的运算符能根据相应的操作数的类型执行不一样的操作(函数重载也很相似,它能够根据不同的参数列表执行不一样的函数体)。

         本质上,运算符重载是一种:函数的特殊变现形式。看吧,说白了运算符重载,就和函数重载是相同道理的。如果你还不懂运算符重载,欢迎去看一看我的【c++笔记】

 

         那我们应该怎样去使用运算符的重载呢?

         在说使用方法之前,我们先来回顾一下,有哪些运算符?(别说你到现在还不懂什么叫做运算符!!!)让你一个个说出来,你肯定记不全啦。那我们先来给运算符分个类:单目运算符双目运算符三目运算符特殊运算符

         我们按照运算符的类型一一为大家讲解。

二.单目运算符的重载

         什么叫单目?就是操作数只有一个。比如我们常见的:++--,!,~等等就是单目运算符。单目运算符也有两种形式变现:

1.#R

         运算符在操作数之前。这样的话,编译器会先去R这个对象对应的类型中找一个成员函数叫operator#()的。如果这个类中没有这个成员函数就去全局域中找一个全局函数叫operator#(R)

         从以上这段话我们可以知道,运算符重载先是找成员函数,再是找全局函数。而且函数形式比较特殊,是operator关键加上要重载的运算符的符号#

我们先来一起写一个成员函数的运算符重载。但是写之前你要好好考虑一下这个函数的返回值问题。我们以前++运算符为例,写一下运算符重载:

#include <iostream>using namespace std;class Integer{    int data;public:    Integer(int data=0):data(data){}    void operator++(){        ++data;    }    void show(){        cout<<data<<endl;    }};int main(){    Integer a(10);    ++a;    a.show();    return 0;}

 

       我们发现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=0):data(data){}    Integer operator++(){        ++data;    }    void show(){        cout<<data<<endl;    }};int main(){    Integer a(10);    ++++a;    a.show();    return 0;}

       函数运行很成功,没有报错。可是结果就差强人意了。怎么还是11呢?我们明明做了两次前加加啊!!!结果应该是12才对。

       我们一起回顾一下前几篇文章中提到的:拷贝构造函数(不懂的请看【c++笔记】)。如果我们的返回值类型是Integer类型的,返回出去的类型就会发生拷贝构造。++++a => ++++a),实际上第二次++的是一个临时的Integer变量,并不是对象a本身。那么怎么解决这个问题呢?我们需要用到引用(不懂的请看【c++笔记】)!     这样我们就能确保函数返回的是对象本身了。

 

       函数一切正常,如我们预期的做了两次前++


       如此这般,对于其他单目运算符的重载也类似。我们简单的对几个运算符进行重载。

#include <iostream>using namespace std;class Integer{    int data;public:    Integer(int data=0):data(data){}    Integer& operator++(){        ++data;  return *this;    }    Integer& operator!(){        data =  !data;return *this;    }    Integer& operator~(){        data = ~data;  return *this;    }    void show(){        cout<<data<<endl;    }};int main(){    Integer a(10);    ++a;    a.show();    !a;    a.show();    ~a;    a.show();    return 0;}

 

那运算符写成全局函数应该怎么写呢?也很简单,把运算符重载的函数实现体写在全局域中,参数列表加上Integer类型就行了。可是问题来了:

 

       我们要对对象a的值data进行修改,可是data的值是private权限的,我们在类外更改不了啊!!!怎么办呢?这里需要引入友元函数的概念。

       什么是友元函数?它是先在类中定义非成员函数并在函数体之前加上friend关键字,让这个非成员函数成为这个类的朋友,然后这个函数就可以随意访问该类的任何权限的成员了。我们对程序稍加修改,加上友元函数:

#include <iostream>using namespace std;class Integer{    int data;public:    Integer(int data=0):data(data){}    void show(){        cout<<data<<endl;    }    friend Integer& operator++(Integer&);};Integer& operator++(Integer& a){    ++a.data;    return a;}int main(){    Integer a(10);    ++++a;    a.show();    return 0;}

        函数运行起来一切正常啦!

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=0):data(data){}    Integer operator++(int){        return Integer(data++);    }    void show(){        cout<<data<<endl;    }};int main(){    Integer a(10);    a++.show();    a.show();    return 0;}


        关于全局域中的运算符重载这里就省略了。同1

三.双目运算符

        所谓双目,就是操作数是两个的,如:+-*/都是双目运算符。

        形如:L#R

        先去L对象对应的类型中找一个成员函数叫 operator#(R) ,如果找不到就去全局域中找一个全局函数叫operator#(L,R),最后综合选择最优的函数调用。

        其实双目和单目的操作很类似。还是一起来看例子,我们就先来一起完成本文开篇时用的程序,完成两个Integer对象的相加操作。

#include <iostream>using namespace std;class Integer{    int data;public:    Integer(int data=0):data(data){}    void show(){        cout<<data<<endl;    }    Integer operator+(Integer& b){        return Integer(data+b.data);    }};int main(){    Integer a(10);    Integer b(20);    Integer c=a+b;    c.show();    return 0;}
 

       很简单就实现了想加的功能啦,当然全局的运算符重载函数你肯定也会自己写啦。当然运用什么样的返回值,根据你的设计需要来设计啦。一般来说两数相加减乘除都是作为右值的,如果想做为左值返回就自己修改返回的类型吧。

四.特殊的运算符

       你有没有发现,我并没有跟你讲三目运算符?是我老年痴呆了吗?还没到那个年龄啦。并不是所有的运算符都能重载的哦!!!

       那有哪些运算符不能重载呢?有:

       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=0):data(data){}    void show(){        cout<<data<<endl;    }    Integer operator+(Integer& b){        return Integer(data+b.data);    }    friend ostream& operator<<(ostream&,Integer&);    friend istream& operator>>(istream&,Integer&);};ostream& operator<<(ostream& os,Integer& a){    os<<a.data;    return os;}istream& operator>>(istream& is,Integer& a){    is>>a.data;    return is;}int main(){    Integer a;    cin>>a;    cout<<a<<endl;    return 0;}

 

        我们从键盘输入1234,然后输出了1234。这样,就轻松的完成了类的输入和输出了。想输出什么样的格式,你说了算!

 

2.newdelete的重载

       newdelete也能重载?!当然了。除了我们说的那五种不能重载的运算符,其他的运算符只要你看到过的,都可以重载。

       在重载之前,我们需要知道一点:newdelete的底层是通过mallocfree实现的。所以在重载之前你要知道我们得通过mallocfree来完成函数的实现。

              void*    operator new (size_t size);

              void     operator delete(void* ptr);

       以上就是就是newdelete的函数原型。我们一起来写代码看看:

#include <iostream>#include <cstdlib>using namespace std;class Integer{    int data;public:    Integer(int data=0):data(data){}    void show(){        cout<<data<<endl;    }    static void* operator new(size_t size){        cout<<"size:"<<size<<endl;        return malloc(size);    }    static void operator delete(void* ptr){        cout<<"这是重载的delete运算符!"<<endl;        free(ptr);    }};int main(){    Integer* a = new Integer();    delete a;    return 0;}

3.赋值运算符“=

       其实重载“=”的实现很好写,但是我们还应该更加关注牵涉到的内存问题。我们回想一下为什么我们需要拷贝构造函数?就是为了保证内存的独立性。其实,赋值运算符同样需要考虑这个问题。

       比如,你把一个含有分配了堆内存的对象赋值给另外一个对象时,他们的成员指针不能简单的指向相同的堆内存地址,需要自己额外申请一片堆内存。

       我们还是写个代码:

#include <iostream>using namespace std;class Integer{    int* data;    int size;public:    Integer(int size=1):size(size){        data = new int[size];        for(int i=0;i<size;i++)            data[i]=i+1;    }    void show(){        for(int i=0;i<size;i++)            cout<<data[i]<<" ";        cout<<endl;    }    Integer& operator=(Integer& a){        size=a.size;        data=new int[size];        for(int i=0;i<size;i++)            data[i]=a.data[i];    }    ~Integer(){        delete[] data;    }};int main(){    Integer a(10);    a.show();    Integer b;    b = a;    b.show();    return 0;}

        我们把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=NULL):data(data){}    ~autoPtr(){        delete data;    }    void showPtr(){        cout<<"showPtr()"<<endl;    }    A* operator->(){        return data;    }    A operator*(){        return *data;    }};int main(){    A *a = new A();    a->show();     autoPtr ap(a);    ap.showPtr();     ap->show();    (*ap).show();    return 0;}<span style="background-color: rgb(255, 255, 255); font-family: Arial, Helvetica, sans-serif;"> </span>


我们在类autoPtr中重载了“->*”,使得autoPtr的对象ap表现的像一个指针,达到了智能指针的作用。

 

6.[ ]下标运算符

这种运算符一般用在数组的取值上。看一个简单的程序你就懂怎么去使用了:

#include <iostream>using namespace std;class Array{    int* data;    int size;public:    Array(int size=0):size(size){        data = new int[size];        for(int i=0;i<size;++i)            data[i] = i+1;    }    int operator[](int flag){        return data[flag];    }};int main(){    Array arr(10);    cout<<arr[5]<<endl;    return 0;}
 

 

------------------------------------结束语--------------------------------------

       今天我们主要讲解了:单目运算符、双目运算符和特殊运算符的重载(>><<,newdelete(),=(),->*[ ])。还介绍了五种不能重载的运算符,告诉了大家运算符重载的三条原则。希望大家看完这篇文章之后,能学到很多知识!

       最后,给大家布置一个作业:

       设立一个数组类,能动态决定数组的大小(重键盘输入)。能对数组进行输入、输出,能完成数组类的两个对象之间的各种运算操作:

+ - * = ! ~  ++  --。能运用[ ]取对象中的值。当然你还可以扩展其他功能,灵活运用本文说提到的所有知识点。

       写好之后我们可以相互交流一下,我的QQ137332024

 

1 0
原创粉丝点击