运算符重载
对于面向对象的程序设计来说,运算符重载可以完成两个对象之间的复杂操作,比如两个对象的加法、减法等。运算符重载的原理是:一个运算符只是一个具有特定意义的符号,只要我们告诉编译程序在什么情况下如何去完成特定的操作,而这种操作的本质是通过特定的函数完成的。
重载运算符
为了重载运算符,首先要定义运算符重载函数,它通常是类的非静态成员函数或者友元函数,运算符的操作数通常也应为对象。
定义运算符重载函数的一般格式为:
返回值类型 类名::operator 需要重载的运算符(形参列表){ ... }
其中:operator为关键字,它与其后需要重载的运算符一起构成函数名。由于运算符重载函数的函数名是以特殊的关键字开始的,编译器很容易辨认出来。
例如,定义一个复数类,重载“+”、“+=”、“=”:
#include <iostream> using namespace std; class Complex { private: int Real, Image; public: Complex(float r = 0, float i = 0) { Real = r; Image = i; } float GetR() { return Real; } float GetI() { return Image; } void Show() { cout << Real << ' ' << Image << endl; } Complex operator +(Complex &); Complex operator +(float); void operator +=(Complex &); void operator =(Complex &); }; Complex Complex::operator +(Complex &c) { Complex t; t.Real = Real + c.Real; t.Image = Image + c.Image; return t; } Complex Complex::operator +(float s) { Complex t; t.Real = Real + s; t.Image = Image; return t; } void Complex::operator +=(Complex &c) { Real = Real + c.Real; Image = Image + c.Image; } void Complex::operator =(Complex &c) { Real = c.Real; Image = c.Image; } int main() { Complex c1(10, 20), c2(20, 30), c3; c1.Show(); c1 += c2; c1.Show(); c3 = c1 + c2; c3.Show(); system("pause"); return 0; }
这段程序的运行结果为:
10 20 30 50 50 80 请按任意键继续. . .
从上一例可以看出,运算符的重载也是函数的重载,只不过是系统约定了重载运算符的函数名。同时,经重载后的运算符的使用方法与普通的运算符一样方便。实际上,实现运算符重载,并对相应成员函数的调用是由函数自动完成的。比如:
c1 += c2; c1.operator +=(c2); //与上句相同含义
对于运算符重载,必须说明以下几点:
- 运算符重载之后,遇到这种运算符的操作,实际上是调用了一个运算符重载函数完成的,在调用时,将右操作数作为函数的实参,而左操作数就是运算符重载函数所在类的实例;
- 当用成员函数实现运算符的重载时,运算符重载函数的参数个数为0个或1个。对于一个操作数的运算符,该运算符重载函数通常不能有参数;而对于有两个操作数的运算符,只能带有一个参数。运算符的左操作数一定是对象,右操作数作为调用运算符重载函数的参数,参数类型没有什么限定。在C++中,不允许重载有三个操作数的运算符;
- 只能对C++中已定义的运算符进行重载,并且当重载一个运算符时,该运算符的优先级和结合律不能改变。
+ | - | * | / | % | ^ | & | | |
~ | ! | , | = | < | > | <= | >= |
++ | -- | << | >> | == | != | && | || |
+= | -= | *= | /= | %= | ^= | &= | |= |
<<= | >>= | [ ] | ( ) | -> | ->* | new | delete |
运算符 | 含义 | 原因 |
?: | 三目运算符 | 在C++没有定义一个三目运算符的语法 |
. | 成员操作符 | 为保证对象操作符对成员访问的安全性 |
.* | 成员指针操作符 | 同上(指向类中成员数据的指针变量) |
:: | 作用域运算符 | 该操作数左边的操作数是一个类型名,不是表达式 |
sizeof | 求字节运算符 | 其操作数是一个类型名,不是表达式 |
一元运算符的重载
前面描述了二元运算符的重载方法,这里说明一下一元运算符的重载方法。用成员函数实现一元运算符重载的一般形式为:
返回值类型 类名::operator 单目运算符(){ ... }
但对于“++”还有“--”来说,还存在前置和后置的问题,在定义运算符重载函数时必须有所区分,以便编译器根据这种区分来调用不同的运算符重载函数。下面以“++”运算符的重载为例说明其实现方法:
“++”作前置运算时,它的运算符重载函数的一般格式为:
返回值类型 类名::operator ++(){ ... }
“++”作后置运算时,它的运算符重载函数的一般格式为:
返回值类型 类名::operator ++(int){ //int仅仅是一个标志,不能用其他的作标志,如float ... }
注意:在后置运算符重载函数中的参数int仅是用作区分的,并没有其他实际意义,可以给一个变量名,也可以不给出变量名。
例如:
#include <iostream> using namespace std; class Complex { private: int Real, Image; public: Complex(float r = 0, float i = 0) { Real = r; Image = i; } float GetR() { return Real; } float GetI() { return Image; } void Show() { cout << Real << ' ' << Image << endl; } Complex operator ++(); Complex operator ++(int); }; Complex Complex::operator ++() { //前置++,就将本身++,再计算其它的 Image++; return *this; } Complex Complex::operator ++(int) { //后置++,就先把本身保存副本返回,再将本身++ Complex c= *this; Image++; return c; } int main() { Complex c1(10, 20), c2; c1.Show(); c2 = ++c1; c2.Show(); c1.Show(); c2 = c1++; c2.Show(); c1.Show(); system("pause"); return 0; }
这段程序的运行结果为:
10 20 10 21 10 21 10 21 10 22 请按任意键继续. . .
当实现前置“++”运算时,应将加1后的对象值作为返回值,即要返回当前对象值。此时必须使用指向当前对象的指针this,用于返回*this的值;当实现后置“++”运算时,应返回当前对象的值,然后完成加1运算。
由于此处用到了隐含的this指针,而静态的成员函数中并不包含this指针。所以,运算符重载函数不能定义为静态的成员函数。
友元运算符
实现运算符重载的方法有两种:用类的成员函数来实现和通过类的友元函数来实现。后者简称为友元运算符。
重载一元运算符的友元函数的一般格式为:
friend 返回值类型 operator 需要被重载的运算符(类名 &对象名){ ... }
重载二元运算符的友元函数的一般格式为:
friend 返回值类型 operator 需要被重载的运算符(参数1说明, 参数2说明){ ... }
其中:两个参数中至少有一个为类X的对象,也就是说,至少有一个是“类名 &对象名”的类型的。
例如:
#include <iostream> using namespace std; class Complex { private: int Real, Image; public: Complex(float r = 0, float i = 0) { Real = r; Image = i; } float GetR() { return Real; } float GetI() { return Image; } void Show() { cout << Real << ' ' << Image << endl; } friend Complex operator +(Complex &, Complex &); //友元函数 friend Complex operator +(Complex &, float); //友元函数 friend Complex operator -(Complex); //友元函数 void operator +=(Complex &); //成员函数 }; Complex operator +(Complex &c1, Complex &c2) { Complex t; t.Real = c1.Real + c2.Real; t.Image = c1.Image + c2.Image; return t; } Complex operator +(Complex &c, float s) { Complex t; t.Real = c.Real + s; t.Image = c.Image; return t; } Complex operator -(Complex c) { return Complex(-c.Real, -c.Image); } void Complex::operator +=(Complex &c) { Real = Real + c.Real; Image = Image + c.Image; } int main() { Complex c1(10, 20), c2(20, 30), c3; c1.Show(); c1 += c2; c1.Show(); c3 = c1 + c2; c3.Show(); c3 = -c3; c3.Show(); system("pause"); return 0; }
这段程序的运行结果为:
10 20 30 50 50 80 -50 -80 请按任意键继续. . .
这里对分别使用成员函数、友元函数实现运算符重载进行对比:
friend Complex operator +(Complex &, Complex &); //友元函数,两个操作数就两个参数 Complex operator +(Complex &c1, Complex &c2) { //友元函数 Complex t; t.Real = c1.Real + c2.Real; t.Image = c1.Image + c2.Image; return t; } Complex operator +(Complex &); //成员函数,两个操作数就一个参数 Complex Complex::operator +(Complex &c) { //成员函数 Complex t; t.Real = Real + c.Real; t.Image = Image + c.Image; return t; }
转换函数
转换函数(又称为类型转换函数)是类中定义的一个成员函数,其一般格式为:
类名::operator 转换后的类型 (){ //将“类名”类型转换为“转换后的类型” ... }
其中:operator和“转换后的类型”一起构成转换函数名。该函数不能带有参数,也不能指定返回值类型。因为它的返回值类型就是“转换后的类型”。转换函数的作用就是将对象内的成员数据转换成某种特定的类型。
例如:
#include <iostream> using namespace std; class Complex { private: int yuan, jiao, fen; public: Complex(int y=0, int j=0, int f=0) { yuan = y; jiao = j; fen = f; } operator float(); //A:类型转换函数 float GetDollar(); }; Complex::operator float() { float amount; amount = yuan*100.0 + jiao*10.0 + fen; amount /= 100; return amount; //B:返回值 } float Complex::GetDollar() { float amount; amount = yuan*100.0 + jiao*10.0 + fen; amount /= 100; return amount; } int main() { Complex c1(25, 50, 70); float f; f = c1; //C cout << f << ' ' << (float)c1 << ' ' << float(c1) << ' ' << c1.GetDollar() << endl; //D system("pause"); return 0; }
这段程序的运行结果为:
30.7 30.7 30.7 30.7 请按任意键继续. . .
- 尽管在A行转换函数定义的时候并没有明确地写出返回值,但是函数是有返回值的,返回值类型就是需要转换的类型(如B行);
- 关于类型转换函数的使用,可以在C行和D行得到验证。直接赋值、或者使用强制类型转换的时候,编译器都会调用类型转换函数来实现类型转换的功能;
- 由D行也可以知道,任何一个成员转换函数都可以通过一个普通的成员函数来进行实现。但是从使用的角度上相比较,转换函数比一般的普通成员函数要简明得多;
- 转换函数只能是成员函数,不能是友元函数。转换函数的操作数是对象。转换函数可以被派生类继承,也可以被说明为虚函数,在一个类中可以定义多个转换函数。
赋值运算符重载
在相同类型的对象之间是可以直接相互赋值的,但是当对象的成员中使用了动态的数据类型时,就不能直接相互赋值了,否则就会在程序的执行期间出现运行错误。
例如:
#include <iostream> using namespace std; class A { private: char* ps; public: A() { ps = 0; } A(char* s) { ps = new char[strlen(s) + 1]; strcpy_s(ps, strlen(s) + 1, s); } ~A() { if (ps) delete[]ps; } char* GetS() { return ps; } }; int main() { A s1("Hello"), s2("World"); cout << s1.GetS() << endl; cout << s2.GetS() << endl; s1 = s2; //A cout << s1.GetS() << endl; cout << s2.GetS() << endl; system("pause"); return 0; //B }
这段程序的运行结果为:
Hello World World World 请按任意键继续. . .
乍一看好像没有什么问题,赋值的工作已经完成了。此时运行到了B行,要是“按任意键继续”之后,整个程序就会卡死。也就是出现了运行错误的情况。
解释:在A行的代码中,s1和s2指向同一个字符串“World”。然后,程序运行结束的时候,同时为s1和s2指向的内存空间进行释放,这个时候就出现了两次收回同一个内存空间的情况,因此出现了运行错误。所以当类中的成员占用动态的存储空间时,必须重载“=”运算符。
也就是说,要加上如下代码:
A & operator = (A &b) { if (ps) delete[]ps; if (b.ps) { ps = new char[strlen(b.ps) + 1]; strcpy_s(ps, strlen(b.ps) + 1, b.ps); }else { ps = 0; } return *this; }
这个时候,结合前面的实现拷贝的构造函数(参考链接:【编程语言】C++类和对象、构造函数和析构函数):
如果在类中的某些成员是使用new运算符动态申请存储空间的,那么:
- 在初始化的过程中,如果想要用已存在的类的对象进行初始化,需要在类中显式地定义一个完成拷贝功能的构造函数;
- 在赋值的过程中,如果想要用已存在的类的对象进行赋值,需要在类中显式地对赋值运算符进行重载。
A s1 = s2; //拷贝功能的构造函数 A s1(s2); //拷贝功能的构造函数 s1 = s2; //赋值运算符重载
几个特殊的运算符的重载
上面提到了“++”和“--”,为了区分前置和后置运算,重载运算符的方法也是有所不同。对于特殊的运算符的重载,有必要对其中的几个进行进一步的说明。除了之前的“++”、“--”之外,还有下标运算符“[ ]”、函数调用运算符、成员存取运算符、new运算符、delete运算符等等。
“++”和“--”运算符
上文讲述了如何使用成员函数重载“++”、“--”运算符,此处就介绍用友元函数对这两个运算符进行重载。
前置“++”运算符的重载(友元函数):
friend 返回值类型 operator ++ (类名 &){ ... }
后置“++”运算符的重载(友元函数):
friend 返回值类型 operator ++ (类名 &, int){ ... }
下标运算符
在C++中使用数组元素时,系统并不做下标是否越界的检查,可以通过重载下标运算符来实现这种检查或完成更广义的操作。也就是说,下标运算符重载之后,就可以将某个对象的实例当做数组名来使用,通过下标运算符来调取元素。其实就是,这个对象中存放一个数组就行了。
重载下标运算符的一般格式为:
返回值类型 类名::operator [](形参列表){ ... }
其中:形参列表用于指定下标值。
对于下标运算符的重载,要注意以下几点:
- 下标运算符只能由类的成员函数来实现,不能使用友元函数来实现;
- 左操作数必须是对象;
- 下标运算符重载函数只能是非静态的成员函数,有且只有一个参数。
例如:
#include <iostream> using namespace std; class Array { private: int len; float* ps; public: Array(int n=0) { if (n > 0) { ps = new float[n]; memset(ps, 0, sizeof(float)*n); len = n; }else { len = 0; ps = 0; } } ~Array() { if (ps) delete[]ps; } int GetLen() const { return len; } void SetLen(int l) { if (l > 0) { if (ps) delete[]ps; ps = new float[l]; memset(ps, 0, sizeof(float)*l); len = l; } } float& operator [](int index) { if (index >= len || index < 0) { cout << "下标错误!" << endl; } return ps[index]; } }; int main() { Array a1(10); for (int i = 0; i < 10; i++) a1[i] = i; cout << a1[3] << endl; cout << a1[20] << endl; system("pause"); return 0; }
这段程序的运行结果为:
3 下标错误! -1.9984e+18 请按任意键继续. . .
尽管下标运算符分成两个部分:一个左方括号和一个右方括号,但系统认为是一个二元运算符。当表达式中出现“[]”运算符时,第一个操作数在“[”的左边,并且该操作数总是一个类的对象;而另一个操作数在“[”的右边,该操作数实际上可以是由operator[]所能处理的任意类型,函数的返回值也可以是由operator[]函数处理后的任意类型数据。
由于该函数只能有一个参数,所以只能对一维数组进行处理;对于多维数组,可以将其作为一个一维数组来处理。
函数调用运算符
在C++中,把函数名后的括号“()”称为函数调用运算符。函数调用运算符也可以像其他运算符一样进行重载。也就是说,函数调用运算符重载之后,就可以将某个对象的实例当做函数名来使用,通过函数调用运算符来执行函数内容。
重载函数调用运算符的格式为:
返回值类型 类名::operator ()(形参列表){ ... }
其中:形参列表与一般的函数一样,可以带有0个或多个参数,但不能带有缺省的参数。
函数运算符重载函数的左操作数一定是对象。
成员选择运算符
成员选择运算符“->”在C++中也允许重载,但这种重载通常情况下并没有什么实际意义,这里不做太多介绍。
new和delete运算符
new和delete运算符分别用于动态申请存储空间和释放存储空间,这两个运算符使用起来相当方便,但对于一些特殊的场合,仍希望对它们进行重载。比如希望将某一个对象存储在固定的存储空间内,并对删除对象的存储空间进行控制等等。
重载new运算符的一般格式为:
void* 类名::operator new(size_t size,形参列表){ ... }
其中:返回值是一个任意类型的指针(void*),第一个参数的类型必须是size_t。在C++中,size_t的定义为:
typr unsigned int size_t
即是一个无符号的整数类型,它表示要求分配存储空间的长度。
重载delete运算符的一般格式为:
void 类名::operator delete(void *p,size_t size){ ... }
该函数没有任何返回值,它至少要有一个参数,这个参数是一个任意类型的指针(void*),通常是用运算符new返回的指针;第二个参数是任选的,若有第二个参数,它的类型必须是size_t。
有关这两个运算符的重载,说明以下几点:
- 重载new和delete运算符的函数必须是成员函数,不能使友元函数;
- 重载运算符new和delete运算符的函数总是静态的成员函数。在operator new和operator delete的定义中,不管是否使用了关键字static,编译程序总是将这两个重载函数看成是静态的成员函数;
- operator new和operator delete函数不能是虚函数;
- 一旦在类中重载了这两个运算符,凡涉及到该类的动态内存空间的分配和释放,系统都会优先调用重载后的运算符。若希望使用预定义的new和delete运算符,则在其前面要加上作用域运算符“::”;
- 在重载new运算符和delete运算符时,为了动态地分配和回收内存空间,仍需要用到C++预定义的new和delete运算符(重载函数定义中不需要用作用域运算符“::”),或者使用与定义函数calloc()动态分配存储空间和free()释放内存空间。
最后强调几点:
用友元函数实现的运算符重载,不会被派生类继承;而用类的成员函数定义的转换函数和重载的运算符绝大多数都可以被派生类继承,在派生类中定义的转换函数和重载运算符函数将会隐藏在基类中定义的这些函数;
成员函数operator=比较特殊,除了它不能被派生类继承外,也不能将它说明为虚函数。其他类运算符和转换函数可以说明为虚函数。赋值运算符operator=不能被派生类继承的原因是:派生类的赋值语句除了包含有基类的赋值语句之外,还增加了新的赋值语句(如增加新的成员)。由于派生类的赋值语义与基类中定义的赋值语义不一样,所以派生类不能继承基类中的赋值运算符。