操作符重载浅析

操作符重载浅析
下面的文章记录的是,我在研究了C++ Primer的操作符重载这一章,并结合自己之前遇到过的操作符重载相关知识之后,对操作符重载的理解,内容不是很深,主要涉及到的是操作符重载经常使用的环节,对于一些比较深的以及比较复杂且不常用的,我没有做深入的研究,因为我认为即使花时间把它们研究了,以后也不一定用的上,再加上如果常时间不用,这些比较难的知识点也就慢慢忘记了,所以与其如此,到不如把精力放在操作符重载的基础知识和最常用的知识点上,加深对它们的理解以及熟练运用程度,至于哪些比较难的,可以再以后需要时现查也是来得及的。
当然这只是我对常用知识和非常用知识点的一点看法,如果可以全都理解固然是最好的,但如果精力时间有限的话,我想还是现把重要的完全吃透才是最重要的。俗话说的好:好钢要用在刀刃上。。。。
好了,废话说了一大堆,现在转入正题:
一、什么是操作符重载?
一看到重载,很容易就让人联想到成员函数重载,函数重载可以使名称相同的函数具有不同的实际功能,只要赋给这些同名函数不同的参数就可以了,操作符重载也是基于这一机制的。系统为我们提供了许多操作符,比如“+”,“[ ]”等,这些操作符都有一些默认的功能,而操作符重载机制允许我们给这些操作符赋予 不同的功能,并能够按照普通操作符的使用格式来使用自己定义功能的操作符(即重载的操作符)
定义之后,我们就可以 按照平常使用操作符的格式来使用我们自己的重载操作符了。
操作符重载一般在 类内部定义,就像成员函数一样定义,这叫做类成员重载操作符。当然也可以在类外定义,即非类成员操作符重载。
二、为什么要使用操作符重载?
举例说明,比如类String,该类有这样一个功能,可以将两个字符串连接成一个字符串,为此,我们可以给类String定义一个成员函数实现此功能,可以给该函数取一个形象的名字,比如concatenate或append,但是相比较,这两个名字都不如操作符“+=”形象直观。在这种情况下,我们就可以定义操作符“+=”的重载,来实现此功能。
也就是说,如果要定义一个函数,而这个函数的功能与操作符的功能比较类似时,这个时候我们就可以定义重载操作符,而不使用通常的成员函数定义。这里所说的 操作符重载,指的是与系统定义的操作符重载,而不是说定义两个“ += ”,这两个重载,这一点需要清楚。
但是这四个操作符不能用于重载: :: *   ? :
三、如何声明操作符重载?
同普通函数类似,只不过它的名字包括 关键字 operator ,以及紧随其后的一个预定义操作符。例如:
String& operator+=(const String&);
String& operator+=(const char*);
注意:上面的括号表示形式参数,即使操作符重载不需要参数,也应该写上一个空的“( )”,而不是将其省略,这一点其实和普通函数的声明是类似的。其实, 声明的唯一区别就是名字不同而已
四、怎样使用操作符重载?
两种操作符重载:类成员操作符重载和非类成员操作符重载。
1、类成员操作符重载
已知类String中声明了两个“==”操作符重载,分别是:
bool operator==(const char*) const;
bool operator==(const String&) const;
其中第一个重载的操作符允许我们比较一个String类对象是否等于一个C风格字符串,第二个允许我们比较两个String类对象是否相等。
示例代码:
#include
int main()
{
       String flower;
       If(flower==”lily”) //正确:调用bool operator==(const char*) const;
       ……
       else
              if(“tulip”==flower) //错误
              …….
}
关键看一下,为什么第二个重载操作符的使用是错误的?
因为: 只有在左操作数是该类类型的对象时,才会考虑使用作为类成员的重载操作符。
因为这里的”tulip”不是String类型对象,所以编译器试图找到一个内置操作符,它可以有一个C风格字符串的左操作数,然而事实上并不存在这样的操作符,所以编译时产生错误。
疑问:我们可以使用String类的构造函数将一个C风格字符串,转换成一个String对象,为什么编译器不能做以上转换呢?即
      if(String(“tulip”)==flower);//这样就是正确的
答:为了效率和正确性
重载操作符并不要求两个操作数的类型一定相同。可能有这样一个类Text,这个类的构造函数的参数及其成员重载操作符的参数都与String类一致,如果使编译器能够自动将C风格字符串转换成某个类型的对象,那么编译器首先会检索所有的类定义,选择能够提供正确构造函数和重载操作符的类进行转换,这无疑会增加程序的编译时间,还有就是类String和类Text均合适,编译器也不知道该将C风格字符串转换成String还是Text对象了。
 
对于类成员重载操作符,隐式的this指针被用作隐式的第一个参数,对于成员操作符, flower==”lily” 会被编译器重写为: flower.operator==(“lily”);
 
2、非类成员操作符重载
为了解决上面的问题,我们可以考虑使用 非类成员操作符代替类成员操作符,这样做的好处是左操作数不必非要是某个类的类型对象了,对于需要两个操作数的操作符重载,我们就可以定义两个参数了。比如:
bool operator==(const String&,const String&);
bool operator==(const String&,const char*);
可以看到,这两个全局重载操作符比成员操作符多了一个参数。
这样定义之后,还是上面的代码,当调用flower==”lily”时,会调用上面的bool operator==(const String&,const char*);。
然而“tulip”==flower会调用哪个操作符重载呢,我们并没有定义bool operator==(const char*,const String&);,我们是不是必须定义这样一个全局操作符重载呢?答案是否定的,因为当一个重载操作符是一个名字空间函数时,对于操作符的第一个和第二个参数,即等于操作符的左右两个操作数都会考虑转换,就像
int vi=1; double vd=2.0; vi=vi+vd; 会先将vd转换成int型,再做加法一样
这意味着,编译器将解释第二个用法如下:
bool operator==(String(“tulip”),flower)。这样会增加系统转换开销。
因此,如果需要频繁比较C风格字符串和String对象,那么最好定义上面的操作符重载,如果不频繁,我们只需定义下面一个就够了:
bool operator==(const String&,const String&);
 
