运算符重载概念
之前讲的重载包括构造、析构、拷贝复制的函数,但是一个完整的分数类的实现还远远不够,例如常见的四则运算还不能借助 + - * /
计算结果,分数之间的用 =
赋值也不能实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| class Fraction{ private: int num,den; public: Fraction() :Fraction(0,1){ } Fraction (int n):Fraction(n,1){ } Fraction(int n,int d):num(n),den(d){ } Fraction(const Fraction& f); void output()const ; double getValue()const; void copy(const Fraction& f); ... };
int main(){ Fraction a(3,4),b(4,5); a.output(); a.copy(b); a.output(); return 0; }
|
当然我们可以向下面这样写一个 add
成员函数来解决上述问题,但是使用起来并不直观,平常我们用的官方的库函数都是可以直接用运算符计算的,所以重载运算符也是很重要的
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| Fraction Fraction ::add(const Fraction &f)const{ int x,y; x=num*f.den+den*f.num; y=den*f.den; Fraction temp(x,y); return temp; }
int main() { c=a.add(b); return 0; }
|
- 表达式
9/2=4
,而 9.0/2.0=4.5
。这里的同一个运算符 /
,由于所操作的数据不同而具有不同的意义——就是因为对于运算符也重载了很多版本
- 本质上 C++ 都是由函数组成的,在 C++ 内部任何运算都是通过函数来实现的。在处理表达式
8+7
时,C++ 将这个表达式解释成如下的函数调用表达式:operator+(8,7);
和那个成员函数在 C++ 内部穿一个常指针 this 类似,运算符在 C++ 内部也被转化为一个函数进行执行,只不过再调用的时候格式不太一样
-
相同的运算符对不同数据有不同的操作,实质上是函数的重载
-
C++已经为各种基本数据类型定义了可能的运算符函数
1 2 3 4
| operator + (int,int) operator - (int,int) operator / (int,int); operator / (double, double);
|
-
同理想让类的对象也能使用这些运算符,就需要重载对应的运算符。一般都是使用友元函数和非静态成员函数两种方法实现运算符重载,当然语法上来说也可以用普通函数实现
有的教材传入错的观点,就好像重载运算符必须用友元或者成员函数,其实不然重载运算符其实本质上就是重载函数,所以重载函数的方法一般都可以使用
但是也不排除有的运算符必须用规定的方式重载!!C++ 规定 = [] () ->
这四个运算符只能被重载为类的非静态成员函数,其他的可以被友元重载,而 << >>
这两个运算符只能被重载为友元函数
成员函数与友元函数重载
加法运算符 +
重载为例
这里用重载 +
运算符为例讲解不同的重载方式,同理 - * /
也是按照如下方式重载
四则运算符:不改变函数传入的两个对象本身并返还一个临时对象(右值)
类型:双目运算符
成员函数重载
函数传入一个对象,因为调用成员函数本身就需要一个对象(已经传入隐含 this 指针),并且可以直接访问函数成员
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| class Fraction{ private: int num,den; public: Fraction() :Fraction(0,1){ } Fraction (int n):Fraction(n,1){ } Fraction(int n,int d):num(n),den(d){ } Fraction(const Fraction& f); void output()const ; Fraction operator+(const Fraction &f)const; };
Fraction Fraction::operator + (const Fraction& f)const{ return Fraction(num*f.den+den*f.num,den*f.den); }
int main(){ Fraction a(1,4),b(1,3),c; c=a+b; c.output(); return 0; }
|
其实 c=a+b
在内部执行时相当于是 c=a.operator+(b);
,operator 函数看为 a 对象调用了一个加法函数并传入对象 b 作为参数
函数重载的一个注意点
1 2 3 4 5 6 7 8 9 10 11 12
| Fraction& Fraction::operator + (const Fraction& f)const { return Fraction(num*f.den+den*f.num,den*f.den); } int main() { Fraction a(3,4),b(5,6),c; c=a+b; c.output(); return 0; }
|
- 不同的运算符操作数不同,有的会修改对象有的不会,有的会返还临时结果(右值)有的会返还对象本身(左值)还可能会什么都不返还,所以重载运算符的时候要先明白其原理再动手实现
- 我们要明白为什么传入参数的时候可以使用
&
引用对象,因为传入的对象是一个左值(不是临时对象,而是函数之外就存在的),他不会随着调用函数的结束而生命周期结束,但是函数返还的结果是一个右值生命周期很短,在函数调用结束的时候就会被释放,那么此时引用返还的地址内容早就已经没了,这并不是我们想要的
- 那么什么时候可以返还引用呢?无疑对于对象
a
这种直接对对象本身进行操作并需要返还计算结果的函数就可以,因为返还的还是对象本身(左值)不会随着函数结束而释放空间
友元函数重载
函数传入两个对象,因为友元函数本质还是普通函数,但是可以借助对象名直接访问函数成员的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| class Fraction{ private: int num,den; public: Fraction() :Fraction(0,1){ } Fraction (int n):Fraction(n,1){ } Fraction(int n,int d):num(n),den(d){ } Fraction(const Fraction& f); void output()const ; Fraction operator + (const Fraction &a,const Fraction &b)const; };
Fraction Fraction::operator + (const Fraction &a,const Fraction &b)const{ return Fraction(a.num*b.den+a.den*b.num,a.den*f.den); }
int main(){ Fraction a(1,4),b(1,3),c; c=a+b; c.output(); return 0; }
|
普通函数重载
也是可以的但是一般不用,因为相较于前两中方法这种方法没有什么独特的优势。函数传入两个对象,但是普通函数只能借助间接函数访问函数成员的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| class Fraction{ private: int num,den; public: Fraction() :Fraction(0,1){ } Fraction (int n):Fraction(n,1){ } Fraction(int n,int d):num(n),den(d){ } Fraction(const Fraction& f); void output() const; int getnum() const; int getden() const; };
Fraction Fraction::operator + (const Fraction &a,const Fraction &b)const{ return Fraction(a.getnum()*b.getden()+a.getden()*b.getnum(),a.getden()*f.getden()); }
int main(){ Fraction a(1,4),b(1,3),c; c=a+b; c.output(); return 0; }
|
常用运算符重载
赋值运算符 =
重载
=
运算符:作用于类型相同的两个对象,将 =
右边的对象赋给左边的对象并返还左边的对象(左值)
类型:双目运算符
只能用成员函数重载,不能用友元函数重载
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| class Fraction { ... Fraction& operator=(const Fraction &f); };
Fraction& Fraction::operator =(const Fraction& f) { num=f.num; den=f.den; return *this; }
int main() { Fraction a(3,4),b,c; (c=a)=b; c.output(); return 0; }
|
为什么赋值运算符 =
返还对象呢?因为你默认它不返还对象那么上述 main() 函数中运算结果就错了,执行完 c=a
不返还对象那么 b 又给谁赋值呢
为什么不能用友元函数重载呢?按理说友元函数传入 =
左右两端的值进行修改再返还对象不也可以吗?主要是因为其他的运算符重载函数都会根据参数类型或数目进行精确匹配,但是 = [] () ->
四个不具有这种检查的功能,用友元定义就会出错
上述情况你利用友元函数重载好了分数的赋值运算符,但是对于上述情况编译器会通过隐式调用类型转换构造函数将 1 转换为一个临时 Fraction 对象从而编译通过匹配,如此非左值就出现在了等号的左边,但编译器不会认为它出错,但这样破坏了 =
的语义。但是如果是成员函数重载 =
左边的非左值会被发现报错
加等于运算符 +=
重载
+=
运算符:作用于类型相同的两个对象,将 =
左边的对象加上左边的对象并返还左边的对象(左值)
类型:双目运算符
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| class Fraction { int num,den; public: Fraction() :Fraction(0,1){ } Fraction (int n):Fraction(n,1){ } Fraction(int n,int d):num(n),den(d){ } Fraction(const Fraction& f); void output()const ; Fraction operator+(const Fraction &f) const; Fraction& operator=(const Fraction& f); Fraction& operator+=(const Fraction& f); };
class Fraction { ... Fraction& operator+=(const Fraction &f); };
Fraction& Fraction::operator+=(Fraction& f) { num=num*f.den+f.num*den; den=den*f.den; normalize(); return *this; }
int main() { Fraction f1(3,4), f2(2,3); f1+=f2; f1.output(); return 0; }
|
同理可以友元函数重载
取负运算符 -
重载
-
运算符:反还对象取负的临时结果(右值)但不修改对象本身
类型:单目运算符
1 2 3 4 5 6 7 8 9 10 11
| Fraction Fraction::operator - ()const{ return Fraction(-num, den); }
int main() { Fraction a(3,4),b(5,6),c; c=-b; c.output(); return 0; }
|
同理可以友元函数重载
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| class Fraction { int num,den; public: Fraction() :Fraction(0,1){ } Fraction (int n):Fraction(n,1){ } Fraction(int n,int d):num(n),den(d){ } Fraction(const Fraction& f); void output()const ; Fraction operator+(const Fraction &c)const; friend Fraction operator- (const Fraction&,const Fraction&); Fraction& operator=(const Fraction &c); friend Fraction operator-(const Fraction&); };
Fraction operator - (const Fraction& f){ return Fraction(-f.num, f.den); }
int main() { Fraction a(3,4),b(5,6),c; c=-b; c.output(); return 0; }
|
取负运算符和相减运算符会不会出现歧义呢?不会的。因为相减运算符成员函数重载需要传入另一个操作对象,而取负运算符成员函数重载就是本身所以不用传入其他的对象。相减运算符友元重载需要两个对象,而取负运算符友元重载只需要一个,所有无论如何都不会出现歧义
复合运算——复数和实数运算(涉及类型转换)
对于复合运算强烈推荐使用友元函数重载运算符
在之前文章 C++程序设计——类和对象 中有提到过类型转换构造函数,对于复合运算有时编译器需要用于进行隐式类型转换
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| class Fraction { int num,den; public: Fraction() :Fraction(0,1){ } Fraction (int n):Fraction(n,1){ } Fraction(int n,int d):num(n),den(d){ } Fraction(const Fraction& f); void output()const ; Fraction operator+(const Fraction &c)const; friend Fraction operator- (const Fraction&,const Fraction&); Fraction& operator=(const Fraction &c); };
Fraction::Fraction(int n) :Fraction(n, 1) { }
Fraction operator+(const Fraction &a, const Fraction &b){ return Fraction(a.num*b.den+a.den*b.num,a.den*b.den); }
int main() { Fraction a(3,4),b,c; b=a+2; b.output(); return 0; }
|
为什么推荐使用友元函数?正好和赋值运算符 =
目的相反,=
运算符是为了得到避免右值计算结果,而符合运算就是为了得到右值运算结果,对于 1+Fraction
和 Fraction+1
计算式我们都希望可以计算,如果采用友元函数重载那么上述两个计算式都可以通过隐式调用类型转换函数将 1 转换为 Fraction 然后调用计算函数。
但是如果采用成员函数重载,那么对于 1+Fraction
和 Fraction+1
,由于调用成员函数的对象不同所以我们需要重载两个成员函数在 int 类中定义 opterator(const &Fraction) 和在 Fraction 类中定义 operator(const &int)。当一个复数与一个整数相加时,无论整数出现在左侧还是右侧,使用友元运算符重载函数都能得到很好的解决
所以对于复合运算强烈推荐使用友元函数重载运算符
前置运算符和后置运算符重载
++
和 --
运算符也可以重载,但为了区分前置和后置运算。C++约定把前置运算符重载为单目运算符函数,即表达式 ++a
,解释为 a.operator++()
- 把后置运算符看成双目运算符,在参数表内放置一个整型参数,该参数没有任何作用只是用来作为后置运算符的标识
a++
,解释为 a.operator++(int)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| class Fraction { int num,den; public: Fraction() :Fraction(0,1){ } Fraction (int n):Fraction(n,1){ } Fraction(int n,int d):num(n),den(d){ } Fraction(const Fraction& f); void output()const ; Fraction& operator++(); Fraction operator++(int); };
Fraction& Fraction::operator++(){ num+=den; return *this; }
Fraction Fraction::operator++(int a){ Fraction f(*this); num+=den; return f; }
int main() { Fraction f(3,4); (f++).output(); (++f).output(); return 0; }
|
同理可以友元函数重载
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| class Fraction { int num,den; public: Fraction() :Fraction(0,1){ } Fraction (int n):Fraction(n,1){ } Fraction(int n,int d):num(n),den(d){ } Fraction(const Fraction& f); void output()const ; friend Fraction& operator++(Fraction& f); friend Fraction operator++(Fraction& f, int); };
Fraction& operator++(Fraction& f){ f.num+=f.den; return f; }
Fraction operator++(Fraction& f,int a){ Fraction temp(f); f.num+=f.den; return temp; }
int main() { Fraction f(3, 4); (f++).output(); (++f).output(); return 0; }
|
类型转换运算符重载
- C++ 支持将对象转换为其它类型的类型转换运算符,如将 Fraction 对象转换为 double 类型
- 类型转换构造函数:通过单一参数将其它类型数据构造为对象
- 类型转换运算符,将对象转换为其它类型值
- 避免和类型转换构造函数两者同时使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| class Fraction { ... operator double()const { return getValue(); } }
int main() { Fraction f(3,4); f.output(); cout<<3.5+f<<endl; return 0; }
|
- 在没定义类型转换构造函数和定义分数和实数混合运算情况下会隐式或显示的调用类型转换符转换为 double 类型
- 类型转换构造函数和类型转换运算符都定义的情况下出现二义性错误
强制显示类型转换运算符
如果我们不想让程序隐式自动的转换,C++ 11提供的 explicit
允关键字许我们强制规定只允许显示调用类型转换运算符
1 2 3 4 5 6 7 8 9
| class Fraction { … explicit operator double()const { return getValue(); } }
Fraction f(3,4); cout<<3.5+double(f)<<endl; cout<<3.5+static_cast<double>(f)<<endl;
|
重载下标运算符 []
只能用成员函数重载,不能用友元函数重载
下标运算符 []
:返还对象内部下标指向的元素(左值),同时 []
本身是左结合所以可以用于多维
一般会重载两次:都是成员函数重载但是返还类型一个是常引用(对象只读),一个就是普通引用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| class Array1D { public: Array1D(int *p,int s); Array1D(); Array1D(int s); Array1D(const Array1D& a); ~Array1D(); Array1D& operator=(const Array1D& a); int getValue(int index) const; void setValue(int index,int value); int& operator[] (int i); const int& operator[](int i)const; void getSize() const { return size; } private: int *pData; int size; bool validIndex(int index) const; void copyData(int *data,int s); };
int& Array1D::operator[](int i){ if(validIndex(i)==false){ cout<<"error: Invalid index!\n"; exit(0); } return pData[i]; }
const int& Array1D::operator[](int i)const{ if(validIndex(i)==false){ cout<<"error: Invalid index!\n"; exit(0); } return pData[i]; }
int main() { int a[5]={1,2,3,4,5}; Array1D array1(a,5); Array1D array2=array1; array2[2]=10; for(int i=0;i<array1.getSize();++i) cout<<array2[i]<<' '; cout<<endl; return 0; }
|
两种重载运算符对比
参考文章
-
对双目运算符而言,成员运算符重载函数参数表中含有一个参数,而友元运算符重载函数参数表中含有两个参数。对单目运算符而言,成员运算符重载函数参数表中没有参数,而友元运算符重载函数参数表中含有一个参数
-
双目运算符一般可以被重载为友元运算符重载函数或成员运算符重载函数,但是复合运算强烈推荐(必须)使用友元函数
-
成员运算符函数和友元运算符函数都可以用习惯方式调用,也可以用它们专用的方式调用,下面列出了一般情况下运算符函数的调用形式
习惯调用形式 |
友元运算符重载函数调用形式 |
成员运算符重载函数调用形式 |
a+b |
operator+(a,b) |
a.operator+(b) |
-a |
operator-(a) |
a.operator-() |
a++ |
operator++(a,0) |
a.operator++(0) |
- C++ 的大部分运算符既可说明为成员运算符重载函数,又可说明为友元运算符重载函数。取决于实际情况和习惯来选择合适的运算符函数
- 一般而言,对于双目运算符将它重载为友元运算符比重载为成员运算符便于使用。对于单目运算符则选择重载为成员运算符较好
- 如果运算符所需的操作数(尤其是第一个操作数)希望有隐式类型转换,则运算符重载必须用友元函数
- C++语法规定
= () [] ->
只能作为成员函数重载,<< >>
只能作为友元函数重载
- 对于返还左值结果的函数为了更好的性能我们习惯返还引用类型
完善一维数组类
重载赋值运算符
之前写过的一维数组类重载了构造函数和赋值构造函数,但是对于已存在的数组之间的赋值仍然存在问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| class Array1D { public: Array1D(int *p,int s); Array1D(); Array1D(int s); Array1D(const Array1D& a); ~Array1D(); int getValue(int index) const; void setValue(int index,int value); void getSize() const { return size; } private: int *pData; int size; bool validIndex(int index) const; void copyData(int *data,int s); };
int main() { int a[5]={1,2,3,4,5}; Array1D array1(a,5); int b[5]={6,7,8,9,10}; Array1D array2(b,5); array1=array2; return 0; }
|
如果类中包含指针成员,缺省赋值运算符直接在指针之间赋值,导致内存问题
1 2 3 4 5 6 7 8 9 10 11
| class Array1D { public: Array1D& operator=(const Array1D& a); };
Array1D& Array1D::operator=(const Array1D& a){ pData=a.pData; size=a.size; }
|
因此我们也需要重载赋值运算符
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| class Array1D { public: Array1D(int *p,int s); Array1D(); Array1D(int s); Array1D(const Array1D& a); ~Array1D(); Array1D& operator=(const Array1D& a int getValue(int index) const; void setValue(int index,int value); void getSize() const { return size; } private: int *pData; int size; bool validIndex(int index) const; void copyData(int *data,int s); };
Array1D& Array1D::operator=(const Array1D& a) { if(this==&a) return *this; delete[] pData; copyData(a.pData, a.getSize()); 数组中的内容 return *this; }
|
平常会有很无聊的人写成 a=a;
这种无意义的赋值操作,但是编译器并不能认为它的错的,如果上述没有那个判断就会导致错误释放对象自身的空间
禁止赋值与拷贝
有时我们希望数组在构建之后只允许修改,不允许进行赋值与拷贝,那么我们可以借助 C++ 11 的 delete
关键字
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| class Array1D { public: Array1D(int *p,int s); Array1D(); Array1D(int s); Array1D(const Array1D& a)=delete; ~Array1D(); Array1D& operator=(const Array1D& a)=delete; int getValue(int index) const; void setValue(int index,int value); void getSize() const { return size; } private: int *pData; int size; bool validIndex(int index) const; void copyData(int *data,int s); };
|
增加右值引用和 move 语义
进一步提高性能,编译器根据上下文环境,选择不同的函数
需要编写移动构造函数的类,往往也需要提供移动赋值运算符,实现赋值时的移动(统一行动)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| class Array1D { public: Array1D(const Array1D& a); Array1D(Array1D&& a); Array1D& operator=(const Array1D& a); Array1D& operator=(Array1D&& a); … };
Array1D& Array1D::operator=(Array1D &&a){ if(this==&a) return *this; delete []pData; size=a.size; pData=a.pData; a.size=0; a.pData=null; }
|
升级——动态维护数组
之前的数组都是通过向构造函数传入初始元素个数后分配固定的内存,期间不能修改元素个数无法实现动态添加和删除数据元素
实际上只需要通过添加一个辅助扩容函数便可以实现类似 vector
容器动态扩容分配空间的效果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92
| class Array1D//类声明 { public: Array1D(int maxSize=1); Array1D(const Array1D& a); … void pushData(int data); void removeData(); void removeData(int index); private: int *pData; int size; int maxSize; bool validIndex(int index)const; void copyData(int *data,int maxSize,int curSize); void reAllocMemory(); };
Array1D::Array1D(int maxSize){ this->maxSize=maxSize; pData=new int[maxSize]; size=0; }
Array1D::~Array1D(){ delete[] pData; }
void Array1D::copyData(int *data,int maxSize,int curSize){ int i; this->maxSize=maxSize; size=curSize; pData=new int[maxSize]; for(i=0;i<size;++i) pData[i]=data[i]; }
Array1D::Array1D(const Array1D& a){ copyData(a.pData,a.maxSize,a.size); }
Array1D& Array1D::operator =(const Array1D& a){ if(this==&a) return *this; delete []pData; copyData(a.pData,a.maxSize,a.size); return *this; }
void Array1D::pushData(int data){ if(size==maxSize) reAllocMemory(); pData[size]=data; size++; }
void Array1D::reAllocMemory(){ int *temp=pData; copyData(pData,maxSize*2,size); delete[]temp; }
void Array1D::removeData(){ if(size==0){ cout<<"No data!\n"; return; } size--; }
void Array1D::removeData(int index){ if(validIndex(index)==false || size==0) return; size--; for(int i=index;i<size;++i) pData[i]=pData[i+1]; }
int main() { Array1D array1(5); array1.pushData(1); array1.pushData(2); array1.pushData(3); array1.pushData(4); array1.pushData(5); array1.pushData(6); for(int i=0;i<array1.getSize();++i) cout<<array1[i]<<' '; cout<<endl; return 0; }
|
每次存入新的元素都会先判断是否越界,是的话就会触发扩容函数和拷贝数据函数将对象申请一个是之前 2 倍大小的空间内存,并将之前的内容复制到新申请的空间并释放旧的空间
二维数组类实现
最好的实现方法是上述第三张图——根据封装的思想,我们可以采用“组合”原理借助已经实现的一维数组类来实现二维数组类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| class Array2D { public: Array2D(int row, int col); ~Array2D(); Array1D& operator[](int index); ... private: Array1D *pData; int row, col; };
Array2D::Array2D(int r,intc): row(r), col(c){ pData=new Array1D[row]; for(int i=0;i<row;++i) pData[i].ReSize(col); }
Array2D::~Array2D(){ delete []pData; }
Array1D& Array2D::operator[](int index){ return pData[index]; }
int main() { Array2D a(3,4); a[2][1]=5; ... return 0; }
|