一文帮助深入理解C++运算符的重载
声明Rational有理数类
运算符重载是C++的一个十分有意思的特性,能使除了基本变量之外的,其他的C++类声明也能实现基本的运算,下面将从一个有理数类Rational
的实现与扩展,在这个类的深入设计过程中思考运算符的运用,以达到深入理解C++运算符重载的目的,有理数Rational
相比于double
能更精确地表示一个数据,实现一个Rational
类能提供一种精确表达的方法, 但是Rational
并不是C++的基本类型,需要我们手动实现,下面为Rational
用例图的定义
上述样例图声明了Rational
的方法,其代码实现为
// Rational.h
class Rational{
public:
Rational(); // 初始化一个默认有理数
Rational(int n, int d); // 根据传入的分子分母初始化一个有理数
Rational(int n); // 传入一个整数n,构建n/1有理数
int getNumerator() const; // 获取分子
int getDenominator() const; // 获取分母
Rational add(const Rational &secR) const; // 有理数相加
Rational subtract(const Rational &secR) const; // 有理数相减
Rational multiply(const Rational &secR) const; // 有理数相乘
Rational divide(const Rational &secR) const; // 有理数相除
int compareTo(const Rational &secR) const; // 有理数比较,大于返回1,等于返回0,小于返回-1
bool equals(const Rational &secR) const; // 判断两个有理数是否相等
int intValue() const; // 返回有理数的int值,向下取整
double doubleValue() const; // 返回有理数的双精度值
std::string toString() const; // 返回有理数的字符串描述
private:
int numerator; // 分子
int denominator; // 分母
static int gcd(int n, int d); // 求n和d的最大公约数,用于化简为最简形式
};
// Rational.cpp
#include "Rational.h"
#include <sstream>
#include <cstdlib>
#include <assert.h>
// default constructor
Rational::Rational():numerator(0), denominator(1) {}
// constructor
Rational::Rational(int n, int d) {
assert(d != 0);
int factor = Rational::gcd(n, d);
this->numerator = ( (d>0) ? 1 : -1 ) * n / factor;
this->denominator = abs(d)/factor;
}
Rational::Rational(int n):numerator(n), denominator(1) {}
// return the value of the numerator
int Rational::getNumerator() const {
return this->numerator;
}
// return the value of the denominator
int Rational::getDenominator() const {
return this->denominator;
}
// add two rational and return the result
Rational Rational::add(const Rational &secR) const {
int n = numerator * secR.getDenominator() + denominator * secR.getNumerator();
int d = denominator * secR.getDenominator();
return Rational(n, d);
}
// subtract two rational and return the result
Rational Rational::subtract(const Rational &secR) const {
int n = numerator * secR.getDenominator() - denominator * secR.getNumerator();
int d = denominator * secR.getDenominator();
return Rational(n, d);
}
// multiply two rational and return the result
Rational Rational::multiply(const Rational &secR) const {
int n = numerator * secR.getNumerator();
int d = denominator * secR.getDenominator();
return Rational(n, d);
}
// divide two rational and return the result
Rational Rational::divide(const Rational &secR) const {
int n = numerator * secR.getDenominator();
int d = denominator * secR.getNumerator();
return Rational(n, d);
}
// Find GCD of two numbers
int Rational::gcd(int n, int d) {
int n1 = abs(n);
int n2 = abs(d);
int gcd = 1;
for(int k = 1 ; k <= n1 && k <= n2; k++){
if(n1 % k == 0 && n2 % k == 0)
gcd = k;
}
return gcd;
}
/**
* @param secR
* @return:
* this > secR -> 1
* this = secR -> 0
* this < secR -> -1
*/
int Rational::compareTo(const Rational &secR) const {
Rational rad = subtract(secR);
if(rad.getNumerator() > 0)
return 1;
else if(rad.getNumerator() < 0)
return -1;
return 0;
}
// compare two rational judge if the value are equal
bool Rational::equals(const Rational &secR) const {
Rational rad = subtract(secR);
if(rad.getNumerator() == 0)
return true;
return false;
}
// return the int value of the rational
int Rational::intValue() const {
return numerator/denominator;
}
// return the double value of the rational
double Rational::doubleValue() const {
return 1.0 * numerator / denominator;
}
// to string
std::string Rational::toString() const {
std::stringstream ss;
ss << numerator;
if(denominator > 1)
ss << '/' << denominator;
return ss.str();
}
上述代码已经实现了一个比较全面的有理数类,能够做很多的事情,比如加、减、乘、除、比较、转换为double、toString等等,已经可以说是很全面了,比如下面的例子
Rational r1(2, 3), r2(5, 8), r3;
r3 = r1.add(r2); // r3 = r1 + r2
从上面的例子中,首先定义了两个有理数r1,r2
,然后他们在相加将得到的结果赋值给·r3
,但是有问题的一点是,这样的加法似乎不太满足我们日常的惯例写法,能不能直接写成r3 = r1 + r2
的形式?答案肯定是可以的,下面我们就来一个个的实现运算符的重载,并分析运算符重载过程中需要注意的事情
一步一步实现各种运算符的重载
C++是支持运算符的重载的,但不是所有运算符可以重载的,可以重载的运算符如下,并且相同的运算符进行不同的重载它所代表的含义是不同的
+ | - | * | / | % | ^ | & | | | ~ | ! | = |
---|---|---|---|---|---|---|---|---|---|---|
< | > | += | -= | *= | /= | %= | ^= | &= | |= | << |
>> | <<= | >>= | == | != | <= | >= | && | || | ++ | – |
->* | , | -> | [] | () | new | delete |
上面的运算符是可以重载的,而下面的运算符是不可以重载的
?: | . | .* | :: |
---|
Note
:
- C++本身定义了运算符的结合性和优先级,重载运算符并不能改变运算符的结合性和优先级
运算符重载本质上是一个函数,只是其调用的方式跟平常的函数调用a.fun()
不同,比如前面的有理数类实现了+
加法运算符,**那么r1 + r2等价于r1.operator+(r2)**是不是跟函数调用很像?下面就着手实现这些运算符,并从中分析其中的一些需要注意的点
重载加减乘除
+,-,*,/
…这些都是二元运算符,顾名思义二元运算符需要两个操作数,加法需要一个加数,和一个被加数,这就是二元的,这里是需要注意的是,如果说一个类重载了+
运算符,他不一定是重载了加运算符,因为+
它本身不止一个含义,因为它既可以作为二元运算符r1+r2
,也可以是一元运算符+r1
,他们的重载方式是不一样的,这些二元运算符的声明如下:
// 声明
Rational operator + (const Rational &secR) const;
Rational operator - (const Rational &secR) const;
Rational operator * (const Rational &secR) const;
Rational operator / (const Rational &secR) const;
// 实现
Rational Rational::operator+(const Rational &secR) const {
return add(secR);
}
Rational Rational::operator-(const Rational &secR) const {
return subtract(secR);
}
Rational Rational::operator*(const Rational &secR) const {
return multiply(secR);
}
Rational Rational::operator/(const Rational &secR) const {
return divide(secR);
}
有了上面的重载,我们就可以在代码中直接这样写了
r3 = r2 + r1; // <==> r3 = r2.operator+(r1);
重载[]
C++的数组, map, vector,他们都支持[]
运算符,那么能不能让Rational
也支持[]
呢,让r1[0]
返回分子,r1[其他整数]
返回分母?答案显而易见,完全可以的!
// 重载[]运算符
int operator[](int index);
// 实现
int Rational::operator[](int index) {
if(index == 0)
return numerator;
return denominator;
}
经过上面的重写,我们也已经可以调用[]
来访问有理数的分子分母了!但是,map,vector等都支持[]
访问并且可以通过[]
来修改其中的值,而上面的重载能不能通过[]
来改变分子分母的值呢?答案是不可以的,因为从上面的声明可以看到,返回的是一个int
类型,即返回的是以分子或分母的一个右值,而右值是用于赋值的,这样的声明不会对分子分母的值造成影响,如果要支持修改特性,或者不仅仅返回值,我们还要拿到这个属性的修改权的话,我们需要对返回值做一些手脚
// 重载[]运算符
int& operator[](int index);
// 实现
int& Rational::operator[](int index) {
if(index == 0)
return numerator;
return denominator;
}
当返回值变成引用的话,就可以实现修改属性值的效果,这也是在重载过程中需要注意的一个点
重载()
重载了()
的类一般有另一种别称,仿函数
,实现了这个运算符的类,可以像调用函数一样调用这个类,为什么要实现这个运算符?因为这类仿函数相对于真正的函数来说,不仅仅能实现函数的功能,而且能持有自己的特性和状态
void operator()();
void Rational::operator()(){
printf("I am a rational!\n");
}
重载简写运算符
简写运算符是类似于+=, *=, -=, /=
这类的运算符,他的重载方式也十分简单,但是也需要注意与前面[]
运算符同样的问题,一般这类运算符是可以重复调用的,比如
int x = 3;
x += 3 += 3; // 这样是合理的,要更明确一点的话 (x += 3)+= 3
从上面可以看出,这类运算符的重载的返回值也是有考究的,也应该是引用类型,需要我们在重载的时候要注意这个细节
Rational& operator+=(const Ratioanl& secR);
Rational& Rational::operator+=(const Rational& secR){
*this = add(secR);
return *this;
}
有了上面的声明与重载,那么下面的测试代码会输出
Rational r1(2, 4);
Rational r2 = r1 += Rational(2, 3);
cout<<"r1 is "<<r1.toString()<<endl;
cout<<"r2 is "<<r2.toString()<<endl;
r1 is 7/6
r2 is 7/6
重载一元运算符
正如之前提到的+
这个运算符它不仅是二元运算符r1 + r2
,还可以是一元运算符+r2
,而这类运算符怎么声明?其格式如下,比如说+ -
两个一元运算符的声明
Rational operator+();
Rational operator-();
Rational Rational::operator-() {
return Rational(-numerator, denominator);
}
Rational Rational::operator+() {
return Rational(numerator, denominator);
}
在一元运算符中,有两个比较特殊的++和--
,这两个运算符有前缀和后缀一说, 一般前缀运算符是左值的,而后缀运算符则不是,前缀运算符的重载返回值为引用类型,并且两者的重载声明也有区别,如下
// 前缀 Prefix
Rational& operator++();
// 后缀 PostFix
Rational operator++(int dummy);
// 实现
Rational& Rational::operator++() {
int n = numerator + denominator;
int d = denominator;
int factor = Rational::gcd(n, d);
this->numerator = ( (d>0) ? 1 : -1 ) * n / factor;
this->denominator = abs(d)/factor;
return *this;
}
Rational& Rational::operator--() {
int n = numerator - denominator;
int d = denominator;
int factor = Rational::gcd(n, d);
this->numerator = ( (d>0) ? 1 : -1 ) * n / factor;
this->denominator = abs(d)/factor;
return *this;
}
Rational& Rational::operator++(int) {
int n = numerator + denominator;
int d = denominator;
int factor = Rational::gcd(n, d);
this->numerator = ( (d>0) ? 1 : -1 ) * n / factor;
this->denominator = abs(d)/factor;
return *this;
}
Rational& Rational::operator--(int) {
int n = numerator - denominator;
int d = denominator;
int factor = Rational::gcd(n, d);
this->numerator = ( (d>0) ? 1 : -1 ) * n / factor;
this->denominator = abs(d)/factor;
return *this;
}
用友元函数重载流运算符<<, >>
C++允许重载>>
和<<
流运算符,但是这些运算符必须以非成员的形式重载,也就是说一般以友元函数的形式重载流运算符,其声明形式为
friend std::ostream& operator<<(std::ostream& out, const Rational& secR);
friend std::istream& operator>>(std::istream& in, Rational& secR);
// 实现
std::ostream& operator<<(std::ostream &out, const Rational &secR){
out<<secR.numerator<<"/"<<secR.denominator;
return out;
}
std::istream& operator>>(std::istream &in, Rational &secR){
int n,d;
in>>n>>d;
secR = Rational(n, d);
return in;
}
经过以上的声明,我们就可以通过cout
和cin
来输出和输入有理数类了
Rational r1;
cin>>r1;
cout>>r1;
5 2
5/2
自定义类型转换
前面也提到,intValue和doubleValue
能将一个有理数分别转换成一个int
值和一个double
值,那能不能自动转换呢?而且int能不能自动转换成Rational呢?答案当然是可以滴!
首先来看Rational
自动转换成int
和double
// 声明
operator double();
operator int();
// 实现
Rational::operator double(){
return doubleValue();
}
Rational::operator int(){
return intValue();
}
Rational r1(3,2);
int a = 1 + r1; // r1 就被自动转换为int了,a = 2
然后再看int
转换成Rational
,可以通过添加一个构造函数的形式来实现自动转换,可以声明并实现Rational(int n)
构造函数来实现。
Rational(int n):numerator(n), denominator(1){}
Rational r1(2,3), r2;
r2 = r1 + 1; // r2 = 5/3
前面的表达式r2 = r1 + 1
其实本质上是r2 = r1.operator+(1)
,而因为实现了这个构造函数,则会隐式地调用Rational(int n)
这个构造函数,前提是Rational(int n)后面没有explict
,但是这里有一个很容易忽视的致命问题,如果上述两种转换都声明与实现了的话。那么会导致两种转换都编译不通过,因为会导致歧义,所以最好只保留一个,假如double
、int
和int到Rational的类型转换都有了,那么r1 + 1
可能会被翻译成
int + int
double + int
Rational + Rational
.....
从而造成歧义!这里一定要注意
总结
关于运算符重载,笔者个人的总结有,我们需要知道
- 哪些运算符可以重载,哪些运算符不能重载
- 要注意区分一元运算符和二元运算符,因为有些是有同一个运算符会有不同的涵义,比如+
- 即使是一元运算符,有的可能有前缀和后缀之分,他们的重载也有区别,比如
++和--
- 要注意重载的运算符是不是左值运算符,即返回值要不要用引用,要用引用的有
[] ++和--的前缀表示....
- 流运算符要用非成员函数重载(即友元函数)