分析:什么时候定义类成员操作符重载,什么时候定义非类成员操作符重载?
答:(1)如果一个重载操作符是类成员,那么只有当跟它一起使用的左操作数是该类对象时,它才会被调用,如果该操作符的左操作数必须是其他类型,那么重载操作符必须是非类成员操作符重载。
(2)C++要求,赋值(=),下标([ ]),调用(())和成员访问箭头(->)操作符必须被指定为类成员操作符,否则错误。
 
 
我们只能为 类类型或枚举类型的操作数定义重载操作符,我们可以这样实现:把重载操作符声明为类的成员,或者声明为非类成员重载操作符但同时至少有一个类或者枚举类型的参数(按值传递或按引用传递)。这句话的意思,实际是说我们不能修改内置类型的操作符重载或添加操作符重载,比如下面的声明便是错误的:
int operator+(int,int);
这改变了内置类型int的+操作符的功能,所以错误。
 
操作符重载相关知识点:
五、友元(friend)
考虑到类成员操作符重载可以访问类中的私有变量,但是非类成员重载操作符却不能很方便的访问类的私有成员,为了方便起见,我们可以通过使用 友元( friend 的方式,方便的访问类的私有成员。
举例:
class String
{
       friend bool operator==(const String&,const String&);
       friend bool operator==(const String&,const char*);
       public:
              //……..
       private:
              //………
}
注意: friend 声明紧跟在类名之后,而不是放在 public private protected 中,因为友元不是授权类的成员,并且该关键字只能出现在类中。
经过上述声明之后,我们的非类成员重载操作符就可以直接方位String类的私有成员了。当然我们也可以不使用友元,而使通过该类的共有成员函数来间接访问该类的私有成员也是可以的,内联函数inline就不错,效率也不低。
 
由此看来,声明友元( friend )主要是为了方便高效的访问类的私有成员变量。
分析:什么时候应该使用友元:
(1)        某个类不提供公有的访问私有成员的函数。
(2)        使用共有成员函数访问私有成员变量效率比较差时。
 
友元除了用在非成员的重载操作符外,一个名字空间函数(比如全局函数),另一个在此之前定义的类的成员函数或者一个完整的类,均可以声明为某个类的成员。
如下声明:
class B;
class A
{
       friend class B;
       public:
              //…….
       Private:
              //……
}
通过上述声明,类A的成员函数可以访问所有的类B的私有成员,同样类B的成员函数可以访问所有类A的私有成员。
 
六、 类类型对象的隐式类型转换
我们知道,系统提供的内置类型有隐式类型转换的功能,实际上 我们也可以为自己编写的类提供隐式类型转换功能。这样,当我们在使用这种类的对象时,如果需要,编译器会自动调用该类的类型转换函数,实现类型转换功能。
C++提供了这样一种机制,通过它,每个类都可以定义一组“可被应用在该类型对象上的转换”
举例:
class SmallInt
{
       public:
              SmallInt(int ival):val(ival){};
              operator int(){return value;} ;// 类型转换操作符 SmallInt->int
       private:
              int value;
}
在上例中,操作符int()是一个转换函数,它定义了一个用户转换,实现在类类型和转换函数中指定的类型之间的转换,本例中,目标类型是int。
经过上面的定义后,SmallInt对象便可以用在任何可以使用int的地方,例如:
SmallInt si(3);
si+3.1415926;
这样,首先调用SmallInt转换函数,产生int型值3,然后将3变为3.0,和3.1415926相加。
实际上,上面类中定义转换函数还有一个好处就是:
省略了为该类定义重载操作符,尤其是重载操作符参数类型多样时。
1、   转换函数
格式: operator type( ); // 参数必须为空,且 ( ) 不能省略,并且无返回值类型。
说明: type 表示目标类型,它可以用内置类型,类类型或 typedef 名取代,但不允许表示数组,转换函数必须是类的成员函数,并且不能指定返回类型和参数表。
 
显式的强制类型转换会导致调用转换函数,如果被转换值的类型是一个类类型,它有一个转换函数,并且该转换函数的类型是强制转换所指定的类型,则调用这个类的强制转换函数。
比如:
char* tokName= static_cast(tok);//tok表示对象,它所属的类提供一个到char*的转换函数。
2、   用构造函数作为转换函数
在一个类的构造函数中,凡是 只带有一个参数的构造函数,例如SmallInt的构造函数SmallInt(int),都定义了一组隐式转换, 把构造函数的参数类型转换成该类类型。
注意:是将int转换成SmallInt型。和前面说的转换函数功能正好相反。
举例:
void calc(SmallInt);
int  vi;
calc(vi);
这时,编译器就会隐式调用类SmallInt的构造函数SmllInt(int),将vi转换成SmallInt对象,然后再将这个对象传递给函数calc( )。
可以这样理解:
{
       SmallInt temp=SmallInt(i);
       Calc(temp);
}
这个大括号指出temp这个临时对象的生命周期。
 
注意:编译器不会使用一个显式构造函数(关键字explicit标志)来执行隐式类型转换,但是却可以使用这样的构造函数来进行强制转换(即static_cast<>)。
 
这只是我自己对操作符重载的一点理解,说得比较简单通用,没有针对具体的操作符重载展开讨论,以后空下来,我会将它们补齐的,好了先写这么多,错误之处还请有心留意的网友指正。
 
 

你可能感兴趣的:(C&C++)