c++入门之—运算符重载和友元函数
运算符重载的意义是:将常见的运算符重载出其他的含义:比如将*重载出指针的含义,将<<与cout联合使用重载出输出的含义,但需要认识到的问题是:运算符的重载:本质仍然是成员函数,即你可以认为:发生运算符重载时,实际上,是调用了成员函数,只不过重新换了个名字,叫运算符重载。
友元函数的意义:使得我们能够像成员函数一样访问私有成员变量,不会受到“权限”。下面通过一个函数来认识这一点:
1 # ifndef MYTIME3_H_H 2 # define MYTIME3_H_H 3 # include "iostream" 4 class Time 5 { 6 private: 7 int hours; 8 int minutes; 9 public: 10 Time(); 11 Time(int h, int m = 0); 12 void Addmin(int m); 13 void AddHr(int h); 14 void Reset(int h = 0, int m = 0); 15 Time operator+(const Time & t) const;//const放在后面表示不能修改成员函数 16 Time operator-(const Time & t) const; 17 Time operator*(double n) const; 18 friend Time operator*(double m, const Time & t) 19 { 20 return t*m; 21 }//类声明中定义的函数都是内联函数??? 22 friend std::ostream & operator<< (std::ostream & os, const Time & t); 23 }; 24 #endif
上述是一个关于类声明的头文件,在此,再次强调这么几个东西:
1 10,11行描述了构造函数(运用了函数重载),构造函数的存在意义是完成对类对象的初始化,在定义自己的类对象的时候,一定要保证了自己的类对象经过了初始化(实际上,在穿件类的时候,首先就会调用构造函数,完成初始化,即使没定义初始化函数,系统会自定生成一个初始化函数)
2 对于15-22行的代码,实际上都运用了运算符的重载,+,-,*,<<都被重载,至少从这里可以看出,运算符的重载,本质上,仍然是函数的调用。
3 18,22行的函数声明为友元函数,可以发现,friend 为关键字。同时注意:17行,18行代码,构成了函数重载!!!因为他们使用了同一个函数名,虽然这其中使用了运算符重载。这两个函数都描述了*运算,这也可以看出,重载函数,往往描述了同一种功能。
4 一个有趣的做法是:22行 的函数返回了一个ostream &,在之后的调用中,我们可以体会到这种妙用。
继续看成员函数定义:
1 # include "mytime3.h" 2 3 Time::Time() 4 { 5 hours = minutes = 0; 6 } 7 8 Time::Time(int h, int m) 9 { 10 hours = h; 11 minutes = m; 12 } 13 14 void Time::Addmin(int m) 15 { 16 minutes = minutes + m; 17 hours = minutes / 60; 18 minutes = minutes % 60; 19 } 20 21 void Time::AddHr(int h) 22 { 23 hours = hours + h; 24 } 25 26 void Time::Reset(int h, int m) 27 { 28 hours = h; 29 minutes = m; 30 } 31 Time Time::operator+(const Time& t) const 32 { 33 Time sum; 34 sum.minutes = minutes + t.minutes; 35 sum.hours = hours + t.hours + sum.minutes / 60; 36 sum.minutes = sum.minutes % 60; 37 return sum;//思考这里有引入Time对象的必要吗 38 } 39 40 Time Time::operator-(const Time& t) const 41 { 42 Time diff; 43 int tot1,tot2; 44 tot1 = t.minutes + 60 * t.hours; 45 tot2 = minutes + 60 * hours; 46 diff.hours = (tot2 - tot1) / 60; 47 diff.minutes = (tot2 - tot1) % 60; 48 return diff; 49 } 50 51 Time Time::operator*(double mult) const 52 { 53 Time result; 54 long totalminutes = hours*mult * 60 + minutes*mult; 55 result.hours = totalminutes / 60; 56 result.minutes = totalminutes % 60; 57 return result; 58 } 59 60 std::ostream & operator<<(std::ostream& os, const Time & t) 61 { 62 os << t.hours << "hours," << t.minutes << "minutes"; 63 return os; 64 }
需要注意的是:51行的*并不是描述友元函数那个,而是另外一个。同时注意到63行,返回了一个类对象的引用,返回引用,本质上其实就是返回了传递到引用的参数!!!
我们注意到:62中,我们直接访问了t.hour和t.minutes,这是因为我们定义的是友元函数,同时注意到,前面并没有Time::类作用域的限定。这说明了友元函数并不是成员函数。注意:函数定义中并没有使用friend关键字。
最后看实际上,调用了这个类的代码:
1 # include <iostream> 2 # include "mytime3.h" 3 4 int main() 5 { 6 using std::cout; 7 using std::endl; 8 Time aida(3, 35); 9 Time tosca(2, 48); 10 Time temp; 11 12 cout << "Aida and Tosca:" << endl; 13 cout << aida << ";" << tosca << endl; 14 temp = aida + tosca; 15 cout << "Aida + Tosca:" << temp << endl; 16 temp = aida*1.17; 17 cout << "Aida *1.17:" << temp << endl; 18 cout << "10.0*Tosca::" << 10.0*tosca << endl; 19 system("pause"); 20 return 0; 21 }
注意:第十行的 对象,被“隐“初始化
这里尤其要注意的是16行的代码,和18行的区别,当执行16行的代码时,系统会调用第一个*函数,当执行18行的代码时,执行的却是二个*函数。同时,我们注意:cout可以用来打印类对象,根本原因是std::ostream & operator<<(std::ostream& os, const Time & t)导致的,同时注意,该函数中返回了一个类对象引用,返回对象引用的目的是:返回cout本身,这就导致了比如定义了两个对象:Time A ,Time B ,当执行 cout<<A<<B时不会出错,因为(cout<<A)执行完毕返回了一个cout继续作用于B。你可以尝试删除return 。但是要运用这个的前提是:你认识到运算符重载本质上,仍然是函数的调用!!!只有认识到这点,才会将cout<<A<<B看成((cout<<A)<<B)
但问题在于:友元函数存在的意义又何在呢?
在这个程序中,有两个地方使用了友元函数,一个地方是:在计算诸如类对象 A*1.17或者1.17* 类对象 A,站在我们的角度看,这两者应该是相同的,但是在程序执行的时候,却完全不同。
比如:在执行A*1.17的时候,我们看会发生什么:上面讲过,运算符重载本质是一种函数调用,因此当看到(类*double)的时候,很明显基本规则中并没有这种方式的运算(因为我们都知道在基本类型中有一种数据类型要匹配的概念)。那么此时,程序就会认为这是一个运算符重载的案例,需要调用其函数。而这里涉及到了函数重载(两个operatorb*),此时会根据函数重载的准则选择最匹配的进行函数调用,这里选择了函数定义中的51行的定义:Time Time::operator*(double mult) const,很明显,这里没有调用友元函数那个。
此时,来解读这个函数:我们可以看到,这个函数定义只有一个显示参数mult。我们看调用时,发生了什么:当执行A*1.17,实际上,执行了A operator*(1.17),站在函数的角度可以认为:A对象作为隐式参数被传递到了函数中。也就是是:左侧的操作数是调用的对象,而对象被隐式传参了。
当我们要执行1.17*A的时候,又当如何呢???
当我们去试图去调用 Time Time::operator*(double mult) const,发现1.17并不是对象,再次声明:运算符重载中左侧的操作数是调用对象!!!此时,我们只能调用另一个operator*函数。但问题在于:似乎就算我们不需要这个友元函数,在成员函数中进行函数重载,定义两个非运算符重载的函数,让重载函数的特征标的顺序不同,照样可以完成这个功能。因此这里并不能很好的说明友元函数的存在意义。
但如果我们要完成cout<<类型A<<类型B;且必须采用这种方式,又当如何呢???
对于此处友元函数定义中,我们采用了friend std::ostream & operator<< (std::ostream & os, const Time & t); 因为要连续使用cout,因此函数的返回类型是ostream & ,.假如,我们去掉friend,变成:std::ostream & operator<< (std::ostream & os, const Time & t); 则编译器报错,说:operator运算符的参数太多,很明显,我们在这里要显示t,假如像之前一样,将t作为隐式参数是否可以呢? 即std::ostream & operator<< (std::ostream & os);此时倒是没报错,但是:这时请再次记住这句话:运算符重载中左侧的操作数是调用对象。也就是说这个时候变成了t<<cout!!!,显然这并不是我们想要的结果。我们甚至可以通过修改函数定义,来满足t<<cout,比如将函数定义改为:
1 std::ostream & Time:: operator<<(std::ostream& os) 2 { 3 os << hours << "hours," << minutes << "minutes"; 4 return os; 5 }
这样,我们可以使用t<<cout了,但如果我们要实现:cout\<<A<<B这样的东西,却发现实现不了(读者可自行尝试)。而且采用t<<cout,显然有悖程序设计原则:简洁而统一。
因此,我们必须使用友元函数。来实现cout<<A<<B.这里来回顾一下,使用友元函数实现这一目的的根本原因:
我们要使用运算符重载来实现显示,而运算符重载必然导致了左侧的操作数是调用的对象,而且运算符重载,显示参数只能有一个,隐式参数是对象本身,且位于运算符左侧,这才是根本限制!!
说明一下:非成员版本的重载运算符函数所需的形参数目与运算符使用的操作数数目相同(如友元函数),而成员函数所需参数少一个,因为其中一个参数是被隐式地传递的调用对象(通过this指针)。