运算符重载一般概念
C++内部定义的数据类型(int , float, …)的数据操作可以用运算符号来表示,其使用形式是表达式,用户自定义的类型的数据的操作则用函数表示,其使用形式是函数调用。为了对用户自定义数据类型的数据的操作与内定义的数据类型的数据的操作形式一致,C++提供了运算符的重载,通过把C++中预定义的运算符重载为类的成员函数或者友员函数,使得对用户的自定义数据类型的数据—对象的操作形式与C++内部定义的类型的数据一致。
运算符重载即对C++内部的基本数据类型所支持的运算符赋予新的含义,让其可操作自定义的数据类型。运算符重载指对已知的运算符,在新的场合,通过程序实现新的行为。
运算符重载规则
允许重载的运算符
标准C++中提供的运算符很多,但允许重载的运算符只能是下表中所列。参见下表。
(允许重载的运算符列表)
双目运算符 | + - * / % |
关系运算符 | == != < > <= >= |
逻辑运算符 | || && + |
单目运算符 | + - * & |
自增自减运算符 | ++ -- |
位运算符 | | & ~ ^ << >> |
赋值运算符 | = += -= *= /= %= &= |= ^= <<= >>= |
空间申请和释放 | New delete new[] delete[] |
其他运算符 | () -> ->* , [] |
不允许重载的运算符
不允许重载的运算符只有5个:
. (成员访问符)
.* (成员指针访问运算符)
:: (域运算符)
Sizeof (长度运算符) typeof typedef
?: (条件运算符号)
其他规则
1) 不允许自己定义新的运算符,只能对已有的运算符号进行重载;
2) 重载不能改变运算符运算对象的个数,如>和<是双目运算符,重载后仍为双目运算符,需要两个参数;
3) 重载不能改变运算符的结合性,如=是从右至左,重载后仍然为从右至左;
4) 重载不能改变运算符的优先级别,例如* / 优先于+ -,那么重载后也是同样的优先级别;
5) 重载运算符的函数不能有默认的参数,否则就改变了运算符参数的个数,与第2条矛盾;
6) 重载的运算符必须和用户的自定义数据类型一起使用,其参数至少应有一个是类对象(或者类对象的引用),或者说参数不能全部是C++的标准类型;
7) 运算符重载函数可以是类的成员函数,也可以是类的友员函数,也可以使普通函数。
运算符重载的三种方式
对同一个运算符号,往往可以通过普通函数、友员函数和类成员函数这三种方式实现重载,以完成同样第功能。通过普通函数实现运算符重载的特点是自定义类即数据类型不得将其数据成员公开,以便让普通函数可以访问类的数据成员,这破坏了类的封装性,所以这种重载方式要少用或不用,C++语言之所以提供此种方式,是为了与C语言的兼容性。通过友员函数重载运算符的特点可以不破坏类的封装性,类的数据成员可以是私有的,但这种方式需要在类中定义友员函数,以允许友员函数可以操作类的私有数据成员,这在实际使用中也很不方便,所以不在必要时不要使用,这量的必要指的是C++中有一些运算符不能用类成员函数的方式实现重载,比如用于cout和cin的流提取符最好使用友员函数实现重载。
通过类成员函数重载运算符是我们推鉴使用的,运算符重载函数是类的成员函数,正好满足了类的封装性要求。这种运算符重载的特点是类本身就是一个运算符参数,参见实例。以下我们分别对一些主要的运算符重载的具体情况展开讨论。
单目运算符、双目运算符重载
我们知道单目运算符是指该运算符只操作一个数据,由此产生另一个数据,如正数运算符和负数运算符。双目运算符则是要操作两个数据,由此产生另一个数据,如一般的算术运算符和比较运算符都双目运算符。
1) 在一个类中重载一元运算符@为类运算符的形式为:
返回值类型 类名::operator@()
{
//…
}
2) 在一个类中重载一元运算符@为友员运算符的形式为:
返回值类型 operator@(const 类名 &obj)
{
//…
}
3) 在一个类中重载二元运算符@为类运算符的形式为:
返回值类型 类名::operator@(参数1)
{
//…
}
双目运算符需要两个运算符,这量的函数原型为何仅列出了一个参数?这是因为类本身也是一个参数,存取该参数可以通过this关键字来实现。
4) 在一个类中重载二元运算符@为友员运算符的形式为:
返回值类型 operator@(参数1,参数2)
{
//…
}
关于双目运算符的例子参见例1。
转换构造函数和类型转换运算符(函数)
类的转换构造函数只带一个参数,它把参数类型的数据转换成相应类型的类的对象。这与拷贝构造函数不同,拷贝构造函数的参数是同类型对象的引用。
例如
classCComplex
{
public:
double m_fReal;
double m_fImag;
CComplex(){};
CComplex(double fReal, double fImag=0)
{
m_fReal=fReal;
m_fImag=fImag;
};
};
intmain()
{
CComplex cpx;
cpx=9;
return 0;
}
“cpx=9”一句将数值9转换成一个临时的Ccomplex对象,并拷贝给cpx。
与之相对应,可以通过转换运算符,将一个类的对象转换成其他类型的数据。对于类X,转换后的类型为T,则类型转换运算符的形式为:
X:operatorT()
{
//…
}
如
classCComplex
{
public:
double m_fReal;
double m_fImag;
CComplex(){};
CComplex(double fReal, double fImag=0)
{
m_fReal=fReal;
m_fImag=fImag;
};
operator double()
{
return m_fReal;
}
};
intmain()
{
CComplex cpx(2,3);
double f=double(cpx);
return 0;
}
类型转换运算符的特点是:
1) 待转换类型是类自己
2) 转换后的目标类型要和operator构成了运算符重载的函数名称即operator double
3) 类型转换运算符没有返回类型,但必须要写return语句返回目标类型的值。
下面是一个类型转换函数的例子,将复数类的转换为一个字符串。
operatorchar*()
{
char* s = (char*)malloc(100);
sprintf(s,"Value:(%.2f,%.2f)",real,imag);
return s;
}
同类型的转换构造函数和类型转换函数同时存在时,会发生二义性,例子:
classComplex
{
public:
Complex(){real=0;imag=0;}
Complex(double r,double i){real=r;imag=i;}
Complex operator+(const CComplex &cpx1);
//转换构造函数
Complex(double c){real = c; imag = 0;}
//类型转换函数
operator double()
{
return real;
}
public:
double real;
double imag;
};
intmain()
{
Complex a,b,c;
c = a+b;
c = a+2.5;
c = 2.5+a;
return 0;
}
其中语句a+b不会发生二义性,C++编译器会定位到加的双目运算符函数并调用,函数返回值赋给变量c。语句a+2.5和2.5+a则要发生二义性,因为编译器可以变量a调用转换构造函数产生一个double数据类型,进行两个double数据类型的加法,也可以将常数2.5调用转换构造函数生成一个复数类型的变量,调用两个复相加的运算符函数,函数返回一个复数类变量,并赋给变量c。
流运算符的重载<<>>
流运算符的最大特点是其第一个参数类型一定是类ostream或istream,并且返回值也是类ostream或istream,这样才能将自定义类的变量输出或输入到流中。显然,流运算符不能通过类的成员函数重载,因为通过类的成员函数重载必须是运算符的第一个参数是类自己。一般我们用友员来实现流运算符的重载,不用普通函数来实现。参见例2。
前加++/--和后加++/--运算符的重载
前加++/--和后加++/--运算符是一种单目运算符的重载,正数和负数单目运算符的重载与此类似。其由于通过类的成员函数实现运算符的重载,第一个参数一定得是类自己,所以前加++的函数原型中没的参数。如下:
ComplexComplex ::operator++()
{
return Complex(++real,++imag);
}
后加++也是单目运算符,C++编译器如何区分后加运算符的情况呢?C++编译器将后加运算符后加++/--强行加一个参数类型int,该参数的作用是仅仅区分之用。
Complex Complex ::operator++(int) //after ++
{
Complex temp(*this);
temp.real++;
temp.imag++;
return temp;
}
赋值运算符的重载
重载方法
缺省的赋值运算符是实行对象间的按位拷贝,如果类成员中含有指针类型的数据成员,一般应该将该类的赋值运算符重载,因为这时调用缺省的赋值运算符没有意义。如:
classCMyString
{
private:
char *m_pszData;
public:
CMyString(char *pszData); //构造函数
CMyString(CMyString &objStr); //拷贝构造函数
CMyString &operator=(CMyString&objStr); //重载=操作符
CMyString &operator=(char *pszData); //重载=操作符
~CMyString()
{
delete []m_pszData;
}
};
CMyString::CMyString(char*pszData)
{
m_pszData=new char[strlen(pszData)+1];
strcpy(m_pszData,pszData);
}
CMyString::CMyString(CMyString&objStr)
{
m_pszData=newchar[strlen(objStr.m_pszData)+1];
strcpy(m_pszData,objStr.m_pszData);
}
CMyString&CMyString::operator=(CMyString &objStr)
{
if(this==&objStr)
return *this;
delete []m_pszData;
m_pszData=newchar[strlen(objStr.m_pszData)+1];
strcpy(m_pszData,objStr.m_pszData);
return *this;
}
CMyString&CMyString::operator=(char *pszData)
{
delete []m_pszData;
m_pszData=new char[strlen(pszData)+1];
strcpy(m_pszData,pszData);
return *this;
}
intmain()
{
CMyString s1="abc";
CMyString s2="xyz";
s1="123";
s1=s2;
return 0;
}
赋值运算符和拷贝构造函数的区别和联系
相同点:都是将一个对象COPY到另一个中去
不同点:拷贝构造函数涉及到要新建立一个对象。在以下三种情况下要调用拷贝构造函数:
1) 形如 A a = b;由于对象a是新建的对象,所以这种情况并不调用赋值运算符,而是要调用拷贝构造函数
2) 对象做函数参数时,由于要在堆栈中建立新对象,所以要调拷贝构造函数
3) 对象是函数的返回值,而赋值运算符不涉及到对象的建立。
在一个对象中如果不定义赋值运算符和拷贝构造函数,编译器将为之生成默认的赋值运算符和拷贝构造函数,它只进行按位COPY,来实现对象的COPY或赋值。一般来讲当一个类定义有指针,默认的赋值运算符和拷贝构造函数将不能完成对象的赋值和COPY,因为按位COPY的结果是将一对象的指针值COPY 到另一个对象中,这会引起相同的内存被多个对象引用的错误。
这时要注意一个问题:引用或指针做函数参数或返回值时,函数调用时会不会调用类的拷贝构造函数呢?是不会的,因为函数在调用时不会涉及到新对象的建立,仅仅是将原对象的地址进行了COPY。
[]下标运算符重载
标准情况下,[]运算符用于访问数组的元素。我们可以通过重载下标运算符为类运算符。使得可以象访问数组元素一样的访问对象中的数据成员。C++只允许把下标运算符重载为非静态的成员函数。
下标运算符的定义形式为:
T1T::operator[] (T2);
其中T1为希望返回的数据类型,T为类名,T2为下标,它可以是任意类型。如需访问第5节中的CMyString的某个字符的话,在类中可声明重载[]运算符:
charoperator[](int iIndex);
在外部定义该运算符重载函数
charCMyString::operator[](int iIndex)
{
if(iIndex<strlen(m_pszData))
return m_pszData[iIndex];
return 0;
}
new和delete重载
通过重载new和delete,我们可以自己实现内存的管理策略。new和delete只能重载为类的静态运算符。而且重载时,无论是否显示制定static关键字,编译器都认为时静态的运算符重载函数。
重载new时,必须返回一个void *类型的指针,它可以带多个参数,但第一个参数必须是size_t类型,该参数的值由系统确定。
classCTest
{
//...
void *operator new(size_t nSize)
{
cout<<"newcalled,size="<<nSize
void *pRet=::new char[nSize];
return pRet;
}
};
重载delete时必须返回void类型,它可以带有多个参数,但第一个参数必须时要释放的内存的地址,其类型为void *,如果重载delete时指定了第二个参数,第二个参数必须为size_t类型。
接上:
classCTest
{
//...
void *operator new(size_t nSize)
{
cout<<"newcalled,size="<<nSize
void *pRet=::new char[nSize];
return pRet;
}
void operator delete(void *pVoid)
{
cout<<"deletecalled"<<endl;
::delete []pVoid;
}
};
一个类可以重载多个new运算符,但是只能重载一个delete类运算符。
指针运算符->的重载
classCDataSetPtr
{
private:
CDataSet *m_pDataSet;
public:
CDataSetPtr()
{
m_pDataSet=new CDataSet;
}
~CDataSetPtr()
{
delete m_pDataSet;
}
CDataSet * operator->()
{
return m_pDataSet;
}
};
voidmain()
{
CDataSetPtr pDataSet;
int iValue;
pDataSet->GetField(iValue,"Title");
}
例1通过复数类的相加运算符演示运算符重载的几种实现方式
如有下列复数类:
classCComplex
{
public:
double m_fReal;
double m_fImag;
char m_szStatus[32];
CComplex(){
m_fReal=0;
m_fImag=0;
}
CComplex(doublefReal,double fImage){
m_fReal=fReal;
m_fImag=fImage;
}
};
如果要实现将该类的两个对象的实部间相加、虚部间相加生成一个新的对象,即:对象3=对象2+对象1;
1) 如果用普通函数的实现运算符+的重载,可以用如下形式
CComplexoperator+(const CComplex &cpx1,const CComplex &cpx2)
{
CComplex cpxRet;
cpxRet.m_fReal=cpx1.m_fReal+cpx2.m_fReal;
cpxRet.m_fImag=cpx1.m_fImag+cpx2.m_fImag;
return cpxRet;
}
通过普通函数重载来实现,必须将复数类的私有数据成员公开,这破坏了类的封装性
2) 由于为类重载运算符函数往往要访问类的成员,而类为了封装性的需要,往往将成员声明为非公有的,因此往往将一个运算符函数重载为类的成员函数或者友员函数。如果CComplex的m_fReal,m_fImag是类的私有或者保护成员,那么普通函数无法访问,在此情况下就不能实现上述重载功能,此时,可以通过友员函数的方式实现运算符+的重载。
如下声明方式,可以在类中声明友员函数上面的普通函数为友员函数
classCComplex
{
private:
double m_fReal;
double m_fImag;
char m_szStatus[32];
public:
CComplex()
{
m_fReal=0;
m_fImag=0;
}
CComplex(double fReal,double fImage)
{
m_fReal=fReal;
m_fImag=fImage;
}
friend CComplex operator+(const CComplex&cpx1,const CComplex &cpx2);
};
函数的实现与普通函数相同。
3) 也可以将运算符重载为类的成员函数,如下:
classCComplex
{
private:
double m_fReal;
double m_fImag;
char m_szStatus[32];
public:
CComplex(){
m_fReal=0;
m_fImag=0;
}
CComplex(doublefReal,double fImage){
m_fReal=fReal;
m_fImag=fImage;
}
CComplex operator+(const CComplex &cpx);
};
并在类外定义该成员函数:
CComplexCComplex::operator+(const CComplex &cpx2)
{
CComplex cpxRet;
cpxRet.m_fReal=this->m_fReal+cpx2.m_fReal;
cpxRet.m_fImag=this->m_fImag+cpx2.m_fImag;
return cpxRet;
}
4) 运算符重载函数如何调用呢?
可以象调用一般的函数一样调用运算符重载函数,运算符重载函数的函数名称是operator+
例如:
void main()
{
CComplex c1,c2,c3;
C3 = c1. operator+(c2);
}
但这样书写很不直观,也不方便。C++编译提供了一种简写的方式,如下:
c3= c1+c2
例2通过友员函数来实现复数类的流运算符重载
classComplex;
ostreamoperator<<(ostream& os, Complex& c);
istreamoperator>>(istream& is, Complex& c);
Complexoperator+(Complex &first, Complex &second );
classComplex
{
public:
Complex(){real=0;imag=0;}
Complex(double r,double i){real=r;imag=i;}
//转换构造函数
Complex(double c){real = c; imag = 0;}
//类型转换函数
operator double()
{
return real;
}
operator char*()
{
char* s = (char*)malloc(100);
sprintf(s,"Value:(%.2f,%.2f)",real,imag);
return s;
}
friend Complex operator+(Complex &first,Complex &second );
friend ostream operator<<(ostream&os, Complex& c);
friend istream operator>>(istream&is, Complex& c);
void display()
{
cout<<"("<<real<<","<<imag<<"i)"<<endl;
};
private:
double real;
double imag;
};
Complexoperator+(Complex &first, Complex &second )
{
return Complex(first.real+second.real,first.imag+second.imag);
}
istreamoperator>>(istream& is, Complex& c)
{
cout<<"input a complex:\n"<<endl;
is>>c.real>>c.imag ;
return is;
}
ostreamoperator<<(ostream& os, Complex& c)
{
os<<"complex value is:("<<c.real<<","<<c.imag<<")"<<endl;
return os;
}
intmain()
{
Complex cc,ccc;
cin>>cc>>ccc;
cout<<cc<<ccc<<endl;
return 0;
}
上述实例中第一条语句含义如何理解?它是类的前导声明,目的是让编译器知道标识符Complex是一个类,以便将友员函数编译通过。
语句cout<<cc<<ccc<<endl如何理解?
cout是ostream类的全局变量,cout<<cc将导致友员函数的调用,并返回一个临时的ostream对象,相当于operator<<(cout, cc)。所以该语句相当于下述伪码:operator<< ((operator<<((operator<<(cout, cc)), ccc)), endl);
语句cin>>cc>>ccc的理解与输出的流提取符的理解是完全一样的