美文网首页
函数运算符重载

函数运算符重载

作者: 琼蘂无徵朝霞难挹 | 来源:发表于2019-08-20 12:50 被阅读0次

    0.如何重载函数运算符

    三种方法:friend function、common function以及member function,下面一一阐述

    1.挑个简单的入手

    假设我们需要定义一个类型表示分数,简单定义如下·:

    class fraction{
    public:
        fraction(int numerator,int denominator):numerator_(numerator),denominator_(denominator){
    
        }
    
    private:
        int numerator_;
        int denominator_;
    };
    

    那么我们需要给这个类型定义一些简单的运算,例如加减乘除等,定义加法操作需要重载operator+operator+是一个binary的运算符,他接受两个参数,返回一个fraction的新对象。
    我们先使用友元函数来重载operator+

    //fraction.h
    class fraction{
    public:
        fraction(int numerator,int denominator):numerator_(numerator),denominator_(denominator){
    
        }
        friend fraction operator+(const fraction& lhs, const fraction& rhs);
    private:
        int numerator_;
        int denominator_;
    };
    fraction operator+(const fraction& lhs, const fraction& rhs);
    //fraction.cpp
    fraction operator+(const fraction &lhs, const fraction &rhs) {
        int demonimator=lhs.denominator_*rhs.denominator_;
        int numerator=lhs.numerator_*rhs.denominator_+rhs.numerator_*lhs.denominator_;
        int gcd=tiny_utils::gcd(numerator,demonimator);
        if(gcd==0){
            return fraction(0,1);
        }
        else{
            return fraction(numerator/gcd,demonimator/gcd);
        }
    }
    

    要注意friend函数在类内仅仅是指定该函数为类的友元函数,并不是函数的声明,所以我们要在下方再次声明这个函数,然后源文件中完成定义。虽然经测试,gcc不必进行这个声明,但是最好还是声明一下,这样代码在所有编译器上都能通过。friend函数也可以直接在类型声明,不过这种函数最好还是遵守声明与定义分离的原则。
    我们也可以使用common function实现operator+,实现大致差不多,如下所示:

    //fraction.h
    class fraction{
    public:
        fraction(int numerator,int denominator):numerator_(numerator),denominator_(denominator){
    
        }
        const int& getNumerator()const {
            return numerator_;
        }
        const int& getDenominator()const {
            return denominator_;
        }
        //friend fraction operator+(const fraction& lhs, const fraction& rhs);
    private:
        int numerator_;
        int denominator_;
    };
    fraction operator+(const fraction& lhs, const fraction& rhs);
    //fraction.cpp
    fraction operator+(const fraction &lhs, const fraction &rhs) {
        int demonimator=lhs.getDenominator()*rhs.getDenominator();
        int numerator=lhs.getNumerator()*rhs.getDenominator()+rhs.getNumerator()*lhs.getDenominator();
        int gcd=tiny_utils::gcd(numerator,demonimator);
        if(gcd==0){
            return fraction(0,1);
        }
        else{
            return fraction(numerator/gcd,demonimator/gcd);
        }
    }
    

    由于common function不能直接访问类的private成员,所以我们需要定义两个getter函数,其实他和friend function也就差一个访问权限。
    我们再使用member function来实现这个函数,实现member function时,我们就不需要两个参数了,因为操作符左边的operand已经由this提供,我们仅需要提供操作符右边的operand。

    //fraction.h
    class fraction{
    public:
        fraction(int numerator,int denominator):numerator_(numerator),denominator_(denominator){
    
        }
        const int& getNumerator()const {
            return numerator_;
        }
        const int& getDenominator()const {
            return denominator_;
        }
        fraction operator+(const fraction& other);
    private:
        int numerator_;
        int denominator_;
    };
    //fraction.cpp
    fraction fraction::operator+(const fraction &other) {
        int demonimator=denominator_*other.getDenominator();
        int numerator=numerator_*other.getDenominator()+other.getNumerator()*denominator_;
        int gcd=tiny_utils::gcd(numerator,demonimator);
        if(gcd==0){
            return fraction(0,1);
        }
        else{
            return fraction(numerator/gcd,demonimator/gcd);
        }
    }
    

    上面用三种不同的方式实现了operator+的重载,三者差别不大,我们接着看。

    2.member function的局限性

    对于分数计算来说,我们常常会将一个整数和一个分数来进行四则运算,那么此时上面重载的operator+就不够用了,此时我们需要重载新的函数以满足我们的要求,例如分数和整数运算或者整数和分数运算。同样我们先用friend function来实现:

    //fraction.h
    #define friend_func
    class fraction{
    public:
        fraction(int numerator,int denominator):numerator_(numerator),denominator_(denominator){
    
        }
        const int& getNumerator()const {
            return numerator_;
        }
        const int& getDenominator()const {
            return denominator_;
        }
    
    #ifdef mem_func
        fraction operator+(const fraction& other);
    #else
        friend fraction operator+(const fraction& lhs, const fraction& rhs);
        friend fraction operator+(const fraction& lhs,const int rhs);
        friend fraction operator+(const int lhs,const fraction& rhs);
    #endif
    private:
        int numerator_;
        int denominator_;
    };
    #ifndef mem_func
    fraction operator+(const fraction& lhs, const fraction& rhs);
    fraction operator+(const fraction& lhs,const int rhs);
    fraction operator+(const int lhs,const fraction& rhs);
    #endif
    //fraction.cpp
    fraction operator+(const fraction &lhs, const fraction &rhs) {
        int demonimator=lhs.denominator_*rhs.denominator_;
        int numerator=lhs.numerator_*rhs.denominator_+rhs.numerator_*lhs.denominator_;
        int gcd=tiny_utils::gcd(numerator,demonimator);
        if(gcd==0){
            return fraction(0,1);
        }
        else{
            return fraction(numerator/gcd,demonimator/gcd);
        }
    }
    fraction operator+(const fraction &lhs, const int rhs) {
        fraction rhsf(rhs,1);
        return lhs+ rhsf;
    }
    
    fraction operator+(const int lhs, const fraction &rhs) {
        fraction lhsf(lhs,1);
        return lhsf+ rhs;
    }
    

    对于member函数,我们没有办法满足这个要求,所以说一旦我们需要定义不同类型之间的binary运算,friend function是更好的选择,当然你也可以选择common function,随你的便😄。
    在定义完operator+之后,我们可以作为依照,将operator-operator/operator*都定义出来。

    3.看看运算结果吧

    当我们完成运算时,最想看到的莫过于他的结果,所以如何将结果友好地输出出来就成了下一项工作,也就是我们喜闻乐见的operator<<operator<<也是一个binary的操作符,他接受一个std::ostream&作为左边的operand,因为std::ostream的拷贝构造函数是delete的,所以这里必须使用引用,又因为我们要向流中输出一些东西,所以不能是const引用。返回类型这里不再像operator+那样使用std::ostream,而是使用std::ostream&,一是因为std::ostream是不可拷贝的,二是这样方便我们进行链式调用,简而言之就是out<<f1<<" "<<f2。你当然可以在这里使用void作为返回类型,那么你就不能链式调用了。除非你跟用户有仇,想要折磨他,那么你最好还是不要使用void,你高兴就好😄。下面是一个简单的operator<<重载,使用friend function实现,对于其他实现方法,就不一一赘述了。

    //fraction.h
    #define friend_func
    class fraction{
    public:
        fraction(int numerator,int denominator):numerator_(numerator),denominator_(denominator){
    
        }
        const int& getNumerator()const {
            return numerator_;
        }
        const int& getDenominator()const {
            return denominator_;
        }
    
    #ifdef mem_func
        fraction operator+(const fraction& other);
    #else
        friend fraction operator+(const fraction& lhs, const fraction& rhs);
        friend fraction operator+(const fraction& lhs,const int rhs);
        friend fraction operator+(const int lhs,const fraction& rhs);
        friend std::ostream& operator<<(std::ostream& out,const fraction& rhs);
    #endif
    private:
        bool isValidFraction()const {
            return denominator_!=0;
        }
    private:
        int numerator_;
        int denominator_;
    };
    #ifndef mem_func
    fraction operator+(const fraction& lhs, const fraction& rhs);
    fraction operator+(const fraction& lhs,const int rhs);
    fraction operator+(const int lhs,const fraction& rhs);
    std::ostream& operator<<(std::ostream& out,const fraction& rhs);
    #endif
    //fraction.cpp
    std::ostream& operator<<(std::ostream &out, const fraction &rhs) {
        if(rhs.isValidFraction()){
            if(rhs.numerator_%rhs.denominator_)
                out<<rhs.numerator_<<"/"<<rhs.denominator_;
            else
                out<<rhs.numerator_;
        }
        else{
            out<<"invalid fraction";
        }
        return out;
    }
    

    我们也可以用同样的方式定义出operator>>,这里不再赘述。

    4.再来个++吧

    对于内置类型intiterator等,标准提供了operator++operator--的操作。如果你想说这就是两个函数,我可是有前置++和后置++的,别急,标准库都为我们定义好了,看下表:

    操作名 语法 类内定义 类外定义
    前自增 ++a T& T::operator++(); T& T::operator++(T& a);
    后自增 a++ T T::operator++(int); T T::operator++(T& a,int);
    前自减 --a T& T::operator--(); T& T::operator--(T& a);
    后自减 a-- T T::operator--(int); T T::operator--(T& a,int);

    其实前后自增的区别就是多了一个int参数,这个参数只有区分前后自增的作用,没有其他任何作用,在调用时默认会传入0,如果你闲的蛋疼,也可以通过operator++(1)把他设成1,那么我猜你跟前面用void做返回类型的是同一种人😅。
    从上表的类内定义来看,前置操作返回的是operand的引用,后置操作返回的是操作数的一个prvalue,你可以理解为一个右值。两者内在的区别就是前置操作的返回值严格等于operator+=或者operator-=,而后者进行自增后返回的是自增前的值。在了解了这些后,我们要重载operator++就很轻松了。而且只要你把上面的都看懂,遇到一些白痴问题例如:int a=0;++(i++);的结果是什么的时候你就能应答自如。很显然上面的白痴表达式不能通过编译,因为你不能将一个右值赋给一个左值引用,除非他是const的😂。
    但是如何为分数定义一个自增操作呢?我们应该在自增后让分数的值加一还是只让分母加一,前者显然更合理,但是如果一个操作不够明显或者容易让人产生误会,那么你最好在声明的地方加上注释,或者干脆直接不重载该运算符。这里我们实现一个让值加一的版本:

    //declaration
    //pre increasement
    fraction& operator++(fraction& operand);
    //post increasement
    fraction operator++(fraction& operand,int flag);
    //implementation
    fraction &operator++(fraction &operand) {
        operand.numerator_+=operand.denominator_;
        return operand;
    }
    
    fraction operator++(fraction &operand, int flag) {
        fraction prvalue=operand;
        operand.numerator_+=operand.denominator_;
        return prvalue;
    }
    

    很简单,对于自减操作也同样如此,不再赘述。

    5.顺便说一下operator+=

    前面提到前置自增的结果严格等于operator+=(1),我们根据这个来定义一个operator+=操作,这个重载你同样可以像operator+那样指定不同的类型,因为他也是一个binary的运算符,简单定义两个:

    //declaration
    fraction& operator+=(fraction& lhs, const fraction& rhs);
    fraction& operator+=(fraction& lhs, const int rhs);
    //implementation
    fraction &operator+=(fraction &lhs, const fraction &rhs) {
        int demonimator=lhs.denominator_*rhs.denominator_;
        int numerator=lhs.numerator_*rhs.denominator_+rhs.numerator_*lhs.denominator_;
        int gcd=tiny_utils::gcd(numerator,demonimator);
        if(gcd==0){
            lhs.numerator_=0;
            lhs.denominator_=1;
        }
        else{
            lhs.numerator_=numerator/gcd;
            lhs.denominator_=demonimator/gcd;
        }
        return lhs;
    }
    
    fraction &operator+=(fraction &lhs, const int rhs) {
        fraction rhsf(rhs,1);
        return operator+=(lhs,rhsf);
    }
    

    operator+不同的是,我们这里也选择了返回一个引用,这不仅可以避免不必要的拷贝,还为我们的fraction类提供了类似于内置int型的链式调用,例如:(f1+=2)+=3;当然你得在调用时加上括号表示优先级,否则运算符优先级会先调用2+=3,两个int&&作为操作数,这与我们重载的任何operator+=都不符合,无法通过编译。事实上,你也无法定义一个两个参数类型都是内置类型的运算符重载,假设编译器允许这样做,在你调用两个内置类型进行运算时,到底应该选择哪个函数来调用呢?

    6.比较一下大小吧

    在定义好数值运算的运算符重载后,对于一个分数而言,比较他们的大小也是极其重要的操作。可以重载的比较操作符由==!=>>=<<=以及<=>,其中最后一个operator<=>C++20新标准增加的three-way comparator,它接受两个参数a和b,如果a>b,那么返回一个正值,如果a==b,那么返回0,a<b则返回一个负数。比较运算符也是binary的运算符,返回值为bool。下面定义一个简单的operator==

    //declaration
    bool operator==(const fraction& lhs, const fraction& rhs);
    //implementation
    bool operator==(const fraction &lhs, const fraction &rhs) {
        if(!lhs.isValidFraction()||!rhs.isValidFraction())
            return false;
        int lGcd=tiny_utils::gcd(lhs.numerator_,lhs.denominator_);
        int rGcd=tiny_utils::gcd(rhs.numerator_,rhs.denominator_);
        return ((lhs.numerator_/lGcd)==(rhs.numerator_/rGcd))&&
            ((lhs.denominator_/lGcd)==(rhs.denominator_/rGcd));
    }
    

    std::unordered_set<T>等关联容器默认使用operator<作为Compare的模板参数,但是在我们在定义了比较运算符后,我们仍然不能将fraction用在关联容器上,还需要为fraction定义一个`function object用作哈希函数,可以参考我的另一篇文章如何在关联容器中使用自定义类型

    6.与内置类型的交互

    对于一个分数,如果我们像将他存储为一个double类型的数该怎么做,一种办法就是提供一个public的方法const double getDoubleValue()const,我们在这个方法里面提供一个计算。当然你也可以提供一个private的成员变量double double_val_,在构造分数时就完成浮点运算。这看上去有点蠢,因为如果你对该分数进行了上面所讲的自增或者自减操作,你还得重新计算浮点数。
    还有一种办法就是重载operator double,不过我建议你最好慎重考虑,因为一旦定义了类型运算符重载,你的代码可能会产生意想不到的结果。对于这里的情况,如果我们定义了operator double,那么我们就绝对不能再定义接受double类型的运算符重载了,因为你定义了也没用,编译器会把fraction隐式转换成double,然后用double执行内置的算数操作😄。一般来说,除了operator bool,我们不应该定义类型转换操作符,如果非要定义,最好定义显式的类型转换运算符:

    explicit operator double ()const {
        return static_cast<double >(numerator_)/denominator_;
    }
    

    7.完了

    关于常用的操作符重载就差不多是这样了,operator[]等不常用的就不写了,主要我也没用过这些😄。

    相关文章

      网友评论

          本文标题:函数运算符重载

          本文链接:https://www.haomeiwen.com/subject/nejusctx.html