1、基本操作符重载
操作符重载指的是将C++提供的操作符进行重新定义,使之满足我们所需要的一些功能。
在C++中可以重载的操作符有:
+ - * / % ^ & | ~ ! = < > += -= *= /= %= ^= &= |=
<< >> <<= >>= == != <= >= && || ++ -- , ->* -> () []
new new[] delete delete[]
上述操作符中,[]操作符是下标操作符,()操作符是函数调用操作符。自增自减操作符的前置和后置形式都可以重载。长度运算符“sizeof”、条件运算符“:?”成员选择符“.”、对象选择符“.*”和域解析操作符“::”不能被重载。
为了介绍基本操作符的重载,我们先来看一个操作符重载的示例。
例1:
在这个例子中,我们定义了一个复数类,一个复数包含实部和虚部两部分,我们分别用real和imag来表示复数的实部和虚部,并将这两个变量作为复数类的成员变量,并设置为private属性。在复数类中,我们定义了三个构造函数用于初始化复数类的对象。之后声明了四个操作符重载函数,分别重载加减乘除四种操作符。最后定义了一个打印复数的函数display。
为了避免大家不熟悉复数的四则运算,在这里将其一一列出,以助于大家对例1的理解。
( a + bi ) + ( c + di ) = ( a + c ) + ( b + d )i
( a + bi ) – ( c + di ) = ( a – c ) + ( b – d )i
( a + bi ) * ( c + di ) = ( ac – bd ) + ( ad + bc )i
( a + bi ) / ( c + di ) = ( ac + bd ) / ( c2 + d2 ) + [ ( bc – ad ) / ( c2 + d2 ) ]i
在例1中我们是将操作符重载函数声明为成员函数,在这个例子中我们首先需要关注的是操作符重载函数的声明。如例中所示,重载加法操作符时,其函数声明为“complex operator+(const complex & A)const;”,两个复数相加,函数返回值为complex类对象,仍然为一个复数。复数相加需要两个复数进行运行,调用函数的类对象本身就是一个复数,因此还需要另外一个复数作为参数,在该函数声明中函数的参数为复数类对象的引用,并且加上了const关键字以确保在函数中不会修改该对象的引用。我们再来看一下加法操作符重载函数的类外定义,在函数体中,我们先定义了一个复数类对象B,之后按照复数的加法运算规则将计算结果赋给B的real 和imag成员变量。由于系统会给类生成一个默认的拷贝构造函数,因此采用传值的形式将计算结果返回没有问题。
调用操作符重载函数的语法和调用普通成员函数语法相同,例如我们定义三个complex类的对象A、B和C,调用的语法是C = A.operator+ ( B );,由于operator关键字的作用,operator+这个成员函数也可以通过我们非常熟知的方法调用:C = A + B;,采用这种方式调用operator+这个操作符重载函数,意义非常明了,在本例中我们也都是按照这种方式调用的。在本例中我们是将操作符重载函数声明为类的成员函数,在我们进行加法运算时需要两个操作数,也即需要两个复数对象,在声明操作符重载函数时,“complex operator+(const complex & A)const;”确只有一个函数参数,这一点无需奇怪,我们看其调用方式就明白了。比较规范的调用方法是C = A.operator+ ( B );,此处是A对象调用该函数,并且函数参数是另一个复数类对象。如此一来,两个操作数分别是A和B。加法操作符二元操作符,但是将其作为成员函数声明时则只需要有一个参数即可,如果重载的操作符是一元操作符,并将其作为类的成员函数,则不需要参数。
例2:
例2中重载的是一元操作符“!”(非),因为是作为成员函数重载的,因此不需要参数。
2、重载操作符的优先级
重载操作符不能改变操作符的优先级和语法特性。例如上一节复数类中的加法操作符重载函数,重载后加法的优先级仍然保持不变,重载后仍然为二元操作符。
例1:
继续沿用上一节的例1中的complex复数类,在例1中定义了该类的四个对象,然后进行四则运算,c4 = c1 + c2 * c3; 这一语句亦等同于c4 = c1 + ( c2 * c3 ) ;,虽然在复数类中重载了加减乘数四个操作符,但是并不会改变它们为二元操作符的特性,同时也不会改变它们的优先级,因此对于重载后的加法操作符而言,其优先级是低于乘法操作符的。
重载后的操作符其用法不会被改变,例如加法操作符总是出现在两个操作数之间,重载后,使用加法操作符仍然是必须将加法操作符置于两个操作数之间。例如例1中的加法和乘法操作符在重载后仍然保持原有的使用方法。
3、用顶层函数重载操作符
在前面两节中,我们是将操作符重载函数声明为类的成员函数,其实除了能将操作符重载函数声明为类的成员函数之外,我们也可以将操作符重载函数声明为顶层函数。在前面将操作符重载函数声明为类成员函数时,我们不断强调二元操作符,其函数参数为一个,一元操作符重载函数不需要函数参数。但是一旦将操作符重载函数声明为顶层函数时,则必须至少有一个类对象参数,否则的话编译器无法区分操作符是系统内建的还是程序设计人员自己定义的,有了一个类对象参数之后,系统则会根据情况调用内建或自定的操作符。如果以顶层函数的形式重载操作符时,二元操作符重载函数必须有两个参数,一元操作符重载必须有一个参数。
例1:
如本例所示,本例则是用顶层函数重载的加法、减法、乘法和除法操作符,使之分别具有加减乘除功能。因为是以顶层函数的形式重载操作符的,因此类中没有声明操作符重载函数。为了能够在类外操作real和imag两个数据成员,我们为类添加了getimag、getreal、setimag和setreal函数。我们以加法操作符的重载为例来看普通操作符重载函数如何作为顶层函数。
加法操作符重载函数的函数头complex operator+(const complex & A, const complex &B),首先因为加法操作符重载后可以计算复数的加法,返回的仍然是一个复数,因此该函数的返回值仍然是complex。操作符重载函数参数为两个complex类对象的引用,加法操作符为二元操作符,因此必须要有两个操作数,因此函数有两个参数。函数体部分比较简单,只是使用getimag、getreal、setimag和setreal四个函数来实现复数的加法操作而已。
其它普通操作符的重载与例1中的加法操作符重载类似。如果我们重载的是一元操作符,则函数需要有一个参数。
例2:
本例中以顶层函数的形式重载非操作符符,因为其为一元操作符,故而函数有一个参数。
以顶层函数的形式重载操作符,其调用方法与普通函数调用类似。
例如本节中的例1中的复数类,我们调用加法重载函数时可以采用如下方法:
complex c1, c2, c3;
c3 = operator+( c1, c2);
这样的函数调用方法和普通的函数调用方法一样,但是由于operator关键字的作用,我们还可以采用另外一种我们熟知的调用方法:
c3 = c1 + c2;
这种调用方法和先前以类成员函数的形式重载操作符调用方法一直。本节例1中也都是采用这种简单明了的调用方法。
需要注意的是指针操作符“->”、下标操作符“[]”、函数调用操作符“()”和赋值操作符“=”只能以成员函数的形式进行操作符重载。
以顶层函数的形式重载操作符,从函数实现上来看其实现相对于以类成员函数的形式实现起来要复杂一些,因为在类外无法直接访问类的私有成员变量。但是以顶层函数的形式来重载操作符有自身的优势,我们来看下面的示例。
例3:
本例中是以成员函数的形式进行操作符重载的,在主函数中我们定义了两个complex复数类的对象,语句“c1 = c2 + 13.5;”是将c2与一个double类型的数据相加,我们可以将其理解为:
c1 = c2.operator+(13.5);
因为我们在类中定义了一个只带一个参数的构造函数complex(double a);,这个构造函数其实可以视为转型构造函数,它可以将double类型转换为一个complex类对象。因此 “c1 = c2 + 13.5;”语句其实也是相当于两个复数类对象相加。当然,如果在类中没有定义complex(double a);这样一个只带一个参数的构造函数,那么这一句也是有语法问题的,因为我们重载的加法只适用于两个complex类对象相加,而系统内建的又只能用于两个普通数据类型的操作数相加,一个complex类对象和一个普通数据类型的操作数相加,系统是无法去处理这样的异常情况的。
我们再来看一下后面一个语句“c1 = 13.5 + c2;”,这一语句我们可以将其理解为:
c1 = 13.5.operator+(c2);
如此一来,这一句的问题非常明显,13.5只是一个double类型的常数,它不是类对象,因此也不可能有调用operator+的能力。虽然我们在类中定义了一个具有一个参数的构造函数,但是编译器将语句“c1 = 13.5 + c2;”理解成“c1 = 13.5.operator+(c2);”并不会将13.5转换成一个complex类对象,因为编译器遇到这种情况并不会产生一种很智能的处理,同样它也并不知道程序设计人员的意图。所以例3中语句“c1 = 13.5 + c2;”是有语法错误的。
例4:
我们再来看一下例4,这个例子则是以顶层函数的形式定义操作符重载函数。我们同样来看主函数,主函数定义了c1和c2两个complex类对象。先来看一下语句“c1 = c2 + 13.5;”,这个语句可以理解如下:
c1 = operator+(c2, 13.5);
因为我们在顶层函数中定义了complex operator+(const complex & A, const complex &B)函数,系统在执行“c1 = operator+(c2, 13.5);”时找到了对应的顶层函数,但是发现参数不对,但是可以通过类的构造函数将13.5转换成complex类对象,如此就满足operator+函数的调用条件了,故而这一句是没有问题的。
我们再来看一下语句“c1 = 13.5 + c2;”,这一语句可以理解为:
c1 = operator+(13.5, c2);
这一句的执行与“c1 = operator+(c2, 13.5);”是一样的,它可以利用类的构造函数将13.5转换为complex类对象,因此这一句也是可以正确执行的。
从例3和例4两个例子中,我们不难看出虽然实现麻烦的以顶层函数的形式进行操作符重载的优势所在了。我们总结一下,以类成员函数的形式进行操作符重载,操作符左侧的操作数必须为类对象;而以顶层函数的形式进行操作符重载,只要类中定义了相应的转型构造函数,操作符左侧或右侧的操作数均可以不是类对象,但其中必须至少有一个类对象,否则调用的就是系统内建的操作符而非自己定义的操作符重载函数了。
在例4中,我们以顶层函数的形式进行操作符重载,但是因为无法直接访问complex类中的私有成员,故而我们在类中增添了getimag、getreal、setimag和setreal函数以操作类中的私有成员变量,如此一来实现这些操作符重载函数看上去就有些复杂了,不是那么直观。除了此种方法以外,我们还可以将complex类中的私有成员real和imag声明为public属性,但是如此一来就有悖类的信息隐藏机制了。除了这两种方法外,我们是否还有其它方法解决这个问题呢?
有,还有一种方法。在前面章节中我们介绍过友元函数,如果我们将操作符重载函数这些顶层函数声明为类的友元函数,那么就可以直接访问类的私有成员变量了。
例5:
本例就是采用友元函数的形式进行操作符重载,如此实现既能继承操作符重载函数是顶层函数的优势,同时又能够使操作符重载函数实现起来更简单。(mine:注意这边的友元函数不一定非是另一个类的函数)
4、重载输入与输出操作符
在C++中,系统已经对左移操作符“<<”和右移操作符“>>”分别进行了重载,使其能够用作输入输出操作符,但是输入输出的处理对象只是系统内建的数据类型。系统重载这两个操作符是以系统类成员函数的形式进行的,因此cout<< var语句可以理解为:
cout.operator<<( var )
如果我们自己定义了一种新的数据类型,需要用输入输出操作符去处理,则需要重载这两个操作符。在前面我们已经定义了complex类,如果我们需要直接输入输出复数的话我们可以对这两个操作符进行重载。下面将以complex为例说明重载输入输出操作符的方法。
我们可以重载输入操作符,使之读入两个double类型数据,并且将之转换为一个复数,并存入到一个复数类对象中。我们采用顶层函数的形式来实现输入操作符的重载。
在上面函数中istream是指输入流,这个将会在后面讲到。因为重载操作符函数需要用到complex类的私有成员变量,为了方便,我们将这个函数声明为complex类的友元函数。其声明形式如下:
friend istream & operator>>(istream & in , complex & a);
该函数可以按照如下方式使用:
complex c;
cin>> c;
有了这两个语句后,我们输入(↙表示用户按下enter键)
1.45 2.34↙(mine:中间空格表示在一个输入)
之后这两个数据就分别成立复数类对象c的实部和虚部了。“cin>> c;”这一语句其实可以理解为:
operator<<(cin , c);
在重载输入操作符时,我们采用的是引用的方式进行传递参数的,输入的参数里面包含一个istream流的引用,返回值仍然为该引用,因此我们仍然可以使用输入操作符的链式输入。
complex c1, c2;
cin>> c1 >> c2;
同样的,我们也可以将输出操作符进行重载,使之能够输出复数。函数在类内部的声明如下:
Friend ostream &(ostream & out, complex & A);
顶层函数的实现如下:
与istream一样,ostream用于表示输出流,同样为了能够直接访问complex类的私有成员变量,我们将其在类内部声明为complex类的友元函数,同样该输出操作符重载函数可以实现链式输出。结合输入输出操作符的重载,我们将complex类的完整实现重新以示例的形式列出如下。
例1:
在本例中,我们均采用顶层函数的形式进行操作符重载,同时为了能够方便访问类中的私有成员变量,我们将这些操作符重载函数声明为complex类的友元函数。我们直接来看主函数。在主函数的一开始我们定义了三个complex类的对象,并且c1和c2都用构造函数对复数进行赋值了。在这一步我们其实也可以用
cin>> c1 >> c2;
来替代,因为我们已经重载了输入操作符。在之前输出复数都必须通过类中的display函数,但是重载了输出操作符之后,就不需要这么麻烦了,直接输出就可以了,写法上简单不少。
5、重载赋值操作符
赋值操作符“=”可以用来将一个对象拷贝给另一个已经存在的对象。如果我们重新定义了一种新的数据类型,比如说复数类,那么我们就需要重载一下赋值操作符,使之能够满足我们的赋值需求。当然拷贝构造函数同样也会有此功能,拷贝构造函数可以将一个对象拷贝给另一个新建的对象。如果我们没有在类中显式定义拷贝构造函数,也没有重载赋值操作符,则系统会为我们的类提供一个默认的拷贝构造函数和一个赋值操作符。前面在介绍类的相关知识时已经提到,系统为我们提供的默认的拷贝构造函数只是将源对象中的数据一一拷贝给目标对象,而系统为类提供的赋值操作符也是这样的一种功能。
例1:
利用前面我们定义的complex类,我们先定义了两个complex类的对象c1和c2,c1对象通过带参构造函数初始化,之后用c1来初始化c2,最后输出这两个复数类对象。在前面定义复数类时我们并未定义拷贝构造函数,也没有重载过赋值操作符,但是在例1中“c2 = c1”并未有语法错误,并且根据函数输出结果也可以得知可以完成我们所需要的赋值操作。这是因为系统默认为类提供了一个拷贝构造函数和一个赋值操作符,而数据一对一的拷贝也满足我们复数类的需求了。
在前面介绍拷贝构造函数时我们提到过这种系统提供的默认拷贝构造函数有一定缺陷,当类中的成员变量包含指针的时候就会有问题,会导致一些意想不到的程序漏洞,此时则需要重新定义一个拷贝构造函数,同样的此时系统提供的赋值操作符也已经不能满足我们的需求了,必须要进行重载。在前面介绍拷贝构造函数那一节中,我们已经详细分析了系统提供的默认拷贝构造函数遇到指针成员变量时带来的风险,其实在直接使用系统默认提供的赋值操作符同样会有此种风险,在此我们将不再重新分析这一问题,而只是将前面的示例再次拿过来,并且在程序中补上赋值操作符重载函数。
例2:
在这个例子中我们以类成员函数的形式重载了赋值操作符,因为前面已经介绍过该程序了,下面就直接来看主函数。主函数中前半部分和之前介绍拷贝构造函数时是相同的,这个我们也忽略过去。直接从arr1 = arr2语句开始看起。这个语句就会调用类中的操作符重载函数,我们可以将这一语句理解为:
arr1.operator=( arr2 );
然后就会执行赋值操作符重载函数的函数体中的代码,在该函数体中我们为arr1重新开辟了一个内存空间,因此就可以规避arr1和arr2中的num指向同一块存储区域的风险。如此一来使用系统默认提供的赋值操作符所带来的风险就可以避免了。在这之后的语句中,我们还修改了arr2中的数据,但是这样的修改并没有影响到arr1,可见确实将风险给化解了。
当然,如果在类中并没有包含需要动态分配内存的指针成员变量时,我们使用系统提供的默认拷贝构造函数和赋值操作符也就可以了,无需再自己多此一举的重新定义和重载一遍的。