一、运算符重载
1、运算符重载
C++允许将运算符重载扩展到用户定义的类型。
要重载运算符,需使用被称为运算符函数的特殊函数形式。运算符函数的格式如下:
operatorop(argument list);
例如,operator+()重载+运算符,operator*()重载*运算符。op必须是一个有效的C++运算符,不能虚构一个新的符号。
例如,将Person对象的体重相加,如果per1、per2和perSum都是Person对象,便可编写这样的等式:
perSum = per1 + per2;
编译器发现,操作数是Person对象,因此使用相应的运算符函数替换上述运算符:
perSum = per1.operator+(per2);
然后该函数将隐式地使用per1对象(因为它调用了方法),显式地使用per2对象(因为他被作为参数传递),来计算总和,并返回这个值。
Time.h
1 #include <stdio.h> 2 class Time{ 3 private: 4 int _hour; 5 int _min; 6 int _sec; 7 public: 8 Time(int hour = 0, int min = 0, int sec = 0); 9 void setHour(int hour); 10 void setMin(int min); 11 void setSec(int sec); 12 int getHour() const; 13 int getMin() const; 14 int getSec() const; 15 void showTime() const; 16 Time operator+(const Time &time) const; 17 };
Time.cpp
1 #include "Time.h" 2 #include <iostream> 3 Time::Time(int hour, int min, int sec){ 4 _hour = hour; 5 _min = min; 6 _sec = sec; 7 } 8 void Time::setHour(int hour){ 9 _hour = hour; 10 } 11 void Time::setMin(int min){ 12 _min = min; 13 } 14 void Time::setSec(int sec){ 15 _sec = sec; 16 } 17 int Time:: getHour() const{ 18 return _hour; 19 } 20 int Time:: getMin() const{ 21 return _min; 22 } 23 int Time:: getSec() const{ 24 return _sec; 25 } 26 Time Time::operator+(const Time &time) const{ 27 int hour, min, sec, min_, hour_; 28 sec = (_sec + time.getSec())%60; 29 min_ = (_sec + time.getSec())/60; 30 31 min = (_min + time.getMin() + min_)%60; 32 hour_ = (_min + time.getMin() + min_)/60; 33 34 hour = (_hour + time.getHour() + hour_)%24; 35 return Time{hour,min,sec}; 36 } 37 void Time:: showTime()const{ 38 printf("时间:%02d:%02d:%02d ",_hour,_min,_sec); 39 }
main.app
1 #include <iostream> 2 #include "Time.h" 3 4 int main(int argc, const char * argv[]) { 5 Time t1{8,12,45}; 6 Time t2{18,45,25}; 7 Time t3 = t1 + t2; 8 t3.showTime(); 9 t3 = t3 + t1 + t2; 10 t1.showTime(); 11 t2.showTime(); 12 t3.showTime(); 13 14 return 0; 15 }
输出结果:
1 时间:02:58:10 2 时间:08:12:45 3 时间:18:45:25 4 时间:05:56:20
注意:在运算符表示法中,运算符左侧的对象是调用对象,运算符右侧的对象是作为参数被传递的对象。
2、运算符重载限制
重载的运算符(有些情况例外)不必是成员函数,但至少有一个操作数是用户定义的类型。对于非成员运算符重载函数来说,运算符表达式左边的操作数对应于运算符函数的第一个参数,运算符表达式右边的操作数对应于运算符函数的第二个参数。
1 #include <iostream> 2 #include "Time.h" 3 Time operator-(const Time &,const Time &);//非成员运算符重载函数 4 void operator-=(Time &time1,const Time & time); 5 int main(int argc, const char * argv[]) { 6 Time t1{8,12,45}; 7 Time t2{18,45,25}; 8 Time t3 = t1 + t2; 9 t3.showTime(); 10 t3-= t2; 11 t3.showTime(); 12 t3 -=t1; 13 t3.showTime(); 14 15 return 0; 16 } 17 Time operator-( const Time & time1,const Time& time2){ 18 int hour = time1.getHour(); 19 int min = time1.getMin(); 20 int sec = time1.getSec(); 21 if (sec < time2.getSec()) { 22 min--; 23 sec = sec + 60 - time2.getSec(); 24 } 25 else 26 sec -= time2.getSec(); 27 if (min < time2.getMin()) { 28 hour--; 29 min = min + 60 - time2.getMin(); 30 } 31 else 32 min -= time2.getMin(); 33 if (hour < time2.getHour()) { 34 hour = hour - time2.getHour() + 24; 35 } 36 else 37 hour -= time2.getHour(); 38 39 return Time{hour,min,sec}; 40 } 41 void operator-=( Time &time1,const Time & time){ 42 time1 = time1 - time; 43 }
下面详细介绍C++对用户定义的运算符重载的限制:
(1)重载后的运算符必须至少有一个操作数是用户定义的类型,这将防止用户为标准类型重载运算符。
(2)使用运算符时不能违反运算符原来的句法规则。例如,不能将%重载成使用一个操作数。同样,不能修改运算符的优先级。
(3)不能创建新的运算符。
(4)不能重载下面的运算符:
* sizeof:szieof运算符;
* . :成员运算符(.);
* .* : 成员指针运算符;
* :: :作用域运算符;
* ?: :条件运算符;
* typeid:一个RTTI运算符;
* const_cast :强制类型转换运算符;
* dynamic_cast :强制类型转换运算符;
* reinterpret_cast:强制类型转换运算符;
* static_cast : 强制类型转换运算符;
(5)下面标中的运算符都可以被重载
可重载的运算符
+ | - | * | / | % | ^ |
& | | | ~= | ! | = | < |
> | += | -= | *= | /= | %= |
^= | &= | |= | << | >> | >>= |
<<= | == | != | <= | >= | && |
|| | ++ | -- | , | ->* | -> |
() | [] | new | delete | new[] | delete[] |
但是下面的运算符只能通过成员函数进行重载:赋值运算符(=)、函数调用运算符(())、下标运算符([])、通过指针访问类成员的运算符(->)。
二、友元
友元有三种:友元函数、友元类、友元成员函数。
1、友元函数
通过让函数成为类的友元,可以赋予该函数与类的成员函数相同访问权限。
由于成员运算符函数重载运算符后,该运算符左边的操作数只能是该对象;然而,当其他类型的操作数与对象运算时,如果其他类型操作数在运算符的左边,例如2*time(其中time为类对象),将会出现错误,因为其他类型的操作数无法调用对象的成员运算符重载函数。这时,可以声明定义非成员运算符重载函数,把函数的第一个参数设置为其他类型的参数,这样就可以与对象的成员运算符重载函数相配合,实现与2*time相似的表达式。
Time类中*的成员运算符重载函数的定义:
1 Time Time::operator*(unsigned int num)const{ 2 int hour,min,sec; 3 sec = _sec * num; 4 min = _min * num + sec/60; 5 hour = _hour * num + min /60; 6 min %= 60; 7 sec %= 60; 8 return Time{hour,min,sec}; 9 }
main.cpp:
1 #include <iostream> 2 #include "Time.h" 3 Time operator*(unsigned int , const Time &); 4 int main(int argc, const char * argv[]) { 5 Time t1{2,25,30}; 6 Time t2 = t1 * 2;//将会调用成员运算符重载函数 7 Time t3 = 3 * t1;//将会调用非成员运算符重载函数 8 using std::cout; 9 cout << "t1"; 10 t1.showTime(); 11 cout << "t2"; 12 t2.showTime(); 13 cout << "t3"; 14 t3.showTime(); 15 16 return 0; 17 } 18 Time operator*(unsigned int num, const Time & time){ 19 int hour, min,sec; 20 sec = time.getSec() * num; 21 min = time.getMin() * num + sec /60; 22 hour = time.getHour() * num + min/60; 23 sec %= 60; 24 min %= 60; 25 return Time{hour,min,sec}; 26 } 27 28 输出结果: 29 t1时间:02小时25分30秒 30 t2时间:04小时51分00秒 31 t3时间:07小时16分30秒
使用非成员函数可以按所需要的顺序获得操作数,但引发了一个新问题:常规非成员函数不能访问类的私有数据。C++提供了友元函数,一种特殊的非成员函数,可以访问类的私有数据。
(1)创建友元
第一步,将原型放在类声明中,并在原型声明前加上关键字friend。这样声明的函数原型,意味着一下两点:
*虽然该函数原型是在类声明中声明的,但是他不是类的成员函数,因此不能使用成员运算符来调用;
*虽然该函数不是类的成员函数,但它与成员函数的访问权相同。
第二部,编写函数定义。因为友元函数不是成员函数,因此不能使用类作用域限定符。另外,也不要在定义中使用关键字friend。
现在,对上面的Time对象乘以整数的例子进行改进:
Time.h文件:
1 #include <stdio.h> 2 class Time{ 3 private: 4 int _hour; 5 int _min; 6 int _sec; 7 public: 8 Time(int hour = 0, int min = 0, int sec = 0); 9 int getHour() const; 10 int getMin() const; 11 int getSec() const; 12 void showTime() const; 13 Time operator*(unsigned int num) const;//声明成员运算符重载函数 14 friend Time operator*(unsigned int, const Time &);//声明友元运算符重载函数 15 };
Time.cpp源文件:
1 #include "Time.h" 2 #include <iostream> 3 Time::Time(int hour, int min, int sec){ 4 _hour = hour; 5 _min = min; 6 _sec = sec; 7 } 8 int Time:: getHour() const{ 9 return _hour; 10 } 11 int Time:: getMin() const{ 12 return _min; 13 } 14 int Time:: getSec() const{ 15 return _sec; 16 } 17 void Time:: showTime()const{ 18 printf("时间:%02d小时%02d分%02d秒 ",_hour,_min,_sec); 19 } 20 Time Time::operator*(unsigned int num)const{//成员运算符重载函数的第一个操作数必须是该类对象 21 int hour,min,sec; 22 sec = _sec * num; 23 min = _min * num + sec/60; 24 hour = _hour * num + min /60; 25 min %= 60; 26 sec %= 60; 27 return Time{hour,min,sec}; 28 } 29 Time operator*(unsigned int num, const Time &time){//通过友元函数进行反转,定义友元函数的时候不能包含类作用域限定符,也不要包含friend关键 30 return time * num; 31 }
main.cpp:
1 #include <iostream> 2 #include "Time.h" 3 4 int main(int argc, const char * argv[]) { 5 Time t1{2,25,30}; 6 Time t2 = t1 * 2;//将会调用成员运算符重载函数 7 Time t3 = 3 * t1;//将会调用友元运算符重载函数 8 using std::cout; 9 cout << "t1"; 10 t1.showTime(); 11 cout << "t2"; 12 t2.showTime(); 13 cout << "t3"; 14 t3.showTime(); 15 16 return 0; 17 } 18 19 输出结果: 20 t1时间:02小时25分30秒 21 t2时间:04小时51分00秒 22 t3时间:07小时16分30秒
提示:如果要为类重载运算符,并将非类的项作为第一个操作数,则可以使用友元函数来反转操作数的顺序。
(2)常用的友元:重载<<运算符
一个很有用的特性是,可以对<<运算符进行重载,使之能与cout一起来显示对象的内容。
要使类知道使用cout,必须使用友元函数。因为像下面这样的语句使用两个对象,其中第一个是ostream类对象(cout):
cout << trip;
如果使用类成员函数来重载<<,类对象将是第一个操作数。就意味着应该这样使用<<:
trip << cout;
这样会使人迷惑。但是,通过友元函数,就可以反转操作数顺序。下面是在Time类中,实现友元<<运算符重载函数:
Time.h文件
1 #include <ostream> 2 3 class Time{ 4 private: 5 int _hour; 6 int _min; 7 int _sec; 8 public: 9 Time(int hour = 0, int min = 0, int sec = 0); 10 int getHour() const; 11 int getMin() const; 12 int getSec() const; 13 Time operator*(unsigned int num) const; 14 friend Time operator*(unsigned int, const Time &);//声明友元函数 15 friend std::ostream& operator<<(std::ostream &, const Time &);//声明友元<<运算符重载函数 16 17 };
Time.cpp文件:
1 #include "Time.h" 2 #include <iostream> 3 Time::Time(int hour, int min, int sec){ 4 _hour = hour; 5 _min = min; 6 _sec = sec; 7 } 8 int Time:: getHour() const{ 9 return _hour; 10 } 11 int Time:: getMin() const{ 12 return _min; 13 } 14 int Time:: getSec() const{ 15 return _sec; 16 } 17 18 Time Time::operator*(unsigned int num)const{//成员运算符重载函数的第一个操作数必须是该类对象 19 int hour,min,sec; 20 sec = _sec * num; 21 min = _min * num + sec/60; 22 hour = _hour * num + min /60; 23 min %= 60; 24 sec %= 60; 25 return Time{hour,min,sec}; 26 } 27 Time operator*(unsigned int num, const Time &time){//通过友元函数进行反转,定义友元函数的时候不能包含类作用域限定符,也不要包含friend关键 28 return time * num; 29 } 30 std::ostream& operator<<(std::ostream &os, const Time &time){//定义友元<<运算符重载函数 31 os << time.getHour() << "小时" << time.getMin() << "分" << time.getSec() << "秒"; 32 return os; 33 }
main.cpp文件:
1 #include <iostream> 2 #include "Time.h" 3 4 int main(int argc, const char * argv[]) { 5 Time t1{2,25,30}; 6 Time t2 = t1 * 2;//将会调用成员*运算符重载函数 7 Time t3 = 3 * t1;//将会调用友元*运算符重载函数 8 std::cout << "t1:" << t1 << " t2:" << t2 << " t3:" << t3; 9 10 return 0; 11 } 12 13 输出结果: 14 t1:2小时25分30秒 15 t2:4小时51分0秒 16 t3:7小时16分30秒
提示:一般来说,要重载<<运算符来显示c_name对象,可使用一个友元函数,其定义如下:
ostream & oprator<<(ostream & os, const c_name & obj){
os <<...;//显示对象的内容
return os;
}
三、重载运算符:作为成员函数还是非成员函数
对于很多运算符来说,可以选择成员函数或非成员函数来实现运算符重载。一般来说,非成员函数应是友元函数,这样他才能访问类的私有数据。
非成员版本的重载运算符函数所需的参数数目与运算符使用的操作数数目相同,所有的操作数作为函数参数显式地传递给非成员运算符重载函数;而成员版本所需的参数数目少一个,因为其中的一个参数是被隐式地传递的调用对象。
在定义作用于类对象的运算符重载函数时,究竟是使用成员函数还是非成员函数还是两者都适用的选择依据有一下两点:
(1)如果运算符的操作数类型一致,选择友元运算符重载函数或成员运算符重载函数中的一种,而不能同时选择这两种;否则将会报错。
(2)如果运算符的操作数类型不一致,例如一个是内置类型,另一个是类对象,那么用成员运算符重载函数实现类对象在运算符左边的重载功能,然后用友元运算符重载函数反转操作数顺序。
四、再谈重载:一个矢量类
五、类的自动转换和强制类型转换
将一个标准类型变量的值赋给另一种标准类型的变量时,如果这两种类型兼容,则C++自动将这个值转换为接收变量的类型。例如:
long count = 5;
double time = 11;
int side = 3.33;
上面的语句都是可行的。因为在C++看来,各种数值类型都表示相同的东西——一个数字,同时C++包含用于进行转换的内置规则。
但是,C++不自动转换不兼容的类型。例如,C++不会自动把int类型转换为指针,虽然他们都是整数,但是整数和指针完全不同:int *ptr = 13;//这样语句是非法的
然而,在无法自动转换时,可以使用强制类型转换。例如,int *ptr = (int *)12;//将整数强制转换为指针后,再赋给指针是可以的
可以将类定义成与基本类型或另一个类相关,使得从一种类型转换成另一种类型是有意义的。在这种情况下,程序员可以指示C++如何自动进行转换,或通过强制类型转换来完成。
下面设计了两个类,类KG以“千克”为单位来表示质量,类Jin以“斤”为单位来表示质量,并且设计了他们之间的相会转换
KG.h
1 #include <iostream> 2 3 class KG{ 4 private: 5 float weight_; 6 public: 7 KG(); 8 KG(float weight);//定义了float到KG类型的转换。 9 float getWeight()const; 10 friend std::ostream& operator<<(std::ostream& os,const KG &); 11 };
KG.cpp
1 #include "KG.h" 2 #include <ostream> 3 KG::KG(){ 4 weight_ = 0; 5 } 6 KG::KG(float weight){ 7 weight_ = weight; 8 } 9 /*KG::KG(Jin jin){ 10 weight_ = jin.getWeight()/2; 11 }*/ 12 float KG::getWeight()const{ 13 return weight_; 14 } 15 std::ostream& operator<<(std::ostream& os, const KG &kg){ 16 os << kg.getWeight() << "千克"; 17 return os; 18 }
Jin.h
1 #include <iostream> 2 #include "KG.h" 3 4 class Jin{ 5 private: 6 float weight_; 7 public: 8 Jin(); 9 Jin(float weight); 10 Jin(KG kg); 11 float getWeight() const; 12 friend std::ostream& operator<<(std::ostream & os, const Jin & jin); 13 operator KG()const;//转换函数 14 };
Jin.cpp
1 #include "Jin.h" 2 Jin::Jin(){ 3 weight_ = 0; 4 } 5 Jin::Jin(float weight){ 6 weight_ = weight; 7 } 8 Jin::Jin(KG kg){ 9 weight_ = kg.getWeight() * 2; 10 } 11 float Jin::getWeight()const{ 12 return weight_; 13 } 14 std::ostream& operator<<(std::ostream & os, const Jin & jin){ 15 os << jin.getWeight() << "斤"; 16 return os; 17 } 18 Jin::operator KG()const{ 19 return weight_/2; 20 }
main.cpp
1 #include <iostream> 2 #include "Jin.h" 3 4 int main(int argc, const char * argv[]) { 5 KG kg1 = 22.5,kg2; 6 Jin jin1 = 90, jin2;//这里,先将int类型的90转换为float,然后使用Jin(float)构造函数 7 kg2 = jin1; 8 jin2 = kg1;//程序用Jin的构造函数Jin(KG)来创建一个临时的Jin对象,并将kg1作为初始化值。然后采用逐成员赋值方式将该临时对象的内容复制到jin2中,这一过程称为隐式转换,因为它是自动进行的 9 std::cout <<" kg1:" << kg1 << " kg2:"<< kg2 << " jin1:" << jin1 << " jin2:" << jin2 << std::endl; 10 11 return 0; 12 } 13 14 15 输出结果: 16 kg1:22.5千克 17 kg2:45千克 18 jin1:90斤 19 jin2:45斤
说明:
(1)在C++中,接受一个参数的构造函数为将类型与该参数相同的值转换为类提供了蓝图。例如,Jin类的构造函数Jin(KG kg)为将KG类型的变量转换为Jin类提供了可能。
(2)只有接受一个参数的构造函数才能作为转换函数。
(3)将构造函数作为自动类型转换函数有时可能会导致意外的类型转换,C++关键字explicit用于关闭这种自动特性。也就是说这样声明构造函数:
explicit Jin(KG);
将关闭上面例子中介绍的隐式转换,但仍然允许显示转换,即显式强制类型转换:
KG kg0 = 40;
Jin ji = (Jin)kg0;
Jin ji2 = Jin(kg0);
注意:只接受一个参数的构造函数定义了从参数类型到类类型的转换。如果使用关键字explicit限定了这种构造函数,则它只能用于显式转换,否则也可以用于隐式转换。
(4)函数原型化提供的参数匹配过程,允许使用Jin(float)、 KG(float)构造函数来转换其他数值类型。例如,Jin ji = 12;先将int类型的12转换为float,然后使用Jin(float)构造函数。然而,当且仅当不存在二义性时,才会进行这种二步转换。即如果类Jin还定义了构造函数Jin(long),则编译器将拒绝这些语句,可能指出:int可被转换为long或float,因此存在二义性。
1、转换函数
构造函数只用于将某种类型到类类型的转换,要进行反向的转换,必须使用C++的运算符函数——转换函数。
转换函数是用户定义的强制类型转换,可以像使用强制类型转换那样使用它们。
要将类类型转换为typeName类型,需要使用这种形式的转换函数:
oprator typeName();
注意以下几点:
1)转换函数必须是类方法;
2)转换函数不能指定返回类型;
3)转换函数不能有参数。
typeName指出了要转换成的类型,因此不需要指定返回类型。转换函数是类方法意味着:它需要通过类对象来调用,从而告知函数要转换的值。因此,函数不需要参数。
当类定义了两种或更多种的转换时,可能会出现二义性,例如,类Jin有operator int()和operator float()着两种转换函数,编译器认为下面的语句有二义性而拒绝他:
Jin ji = 12.4;
double a = ji;
因为int和float类型都可以赋值给double变量,所以编译器使用任何一种转换都是合法的,而编译器不承担选择函数的责任,这样就出现了二义性。但是,仍然可以使用显式强制类型转换来指出要使用哪个转换函数。
和转换构造函数一样,转换函数也有其优缺点。提供执行自动、隐式转换的函数所存在的问题是:在用户不希望进行转换时,转换函数也可能进行转换。原则上,应尽量避免隐式转换,使用显式转换。在C++11中,与关闭转换构造函数的隐式转换功能一样,声明转换函数时在其前面加上explicit关键字来关闭转换函数的隐式转换功能。
总结,C++为类提供了下面的转换函数:
*只有一个参数的构造函数用于将类型与该参数类型相同的值转换为类类型。然而,在构造函数声明中使用explicit关键字可以防止隐式转换,而只允许显式转换。
*被称为转换函数的特殊类成员运算符函数,用于将类对象转换为其他类型。转换函数是类成员、没有返回值、没有参数、名为operator typeName(),其中,typeName是类将被转换成的类型。将类对象赋给typeName变量或将其强制转换为typeName类型时,该转换函数将自动被调用。