友元
类可以允许其他类或者函数访问它的非公有成员,方法是令其他类或者函数成为他的友元(friend)。如果类想把一个函数作为它的友元,只需要增加一条以friend关键字开始的函数声明语
即可:
class Sales_data {
//为Sales_data的非成员函数所做的友元声明
friend Sales_data add(const Sales_data&, const Sales_data&);
friend std::istream &read(std::istream&, Sales_data&);
friend std::ostream &print(std::ostream&, const Sales_data&);
//其他成员及访问说明符与之前一致
public :
Sales_data()=default;
Sales_data(const std::string &s,unsigned n,double p):bookNo(s),units_sold(n),revenue(p*n){}
Sales_data(const std::string &s):bookNo(s){}
Sales_data(std::istream&);
std::string isbn()const {return bookNo;}
Sales_data &combine(const Sales_data&);
private :
std::string bookNo;
unsigned units_sold=0;
double revenue =0.0;
};
//Sales_data接口的非成员组成部分的声明
Sales_data add(const Sales_data&,const Sales_data&);
std::istream &read(std::istream&,Sales_data&);
std::ostream &print(std::ostream&,const Sales_data&);
友元声明只能出现在类定义的内部,但是在类内出现的具体位置不限。友元不是类的成员,也不受它所在区域访问控制级别的约束。
/*一般来说,最好在类定义开始或结束前的位置集中声明友元。*/
友元的声明仅仅指定了访问的权限,而非一个通常意义上的函数声明。如果我们希望类的用户能够调用某个友元函数,那么我们就必须在友元声明之外再专门对函数进行一次声明。
为了使友元对类的用户可见,我们通常把友元的声明与类本身放置在同一个头文件中(类的内部)。因此,我们的Sales_data头文件应该为read、print和add提供独立的声明(除了类内部的友
元声明之外)。
/*许多编译器并未强制限定友元函数必须在使用之前在类的外部声明。*/
一些编译器允许在尚无友元函数的初始声明的情况下就调用它。不过即使你的编译器支持这种行为,最好还是提供一个独立的函数声明。这样即使你更换了一个有这种强制要求的编译器,也不必
改变代码。
类还可以把其他的类定义成友元,也可以把其他类(之前已经定义过的)的成员函数定义成友元。此外,友元函数能定义在类的内部,这样的函数是隐式内联的。
类之间的友元关系
举个友元类的例子,我们的window_mgr类的某些成员可能需要访问它管理的Screen类的内部数据。例如,假设我们需要为window_mgr添加一个名为clear的成员,它负责把一个指定的Screen的内容都设为空白。为了完成这一任务,clear需要访问Screen的私有成员;而想要令这种访问合法,Screen需要把window_mgr指定成它的友元:
class Screen{
//window_mgr的成员可以访问Screen类的私有部分
friend class window_mgr;
// Screen类的剩余部分
};
如果一个类指定了友元类,则友元类的成员函数可以访问此类包括非公有成员在内的所有成员。通过上面的声明,window_mgr被指定为Screen的友元,因此我们可以将window_mgr的clear成员写成如下的形式:
class window_mgr{
public :
//窗口中每个屏幕的编号
using ScreenIndex = std::vector<Screen>::size_type;
//按照编号将指定的Screen重置为空白
void clear(ScreenIndex);
private :
std::vector<Screen> screens{Screen(24,80,' ')};
};
void window_mgr::clear(ScreenIndex i){
//s 是一个Screen的引用,指向我们想清空的那个屏幕
Screen &s = screens[i];
s.contents=string(s.height*s.width,' ');
}
一开始,首先把s定义成screen vector中第i个位置上的Screen的引用,随后利用Screen的height和width成员计算出一个新的string对象,并令其含有若干个空白字符,最后我们把这个含有很多空白字符的字符串赋给contents成员。
如果clear不是Screen的友元,上面的代码将无法通过编译,因为此时clear将不能访问Screen的height、width的contents成员。而当Screen将window_mgr指定为其友元之后,Screen的所有成员对于window_mgr就都变成可见的了。
必须要注意的一点是,友元关系不存在传递性。也就是说,如果window_mgr有它自己的友元,则这些友元并不能理所当然地具有访问Screen的特权。
每个类负责控制自己的友元类或友元函数。
令成员函数作为友元
除了令整个window_mgr作为友元之外,Screen还可以只为clear提供访问权限。当把一个成员函数声明成友元时,我们必须明确指出该成员函数属于哪个类:
class Screen{
//window_mgr::clear必须在Screen类之前被声明
friend void window_mgr::clear(ScreenIndex);
//Screen类的剩余部分
};
要想令某个成员函数作为友元,我们必须仔细组织程序的结构以满足声明和定义的彼此依赖关系。在这个例子中,我们必须按照如下方式设计程序:
·首先定义window_mgr类,其中声明clear函数,但是不能定义它。在clear使用Screen的成员之前必须先声明Screen。
·接下来定义Screen,包括对于clear的友元声明。
·最后定义clear,此时它才可以使用Screen的成员。
函数重载和友元
尽管重载函数的名字相同,但他们仍然是不同的函数。因此,如果一个类想把一组重载函数声明成它的友元,它需要对这组函数中的每一个分别声明:
//重载的storeOn函数
extern std::ostream &storeOn(std::ostream &,Screen &);
extern BitMap& storeOn(BitMap &,Screen &);
class Screen{
//storeOn 的ostream版本能访问Screen对象的私有部分‘
friend std::ostream& storeOn(std::ostream &,Screen &);
//...
};
Screen类把接受ostream&的storeOn函数声明成它的友元,但是接受BitMap&作为参数的版本仍然不能访问Screen。
友元声明和作用域
类和非成员函数的声明不是必须在它们的友元声明之前。当一个名字第一次出现在一个友元声明中,我们隐式地假定该名字在当前作用域中是可见的。然而,友元本身不一定真的声明在当前作用域中。
甚至就算在类的内部定义该函数,我们也必须在类的外部提供相应的声明从而使得函数可见。换句话说,即使我们仅仅是用声明友元的类的成员调用该友元函数,它也必须是被声明过的:
struct X{
friend void f(){/*友元函数可以定义在类的内部*/}
X(){f();} //错误:f还没有被声明
void g();
void h();
};
void X::g(){return f();} //错误:f还没有被声明
void f(); //声明那个定义在X中的函数
void X::h(){return f();} //正确:现在f的声明在作用域中了
关于这段代码最重要的是理解友元声明的作用是影响访问权限,它本身并非普通意义上的声明。
请注意,有的编译器并不强制执行上述关于友元的限定规则