C++友元详解:友元函数与重载<<运算符
在C++中,友元(friend)是一种特殊的访问权限机制,它允许外部函数或类访问当前类的私有成员。今天我们就来深入探讨友元函数,并通过一个完整的示例演示如何使用友元函数重载运算符,特别是重载
<<运算符来实现自定义对象的输出。
一、为什么需要友元?
通常情况下,类的私有成员只能通过公有成员函数访问,这种封装性保证了数据的安全性。但在某些场景下,这种限制显得过于严格,例如:
- 重载双目运算符时,如果左侧操作数不是本类对象,就无法使用成员函数版本。
- 希望将输出运算符
<<重载,以便直接用cout << 对象的方式打印对象内容。
这时候,友元就派上了用场。
二、友元的三种形式
C++支持三种友元:
- 友元函数
- 友元类
- 友元成员函数
本文重点介绍友元函数,其他两种将在后续文章中讲解。
三、何时需要友元函数?
来看一个典型的例子:为Time类重载乘法运算符,使其支持Time对象乘以一个double值。
成员函数版本的问题
假设我们有如下成员函数:
Time Time::operator*(doublen)const;那么下面的语句可以正常工作:
A=B*2.75;// 相当于 A = B.operator*(2.75)但下面的语句却无法通过编译:
A=2.75*B;// 左侧不是Time对象,无法调用成员函数因为左侧操作数必须是调用对象,而2.75不是Time类型。
非成员函数的思路
我们可以使用非成员函数来解决顺序问题:
Timeoperator*(doublem,constTime&t);这样2.75 * B就会匹配到这个函数。但问题来了:非成员函数不能直接访问类的私有数据!
此时就需要友元函数——它既不是成员函数,却拥有和成员函数一样的访问权限。
四、创建友元函数
第一步:在类声明中添加原型,前面加上关键字friend
friendTimeoperator*(doublem,constTime&t);注意:
friend只出现在类声明中的原型里。- 该函数不是成员函数,不能用成员运算符调用。
- 但它与成员函数具有相同的访问权限。
第二步:编写函数定义(不加friend,也不加类限定符)
Timeoperator*(doublem,constTime&t){Time result;longtotalminutes=t.hours*m*60+t.minutes*m;result.hours=totalminutes/60;result.minutes=totalminutes%60;returnresult;}现在就可以使用2.75 * B了。
更巧妙的实现
其实我们可以让友元函数直接调用成员函数版本,从而避免直接访问私有数据:
Timeoperator*(doublem,constTime&t){returnt*m;// 调用成员函数 operator*(double)}这样既保持了友元关系(作为接口的一部分),又利用了已有的成员函数。
五、常用的友元:重载<<运算符
重载<<运算符是友元函数最经典的应用之一。我们希望实现:
cout<<trip;// trip是Time对象为什么必须是友元?
如果使用成员函数重载<<,第一个操作数必须是Time对象,那就得写成:
trip<<cout;// 太反直觉了!所以只能用非成员函数,而且通常需要访问私有数据,因此友元是理想选择。
第一个版本:输出内容
voidoperator<<(ostream&os,constTime&t){os<<t.hours<<" hours, "<<t.minutes<<" minutes";}这样cout << trip;就能工作了。
第二个版本:支持链式输出
上面的版本无法实现类似cout << "Time: " << trip << endl;的链式输出,因为operator<<返回void。要支持链式,必须返回ostream对象的引用。
ostream&operator<<(ostream&os,constTime&t){os<<t.hours<<" hours, "<<t.minutes<<" minutes";returnos;}现在就可以愉快地链式输出了:
cout<<"Trip time: "<<trip<<" (Tuesday)\n";扩展到文件输出
由于ostream是基类,这个函数也能用于ofstream对象,将数据写入文件:
ofstreamfout("savetime.txt");fout<<trip;六、完整代码示例
下面给出一个完整的Time类定义,包含友元函数重载*和<<。
mytime3.h
#ifndefMYTIME3_H_#defineMYTIME3_H_#include<iostream>classTime{private:inthours;intminutes;public:Time();Time(inth,intm=0);voidAddMin(intm);voidAddHr(inth);voidReset(inth=0,intm=0);Timeoperator+(constTime&t)const;Timeoperator-(constTime&t)const;Timeoperator*(doublen)const;// 友元函数:重载 * 以支持 double * TimefriendTimeoperator*(doublem,constTime&t){returnt*m;// 调用成员函数}// 友元函数:重载 <<friendstd::ostream&operator<<(std::ostream&os,constTime&t);};#endifmytime3.cpp
#include"mytime3.h"Time::Time():hours(0),minutes(0){}Time::Time(inth,intm):hours(h),minutes(m){}voidTime::AddMin(intm){minutes+=m;hours+=minutes/60;minutes%=60;}voidTime::AddHr(inth){hours+=h;}voidTime::Reset(inth,intm){hours=h;minutes=m;}Time Time::operator+(constTime&t)const{Time sum;sum.minutes=minutes+t.minutes;sum.hours=hours+t.hours+sum.minutes/60;sum.minutes%=60;returnsum;}Time Time::operator-(constTime&t)const{Time diff;inttot1=t.minutes+60*t.hours;inttot2=minutes+60*hours;diff.minutes=(tot2-tot1)%60;diff.hours=(tot2-tot1)/60;returndiff;}Time Time::operator*(doublemult)const{Time result;longtotalminutes=hours*mult*60+minutes*mult;result.hours=totalminutes/60;result.minutes=totalminutes%60;returnresult;}std::ostream&operator<<(std::ostream&os,constTime&t){os<<t.hours<<" hours, "<<t.minutes<<" minutes";returnos;}usetime3.cpp(测试程序)
#include<iostream>#include"mytime3.h"intmain(){usingstd::cout;usingstd::endl;Timeaida(3,35);Timetosca(2,48);Time temp;cout<<"Aida and Tosca:\n";cout<<aida<<"; "<<tosca<<endl;temp=aida+tosca;cout<<"Aida + Tosca: "<<temp<<endl;temp=aida*1.17;cout<<"Aida * 1.17: "<<temp<<endl;cout<<"10.0 * Tosca: "<<10.0*tosca<<endl;return0;}运行结果
Aida and Tosca: 3 hours, 35 minutes; 2 hours, 48 minutes Aida + Tosca: 6 hours, 23 minutes Aida * 1.17: 4 hours, 11 minutes 10.0 * Tosca: 28 hours, 0 minutes七、总结
- 友元函数提供了成员函数与外部函数之间的桥梁,使得非成员函数也能访问类的私有成员。
- 常用于重载运算符,特别是当左侧操作数不是本类对象时(如
double * Time)或需要与标准输出流配合时(如重载<<)。 - 友元函数不是成员函数,但在类中声明时需加
friend关键字,定义时则不加。 - 重载
<<时应返回ostream&以支持链式输出,这是C++中非常实用的惯用法。
掌握友元,让你的C++类接口更加灵活、自然!