函数运算符重载

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<。你当然可以在这里使用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<

我们也可以用同样的方式定义出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,abool。下面定义一个简单的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等关联容器默认使用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(numerator_)/denominator_;
}

7.完了

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

你可能感兴趣的:(函数运算符重载)