C++:运算符重载函数之"++"、"--"、"[ ]"、"=="的应用
5.2.5 "++"和"--"的重载
对于前缀方式++ob,可以用运算符函数重载为: ob.operator++() //成员函数重载 或 operator++(X &ob) //友元函数重载,其中ob为类X的对象的引用 对于后缀方式++ob,可以用运算符函数重载为: ob.operator++(int) //成员函数重载 或 operator++(X &ob,int) //友元函数重载,其中ob为类X的对象的引用 调用时,参数int一般被传递给值0,例如: class X{ ... public: ... X operator++(); //前缀方式 X operator++(int); //后缀方式 }; int main() { X ob; ... ++ob; //隐式调用ob.operator++() ob++; //隐式调用ob.operator++(int) ob.operator++(); //显式调用ob.operator++(),意为++ob ob.operator++(0); //显式调用ob.operator++(int),意为ob++ } 类似的,也可以重载为友元函数,例如: class Y{ ... public: ... firend Y operator++(Y &); //前缀方式 friend Y operator++(Y &,int); //后缀方式 }; int main() { Y ob; ... ++ob; //隐式调用ob.operator++(Y&) ob++; //隐式调用ob.operator++(Y&,int) operator++(ob); //显式调用ob.operator++(Y&),意为++ob operator++(ob,0); //显式调用ob.operator++(Y&,int),意为ob++ }
//例5.8 使用成员函数以前缀方式后后缀方式重载运算符"--"
#include<iostream> using namespace std; class Three{ public: Three(int I1=0,int I2=0,int I3=0) { i1 = I1; i2 = I2; i3 = I3; } Three operator--(); //声明自减运算符--重载成员函数(前缀方式) Three operator--(int a); //声明自减运算符--重载成员函数(后缀方式) void show(); private: int i1,i2,i3; }; Three Three::operator--() //定义自减运算符--重载成员函数(前缀方式) { --i1; --i2; --i3; return *this; //返回自减后的当前对象 } Three Three::operator--(int a) //定义自减运算符--重载成员函数(后缀方式) { Three temp(*this); i1--; i2--; i3--; return temp; //返回自减前的当前对象 } void Three::show() { cout<<"i1="<<i1<<","; cout<<"i2="<<i2<<","; cout<<"i3="<<i3<<endl; } int main() { Three t1(4,5,6),t2,t3(11,12,13),t4; t1.show(); --t1; //隐式调用(前缀方式) t1.show(); //显示执行--t1后的值 t2=t1--; //隐式调用(后缀方式),将t1自减前的值赋给t2 t2.show(); //显示t2保存的是执行t1--之前的t1的值 t1.show(); //显示执行t1--之后的t1的值 cout<<endl; t3.show(); t3.operator--(); //显示调用(前缀方式) t3.show(); t4=t3.operator--(0); //显示调用(后缀方式) t4.show(); t3.show(); return 0; }
//例5.8 使用友元函数以前缀方式和后缀方式重载运算符"++"
#include<iostream> using namespace std; class Three{ public: Three(int I1=0,int I2=0,int I3=0) { i1 = I1; i2 = I2; i3 = I3; } friend Three operator++(Three &); //声明自加运算符++重载成员函数(前缀方式) friend Three operator++(Three &,int ); //声明自加运算符++重载成员函数(后缀方式) void show(); private: int i1,i2,i3; }; Three operator++(Three &T) //声明自加运算符++重载友元函数(前缀方式) { ++T.i1; ++T.i2; ++T.i3; return T; //返回自加后的对象 } Three operator++(Three &T,int a) //声明自加运算符--重载友元函数(后缀方式) { Three temp(T); T.i1++; T.i2++; T.i3++; return temp; //返回自加前的对象 } void Three::show() { cout<<"i1="<<i1<<","; cout<<"i2="<<i2<<","; cout<<"i3="<<i3<<endl; } int main() { Three t1(4,5,6),t2,t3(14,15,16),t4; t1.show(); ++t1; //隐式调用(前缀方式) t1.show(); //显示执行++t1后的值 t2=t1++; //隐式调用(后缀方式),将t1自加前的值赋给t2 t2.show(); //显示t2保存的是执行t1++之前的t1的值 t1.show(); //显示执行t1++之后的t1的值 cout<<endl; t3.show(); operator++(t3); //显示调用(前缀方式) t3.show(); t4=operator++(t3,0); //显示调用(后缀方式) t4.show(); t3.show(); return 0; }
说明:
(1)由于友元运算符重载函数没有this指针,所以不能引用this指针所指的对象,使用友元函数重载增运算符"++"或自减"--"时,应采用对象引用传递数据。
例如:
friend Three operator++(Three &); //声明自加运算符++重载成员函数(前缀方式)
friend Three operator++(Three &,int ); //声明自加运算符++重载成员函数(后缀方式)
(2)前缀方式和后缀方式的函数内部语句可以相同,也可以不同,取决于编程的需要。
5.2.7 下标运算符"[]"的重载
在C++中,在重载下标运算符[]时,认为它是一个双目运算符,例如X[Y]可以看成:
[]---------双目运算符
X----------左操作数
Y----------右操作数
其相应的运算符重载函数名为operator[]。
设X是一个类的对象,类中定义了重载"[]"的operator[]函数,则表达式
X[Y] 被解释为 X.operator[](y);
下标运算符重载函数只能定义成员函数,其形式如下:
返回类型 类名::operator[](形参)
{
//函数体
}
注意:形参在此表示下标,C++规定只能有一个参数
//例5.12 使用下标运算符重载函数的引例
#include<iostream> using namespace std; class Vector4{ public: Vector4(int a1,int a2,int a3,int a4) { v[0]=a1; v[1]=a2; v[2]=a3; v[3]=a4; } private: int v[4]; }; int main() { Vector4 ve(1,2,3,4); cout<<v[2]; //运行错误,v[2]是类Vector4的私有成员,即使是公有成员,输出格式应为ve.v[2]; return 0; } 可是,如果对[]进行重载,即使v[2]是私有成员,也可以运行成功,直接访问。即: int &Vector::operator[](int bi) { if(bi<0||bi>=4) { cout<<"Bad subscript!\n"; exit(1); } return v[bi]; //v[bi]被解释为v.operator[](2) }
//例5.13 使用下标运算符[]重载函数
#include<iostream> using namespace std; class Vector4{ public: Vector4(int a1,int a2,int a3,int a4) { v[0]=a1; v[1]=a2; v[2]=a3; v[3]=a4; } int &operator[](int ); private: int v[4]; }; int &Vector4::operator[](int bi) //返回一个int型的引用 { if(bi<0||bi>=4) { cout<<"Bad subscript!\n"; exit(1); } return v[bi]; //v[bi]被解释为v.operator[](2) } int main() { Vector4 v(1,2,3,4); int i=0; for(;i<4;i++) cout<<"v["<<i<<"]="<<v[i]<<"\n"; //v[i]被解释为v.operator[](i); cout<<endl; v[3]=v[2]; //v[2]被解释为v.operator[](2); cout<<"v[3]="<<v[3]<<endl; //v[3]被解释为v.operator[](3); v[2]=22; cout<<"v[2]="<<v[2]<<endl; return 0; } /* 运行结果: v[0]=1 v[1]=2 v[2]=3 v[3]=4 v[3]=3 v[2]=22 */
5.2.6 赋值运算符"="的重载
对于任一类X,如果没有用户自定义的赋值运算符函数,那么系统将自动地为其生成一个默认的
赋值运算符函数,例如:
X &X::operator=(const X &source)
{
//成员间赋值
}
若obj1和obj2是类X的两个对象,obj2已经建立,则编译程序遇到如下语句;
obj1=obj2;
就调用默认的赋值运算符函数,将对象obj2的数据成员逐域复制到obj1中。
采用默认的赋值运算符函数实现的数据成员逐一赋值的方法是一种浅层复制非方法。通常,默认的赋值运算符函数是能够胜任工作的。但是,对于许多重要的实例类来说,仅有默认的赋值运算符函数还是不够的,还需要用户根据实际需要自己对赋值元算法进行重载,以解决遇到的问题。指针悬挂就是这方面的一个典型问题。
1.指针悬挂问题
在某些特殊情况下,如类中有指针类型时,使用默认的赋值运算符函数会产生错误。
//例 5.10 关于浅层复制的例子。
#include<iostream> using namespace std; class STRING{ public: STRING(char* s) { cout<<"Constructor called."<<endl; pt = new char(strlen(s)+1); strcpy(pt,s); } ~STRING() { cout<<"Destructor called."<<pt<<endl; delete pt; } private: char* pt; }; int main() { STRING p1("book"); STRING p2("jeep"); p2=p1; return 0; }
运行结果:
Constructor called. (1)
Constructor called. (2)
Destructor called.book (3)
Destructor called.*q (4)
结果出现了指针悬挂问题。
原因分析:对象p1和p2建立时,分别调用构造函数,输出(1)(2)。通过new运算符分别从内存动态分配一块空间,对象p1的字符指针pt指向book,对象p2的字符指针pt指向jeep;执行p2=p1时,由于用户没有定义赋值运算符函数,系统于是就会调用默认的赋值运算法函数。使对象p1和p2的字符指针pt都指向new开辟的同一块内存空间,该内存空间里所存放的内容是book;
主程序结束时,系统逐一撤销建立的对象,因此第一次调用析构函数,撤销对象p2,输出(3),并用delete释放new开辟的动态空间。再进行第二次调用析构函数,撤销对象p1,由于p1和p2的指针pt是指向同一内存的,在撤销p2时,已经释放了pt指向的空间,此时,尽管对象p1的指针pt存在,可是却无法访问此空间了。所以输出的(4)中pt指向的内容是随机字符,而不是book.同一空间当是不允许用delete释放两次的,这就是所谓的指针悬挂问题。
由于本例的类中含有指向动态空间的指针pt,执行语句"p2=p1"时,调用的就是默认的赋值运算符函数,采用的是浅层复制方法,使两个对象p1和p2的指针pt都指向new开辟的同一个空间,于是出现了指针悬挂现象。
2.用深层复制解决指针悬挂问题
为了解决浅层复制出现的错误,必须显示地定义一个自己的赋值运算符重载函数,使之不但复制数据成员,而且为对象p1和p2分配了各自的内存空间,这就是所谓的深层复制。
//例5.11 关于深层赋值的例子
#include<iostream> using namespace std; class STRING{ public: STRING(char* s) { cout<<"Constructor called."<<endl; pt = new char(strlen(s)+1); strcpy(pt,s); } STRING &operator=(const STRING &s); //声明赋值运算符重载函数 ~STRING() { cout<<"Destructor called."<<pt<<endl; delete pt; } private: char* pt; }; STRING &STRING::operator=(const STRING &s) //定义赋值运算符重载函数 { if(this==&s) return *this; //防止s=s的赋值 delete pt; //释放掉原区域 pt = new char(strlen(s.pt)+1); //分配新区域 strcpy(pt,s.pt); //字符串复制 return *this; } int main() { STRING p1("book"); STRING p2("jeep"); p2=p1; return 0; }
运行结果:
Constructor called.
Constructor called.
Destructor called.book
Destructor called.book
结果解决了指针悬挂问题。
原因分析:对象p1和p2建立时,分别调用构造函数,输出(1)(2)。通过new运算符分别从内存动态分配一块空间,对象p1的字符指针pt指向book,对象p2的字符指针pt指向jeep;执行p2=p1时,由于用户自己定义了赋值运算符函数,释放掉了p2指针pt所指的旧区域,又按照新的长度分配新的内存空间给p2,再把对象p1的指针pt所指向的数据book赋给p2对象的指针pt所指向的区域内。也即 p1的指针pt指向book,p2的指针pt也指向book。
主程序结束时,系统逐一撤销建立的对象,虽然对象p1和对象p2的指针pt都指向了相同内容book,但是它们却分别有自己的动态分配的空间。所以delete释放空间时,就不会出现指针悬挂现象了。
说明:类的赋值运算符"="只能重载为成员函数,而不能把它重载为友元函数,因为若把上述赋值运算符"="重载为友元函数
friend string &operator=(string &p2,string &p1)
表达式
p1="book" 将被解释为 operator=(p1,book) 这显然是没有问题的,
但是对于表达式"book"=p1 将被解释为 operator=(book,p1),即C++编译器首先将book转换成一个隐藏的string对象,然后使用对象p2引用该隐藏的对象,并不认为这个表达式是错的,从而导致赋值语句上的混乱。因此,双目赋值运算符重载为成员函数,而不能重载为友元函数。