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.再来个++吧
对于内置类型int
和iterator
等,标准提供了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[]
等不常用的就不写了,主要我也没用过这些。